├── .editorconfig ├── .gitignore ├── after └── ftplugin │ ├── c.lua │ └── gitcommit.lua ├── ftplugin └── qf.lua ├── init.lua ├── lazy-lock.json ├── lua ├── keymaps.lua ├── plugins │ ├── code_llm.lua │ ├── dap.lua │ ├── editing.lua │ ├── formatting.lua │ ├── git.lua │ ├── language.lua │ ├── language │ │ └── markdown.lua │ ├── lsp │ │ ├── clients.lua │ │ └── init.lua │ ├── misc.lua │ ├── navigation.lua │ ├── snacks.lua │ ├── status_items.lua │ ├── treesitter.lua │ └── ui.lua ├── settings.lua └── utilities │ └── os.lua └── plugin ├── autocd.lua ├── autocmds.lua ├── delete_hidden_buffers.lua ├── duplicate_and_comment.lua ├── lastplace.lua ├── project_manager.lua ├── range_hl.lua ├── tabline.lua ├── to_buffer.lua └── yank_without_indent.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | [*.lua] 6 | indent_style = space 7 | indent_size = 4 8 | max_line_length = 120 9 | quote_type = single 10 | call_parentheses = Always 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .luarc.json 2 | spell/ 3 | -------------------------------------------------------------------------------- /after/ftplugin/c.lua: -------------------------------------------------------------------------------- 1 | -- Use single line comments for C files 2 | vim.o.commentstring = '// %s' 3 | -------------------------------------------------------------------------------- /after/ftplugin/gitcommit.lua: -------------------------------------------------------------------------------- 1 | vim.o.textwidth = 72 2 | -------------------------------------------------------------------------------- /ftplugin/qf.lua: -------------------------------------------------------------------------------- 1 | local function RemoveQuickFixEntry() 2 | local line = vim.api.nvim_win_get_cursor(0)[1] 3 | local qflist = vim.fn.getqflist() 4 | 5 | -- Remove line from qflist. 6 | table.remove(qflist, line) 7 | vim.fn.setqflist(qflist, 'r') 8 | 9 | -- Restore cursor position. 10 | local max_lines = vim.api.nvim_buf_line_count(0) 11 | vim.api.nvim_win_set_cursor(0, { math.min(line, max_lines), 0 }) 12 | end 13 | 14 | -- Allow easily removing quickfix items. 15 | vim.keymap.set('n', 'dd', RemoveQuickFixEntry, { buffer = 0, silent = true }) 16 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- Enable the experimental Lua module loader 2 | vim.loader.enable() 3 | 4 | local fn = vim.fn 5 | local lazypath = fn.stdpath('data') .. '/lazy/lazy.nvim' 6 | local os_utils = require('utilities.os') 7 | 8 | local CheckConfigDeps 9 | local LoadPlugins 10 | 11 | -- Load settings and keybinds 12 | require('settings') 13 | require('keymaps') 14 | 15 | --- Check if dependencies for Neovim config are installed before bootstrapping the config. 16 | CheckConfigDeps = function() 17 | -- Ensure that the OS is Windows, Mac or Linux. 18 | if not os_utils.is_linux() and not os_utils.is_macos() and not os_utils.is_windows() then 19 | error('Neovim configuration does not support the OS ' .. vim.uv.os_uname().sysname) 20 | end 21 | 22 | local deps = { 23 | { exe = 'rg', reason = 'Live grep' }, 24 | { exe = 'fd', reason = 'File search' }, 25 | { exe = 'fzf', reason = 'Fuzzy finder' }, 26 | { exe = 'node', reason = 'Tree-sitter and LSP' } 27 | } 28 | 29 | local missing_deps = false 30 | 31 | for _, dep in ipairs(deps) do 32 | if fn.executable(dep.exe) == 0 then 33 | vim.notify('Missing ' .. dep.exe .. ' required for ' .. dep.reason, vim.log.levels.ERROR) 34 | missing_deps = true 35 | end 36 | end 37 | 38 | if missing_deps then 39 | error('Missing dependencies') 40 | end 41 | end 42 | 43 | LoadPlugins = function() 44 | vim.opt.rtp:prepend(lazypath) 45 | 46 | require('lazy').setup({ import = 'plugins' }, { 47 | git = { 48 | timeout = -1, -- Disable timeout. 49 | }, 50 | dev = { 51 | path = vim.uv.os_homedir() .. '/Dev/neovim', 52 | fallback = true, 53 | }, 54 | concurrency = require('utilities.os').pu_count(), 55 | }) 56 | end 57 | 58 | -- Check to see if config dependencies are found. 59 | CheckConfigDeps() 60 | 61 | -- Bootstrap lazy.nvim if required. 62 | if not vim.uv.fs_stat(lazypath) then 63 | fn.system({ 64 | 'git', 65 | 'clone', 66 | '--filter=blob:none', 67 | 'https://github.com/folke/lazy.nvim.git', 68 | '--branch=stable', 69 | lazypath, 70 | }) 71 | end 72 | 73 | LoadPlugins() 74 | -------------------------------------------------------------------------------- /lazy-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "avante.nvim": { "branch": "main", "commit": "87c4c6b4937d1884960759aba4a0e42645688f2f" }, 3 | "blink.cmp": { "branch": "main", "commit": "196711b89a97c953877d6c257c62f18920a970f3" }, 4 | "catppuccin": { "branch": "main", "commit": "be1e5e6308bb9d016bf5c1565e0f1d5e46400d7a" }, 5 | "conform.nvim": { "branch": "master", "commit": "6feb2f28f9a9385e401857b21eeac3c1b66dd628" }, 6 | "copilot.lua": { "branch": "master", "commit": "a620a5a97b73faba009a8160bab2885316e1451c" }, 7 | "diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" }, 8 | "dropbar.nvim": { "branch": "master", "commit": "f7ecb0c3600ca1dc467c361e9af40f97289d7aad" }, 9 | "fidget.nvim": { "branch": "main", "commit": "d9ba6b7bfe29b3119a610892af67602641da778e" }, 10 | "flash.nvim": { "branch": "main", "commit": "3c942666f115e2811e959eabbdd361a025db8b63" }, 11 | "friendly-snippets": { "branch": "main", "commit": "572f5660cf05f8cd8834e096d7b4c921ba18e175" }, 12 | "gitsigns.nvim": { "branch": "main", "commit": "8b729e489f1475615dc6c9737da917b3bc163605" }, 13 | "harpoon": { "branch": "harpoon2", "commit": "ed1f853847ffd04b2b61c314865665e1dadf22c7" }, 14 | "helpview.nvim": { "branch": "main", "commit": "8df486915a29483c7955067a7c17bffdf3b1e5f5" }, 15 | "hover.nvim": { "branch": "main", "commit": "07c7269c3a88751f2f36ed0563dc6e7b8b84f7f7" }, 16 | "lazy.nvim": { "branch": "main", "commit": "6c3bda4aca61a13a9c63f1c1d1b16b9d3be90d7a" }, 17 | "lualine.nvim": { "branch": "master", "commit": "0c6cca9f2c63dadeb9225c45bc92bb95a151d4af" }, 18 | "markview.nvim": { "branch": "main", "commit": "68c9603b6f88fd962444f8579024418fe5e170f1" }, 19 | "mason.nvim": { "branch": "main", "commit": "8024d64e1330b86044fed4c8494ef3dcd483a67c" }, 20 | "mini.ai": { "branch": "main", "commit": "b91997d220086e92edc1fec5ce82094dcc234291" }, 21 | "mini.align": { "branch": "main", "commit": "969bdcdf9b88e30bda9cb8ad6f56afed208778ad" }, 22 | "mini.surround": { "branch": "main", "commit": "5aab42fcdcf31fa010f012771eda5631c077840a" }, 23 | "neogit": { "branch": "master", "commit": "f48912295e86065e84808bbc85619fb6e7fcbc0e" }, 24 | "nui.nvim": { "branch": "main", "commit": "f535005e6ad1016383f24e39559833759453564e" }, 25 | "nvim-dap": { "branch": "master", "commit": "b0f983507e3702f073bfe1516846e58b56d4e42f" }, 26 | "nvim-dap-ui": { "branch": "master", "commit": "73a26abf4941aa27da59820fd6b028ebcdbcf932" }, 27 | "nvim-dap-virtual-text": { "branch": "master", "commit": "fbdb48c2ed45f4a8293d0d483f7730d24467ccb6" }, 28 | "nvim-lint": { "branch": "master", "commit": "b47cbb249351873e3a571751c3fb66ed6369852f" }, 29 | "nvim-lspconfig": { "branch": "master", "commit": "3ea99227e316c5028f57a4d86a1a7fd01dd876d0" }, 30 | "nvim-nio": { "branch": "master", "commit": "21f5324bfac14e22ba26553caf69ec76ae8a7662" }, 31 | "nvim-pqf": { "branch": "main", "commit": "148ee2ca8b06d83fd9bf6f9b9497724ad39a07d6" }, 32 | "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, 33 | "nvim-treesitter-textobjects": { "branch": "master", "commit": "0f051e9813a36481f48ca1f833897210dbcfffde" }, 34 | "nvim-web-devicons": { "branch": "master", "commit": "1fb58cca9aebbc4fd32b086cb413548ce132c127" }, 35 | "oil.nvim": { "branch": "master", "commit": "685cdb4ffa74473d75a1b97451f8654ceeab0f4a" }, 36 | "plenary.nvim": { "branch": "master", "commit": "857c5ac632080dba10aae49dba902ce3abf91b35" }, 37 | "snacks.nvim": { "branch": "main", "commit": "bc0630e43be5699bb94dadc302c0d21615421d93" }, 38 | "spaceless.nvim": { "branch": "main", "commit": "927fb0afb416ea39306af5842c247d810dfd5938" }, 39 | "statuscol.nvim": { "branch": "main", "commit": "a2580e009a3b4c51b5978768d907dafae2c919ac" }, 40 | "todo-comments.nvim": { "branch": "main", "commit": "304a8d204ee787d2544d8bc23cd38d2f929e7cc5" }, 41 | "treesj": { "branch": "main", "commit": "3b4a2bc42738a63de17e7485d4cc5e49970ddbcc" }, 42 | "undotree": { "branch": "master", "commit": "b951b87b46c34356d44aa71886aecf9dd7f5788a" }, 43 | "vim-eunuch": { "branch": "master", "commit": "e86bb794a1c10a2edac130feb0ea590a00d03f1e" }, 44 | "vim-sleuth": { "branch": "master", "commit": "be69bff86754b1aa5adcbb527d7fcd1635a84080" } 45 | } 46 | -------------------------------------------------------------------------------- /lua/keymaps.lua: -------------------------------------------------------------------------------- 1 | -- Insert new line above/below current line in inset mode 2 | vim.keymap.set('i', '', 'o') 3 | vim.keymap.set('i', '', 'O') 4 | 5 | -- Delete whole words in insert mode with Ctrl + Backspace and Ctrl + Delete 6 | -- If Shift is pressed, delete whole WORDs 7 | vim.keymap.set('i', '', 'db') 8 | vim.keymap.set('i', '', 'dw') 9 | 10 | -- Map H and L to ^ and $ 11 | vim.keymap.set({ 'n', 'x', 'o' }, 'H', '^') 12 | vim.keymap.set({ 'n', 'x', 'o' }, 'L', '$') 13 | 14 | -- Don't move cursor when using J to join lines 15 | vim.keymap.set({ 'n', 'x' }, 'J', 'mzJ`z') 16 | 17 | -- Search only visual area in Visual mode 18 | vim.keymap.set('x', '/', '/\\%V') 19 | 20 | -- Make scroll motions keep cursor in the middle 21 | local scroll_motions = { '', '', '', '', 'n', 'N' } 22 | 23 | for _, motion in ipairs(scroll_motions) do 24 | vim.keymap.set({ 'n', 'x' }, motion, motion .. 'zz', { silent = true }) 25 | end 26 | 27 | -- Apply the . command to all selected lines in visual mode 28 | vim.keymap.set('x', '.', ':normal .', { silent = true }) 29 | 30 | -- Cycle through windows 31 | vim.keymap.set('n', '[w', 'wincmd W') 32 | vim.keymap.set('n', ']w', 'wincmd w') 33 | 34 | -- Tab keybinds 35 | -- Previous/next tab 36 | vim.keymap.set('n', '[t', 'tabprevious') 37 | vim.keymap.set('n', ']t', 'tabnext') 38 | 39 | -- Move current tab 40 | vim.keymap.set('n', '[T', 'tabmove -1') 41 | vim.keymap.set('n', ']T', 'tabmove +1') 42 | 43 | -- New tab 44 | vim.keymap.set('n', 'tn', 'tabnew') 45 | 46 | -- Close tab 47 | vim.keymap.set('n', 'tx', 'tabclose') 48 | vim.keymap.set('n', 'tX', 'tabclose!') 49 | 50 | -- Previous/next quickfix item 51 | vim.keymap.set('n', ']q', 'cnext') 52 | vim.keymap.set('n', '[q', 'cprevious') 53 | 54 | -- Quitall shortcut 55 | vim.keymap.set('n', 'qq', 'quitall') 56 | vim.keymap.set('n', 'QQ', 'quitall!') 57 | 58 | -- Get out of Terminal mode 59 | vim.keymap.set('t', '', '', { silent = true, desc = 'Exit Terminal mode' }) 60 | 61 | -- Yank/paste to clipboard 62 | vim.keymap.set({ 'n', 'x' }, 'y', '"+y') 63 | vim.keymap.set({ 'n', 'x' }, 'p', '"+p') 64 | 65 | local function toggle_quickfix(loclist) 66 | for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do 67 | local wininfo = vim.fn.getwininfo(win)[1] 68 | if (loclist and wininfo.loclist or wininfo.quickfix) == 1 then 69 | -- Quickfix/Loclist window already open, close it 70 | vim.cmd(loclist and 'lclose' or 'cclose') 71 | return 72 | end 73 | end 74 | 75 | -- Quickfix/Loclist window not open, open it 76 | vim.cmd(loclist and 'lopen' or 'copen') 77 | end 78 | 79 | -- Open quickifx/loclist 80 | vim.keymap.set('n', 'q', function() 81 | toggle_quickfix(false) 82 | end) 83 | vim.keymap.set('n', 'l', function() 84 | toggle_quickfix(true) 85 | end) 86 | 87 | -- Move selected lines up or down with fixed indent 88 | vim.keymap.set('x', '', function() 89 | return ":move '>+" .. vim.v.count1 .. 'gv=gv' 90 | end, { expr = true }) 91 | vim.keymap.set('x', '', function() 92 | return ":move '<-" .. -vim.v.count1 .. 'gv=gv' 93 | end, { expr = true }) 94 | -------------------------------------------------------------------------------- /lua/plugins/code_llm.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'zbirenbaum/copilot.lua', 4 | cmd = 'Copilot', 5 | event = 'InsertEnter', 6 | opts = { 7 | suggestion = { 8 | enabled = true, 9 | auto_trigger = true, 10 | hide_during_completion = true, 11 | debounce = 75, 12 | keymap = { 13 | accept = '', 14 | accept_word = '', 15 | accept_line = '', 16 | next = '', 17 | prev = '', 18 | dismiss = '', 19 | }, 20 | }, 21 | server_opts_overrides = {}, 22 | }, 23 | }, 24 | { 25 | 'yetone/avante.nvim', 26 | opts = { 27 | provider = 'copilot', 28 | }, 29 | build = require('utilities.os').is_windows() 30 | and 'powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false' 31 | or 'make', 32 | dependencies = { 33 | 'nvim-treesitter/nvim-treesitter', 34 | 'nvim-lua/plenary.nvim', 35 | 'MunifTanjim/nui.nvim', 36 | 'nvim-tree/nvim-web-devicons', 37 | 'zbirenbaum/copilot.lua', 38 | }, 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /lua/plugins/dap.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local fn = vim.fn 3 | 4 | local function dapconfig() 5 | local dap = require('dap') 6 | local dapui = require('dapui') 7 | 8 | -- DAP signs 9 | fn.sign_define('DapBreakpoint', { text = '󰏃', texthl = '', linehl = '', numhl = '' }) 10 | fn.sign_define('DapBreakpointCondition', { text = '', texthl = '', linehl = '', numhl = '' }) 11 | fn.sign_define('DapLogPoint', { text = '', texthl = '', linehl = '', numhl = '' }) 12 | fn.sign_define('DapStopped', { text = '→', texthl = '', linehl = '', numhl = '' }) 13 | fn.sign_define('DapBreakpointRejected', { text = '', texthl = '', linehl = '', numhl = '' }) 14 | 15 | -- DAP autocompletion 16 | api.nvim_create_autocmd('FileType', { 17 | pattern = 'dap-repl', 18 | callback = function() 19 | require('dap.ext.autocompl').attach() 20 | end, 21 | group = api.nvim_create_augroup('dap-settings', {}), 22 | desc = 'DAP Autocompletion', 23 | }) 24 | 25 | dap.listeners.after.event_initialized['dapui_config'] = function() 26 | dapui.open() 27 | end 28 | dap.listeners.before.event_terminated['dapui_config'] = function() 29 | dapui.close() 30 | end 31 | dap.listeners.before.event_exited['dapui_config'] = function() 32 | dapui.close() 33 | end 34 | 35 | -- Adapters 36 | dap.adapters.lldb = { 37 | type = 'executable', 38 | command = vim.fn.exepath('lldb-dap'), 39 | name = 'lldb', 40 | } 41 | 42 | local c_cpp_rust_base_config = { 43 | { 44 | name = 'Launch', 45 | type = 'lldb', 46 | request = 'launch', 47 | program = function() 48 | return fn.input('Path to executable: ', fn.getcwd() .. '/', 'file') 49 | end, 50 | cwd = '${workspaceFolder}', 51 | stopOnEntry = false, 52 | args = {}, 53 | runInTerminal = false, 54 | }, 55 | { 56 | name = 'Attach to process', 57 | type = 'lldb', 58 | request = 'attach', 59 | pid = require('dap.utils').pick_process, 60 | args = {}, 61 | }, 62 | } 63 | 64 | local terminals = { 65 | { 'konsole', '-e' }, 66 | { 'gnome-terminal', '--' }, 67 | { 'wt', '--' }, 68 | } 69 | 70 | -- Check if an external terminal is available, and use it if it is. 71 | for _, terminal_info in ipairs(terminals) do 72 | if vim.fn.executable(terminal_info[1]) then 73 | -- External Terminal 74 | dap.defaults.fallback.force_external_terminal = true 75 | dap.defaults.fallback.external_terminal = { 76 | -- Use Windows Terminal for Windows, and GNOME Terminal for Linux. 77 | command = terminal_info[1], 78 | args = { terminal_info[2] }, 79 | } 80 | 81 | break 82 | end 83 | end 84 | 85 | -- Language configurations 86 | dap.configurations.cpp = c_cpp_rust_base_config 87 | dap.configurations.c = c_cpp_rust_base_config 88 | dap.configurations.rust = c_cpp_rust_base_config 89 | 90 | -- Program specific configs 91 | table.insert( 92 | dap.configurations.c, 93 | setmetatable({ 94 | name = 'Neovim', 95 | type = 'lldb', 96 | request = 'launch', 97 | program = vim.uv.os_homedir() .. '/Dev/neovim/neovim/build/bin/nvim', 98 | cwd = '${workspaceFolder}', 99 | stopOnEntry = false, 100 | args = function() 101 | return vim.split(fn.input('Args: ', '--clean '), ' ') 102 | end, 103 | runInTerminal = true, 104 | }, { 105 | __call = function(config) 106 | -- Listeners are indexed by a key. 107 | -- This is like a namespace and must not conflict with what plugins 108 | -- like nvim-dap-ui or nvim-dap itself uses. 109 | -- It's best to not use anything starting with `dap` 110 | local key = 'neovim-debug-auto-attach' 111 | 112 | -- dap.listeners...` 113 | -- We listen to the `initialize` response. It indicates a new session got initialized 114 | dap.listeners.after.initialize[key] = function(session) 115 | -- Immediately clear the listener, we don't want to run this logic for additional sessions 116 | dap.listeners.after.initialize[key] = nil 117 | 118 | -- The first argument to a event or response is always the session 119 | -- A session contains a `on_close` table that allows us to register functions 120 | -- that get called when the session closes. 121 | -- We use this to ensure the listeners get cleaned up 122 | session.on_close[key] = function() 123 | for _, handler in pairs(dap.listeners.after) do 124 | handler[key] = nil 125 | end 126 | end 127 | end 128 | 129 | -- We listen to `event_process` to get the pid: 130 | dap.listeners.after.event_process[key] = function(_, body) 131 | -- Immediately clear the listener, we don't want to run this logic for additional sessions 132 | dap.listeners.after.event_process[key] = nil 133 | 134 | local ppid = body.systemProcessId 135 | -- The pid is the parent pid, we need to attach to the child. This uses the `ps` tool to get it 136 | -- It takes a bit for the child to arrive. This uses the `vim.wait` function to wait up to a second 137 | -- to get the child pid. 138 | vim.wait(1000, function() 139 | return tonumber(vim.fn.system('ps -o pid= --ppid ' .. tostring(ppid))) ~= nil 140 | end) 141 | local pid = tonumber(vim.fn.system('ps -o pid= --ppid ' .. tostring(ppid))) 142 | local home = vim.uv.os_homedir() 143 | 144 | -- If we found it, spawn another debug session that attaches to the pid. 145 | if pid then 146 | dap.run({ 147 | name = 'Neovim embedded', 148 | type = 'lldb', 149 | request = 'attach', 150 | pid = pid, 151 | -- ⬇️ Change paths as needed 152 | program = home .. '/Dev/neovim/neovim/build/bin/nvim', 153 | env = { 'VIMRUNTIME=' .. home .. '/Dev/neovim/neovim/runtime' }, 154 | cwd = home .. '/Dev/neovim/neovim/', 155 | externalConsole = false, 156 | }) 157 | end 158 | end 159 | return config 160 | end, 161 | }) 162 | ) 163 | end 164 | 165 | return { 166 | { 167 | 'mfussenegger/nvim-dap', 168 | dependencies = { 169 | { 'theHamsta/nvim-dap-virtual-text', opts = {} }, 170 | { 171 | 'rcarriga/nvim-dap-ui', 172 | dependencies = { 'nvim-neotest/nvim-nio' }, 173 | opts = {}, 174 | }, 175 | }, 176 | keys = { 177 | { 178 | '', 179 | function() 180 | require('dap').continue() 181 | end, 182 | desc = 'DAP: Continue', 183 | }, 184 | { 185 | '', 186 | function() 187 | require('dap').step_back() 188 | end, 189 | desc = 'DAP: Step back', 190 | }, 191 | { 192 | '', 193 | function() 194 | require('dap').step_over() 195 | end, 196 | desc = 'DAP: Step Over', 197 | }, 198 | { 199 | '', 200 | function() 201 | require('dap').step_into() 202 | end, 203 | desc = 'DAP: Step Into', 204 | }, 205 | { 206 | '', 207 | function() 208 | require('dap').step_out() 209 | end, 210 | desc = 'DAP: Step Out', 211 | }, 212 | { 213 | '', 214 | function() 215 | require('dap').terminate() 216 | end, 217 | desc = 'DAP: Terminate', 218 | }, 219 | { 220 | 'b', 221 | function() 222 | require('dap').toggle_breakpoint() 223 | end, 224 | desc = 'DAP: Toggle Breakpoint', 225 | }, 226 | { 227 | 'B', 228 | function() 229 | require('dap').set_breakpoint(fn.input('Breakpoint condition: ')) 230 | end, 231 | desc = 'DAP: Conditional breakpoint', 232 | }, 233 | { 234 | 'dp', 235 | function() 236 | require('dap').set_breakpoint(nil, nil, fn.input('Log point message: ')) 237 | end, 238 | desc = 'DAP: Log point', 239 | }, 240 | { 241 | 'dc', 242 | function() 243 | require('dap').clear_breakpoints() 244 | end, 245 | desc = 'DAP: Clear breakpoints', 246 | }, 247 | { 248 | 'dr', 249 | function() 250 | require('dap').repl.toggle() 251 | end, 252 | desc = 'DAP: Toggle REPL', 253 | }, 254 | { 255 | 'dl', 256 | function() 257 | require('dap').run_last() 258 | end, 259 | desc = 'DAP: Run last', 260 | }, 261 | }, 262 | config = dapconfig, 263 | }, 264 | } 265 | -------------------------------------------------------------------------------- /lua/plugins/editing.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'echasnovski/mini.align', 4 | keys = { 5 | { 'ga', mode = { 'n', 'x' } }, 6 | { 'gA', mode = { 'n', 'x' } }, 7 | }, 8 | opts = {}, 9 | }, 10 | { 'echasnovski/mini.ai', opts = {} }, 11 | { 12 | 'echasnovski/mini.surround', 13 | keys = { 14 | { 'sa', mode = { 'n', 'x' } }, 15 | 'sd', 16 | 'sr', 17 | 'sf', 18 | 'sF', 19 | 'sh', 20 | 'sn', 21 | }, 22 | opts = {}, 23 | }, 24 | { 25 | 'Wansmer/treesj', 26 | dependencies = { 'nvim-treesitter/nvim-treesitter' }, 27 | cmd = { 'TSJSplit', 'TSJJoin' }, 28 | keys = { 29 | { 30 | 'gS', 31 | function() 32 | require('treesj').split() 33 | end, 34 | }, 35 | { 36 | 'gJ', 37 | function() 38 | require('treesj').join() 39 | end, 40 | }, 41 | }, 42 | opts = { 43 | use_default_keymaps = false, 44 | }, 45 | }, 46 | 'lewis6991/spaceless.nvim', 47 | } 48 | -------------------------------------------------------------------------------- /lua/plugins/formatting.lua: -------------------------------------------------------------------------------- 1 | return { 2 | 'tpope/vim-sleuth', 3 | { 4 | 'stevearc/conform.nvim', 5 | dependencies = { 'neovim/nvim-lspconfig' }, 6 | event = 'BufWritePre', 7 | cmd = { 'ConformInfo' }, 8 | opts = { 9 | formatters_by_ft = { 10 | lua = { 'stylua' }, 11 | python = { 'ruff' }, 12 | css = { 'prettier' }, 13 | html = { 'prettier' }, 14 | javascript = { 'prettier' }, 15 | typescript = { 'prettier' }, 16 | json = { 'prettier' }, 17 | jsonc = { 'prettier' }, 18 | markdown = { 'prettier' }, 19 | }, 20 | }, 21 | keys = { 22 | { 23 | 'F', 24 | function() 25 | require('conform').format({ async = true, lsp_format = 'first' }) 26 | end, 27 | mode = { 'n', 'x' }, 28 | }, 29 | }, 30 | init = function() 31 | vim.o.formatexpr = "v:lua.require'conform'.formatexpr()" 32 | end, 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /lua/plugins/git.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'NeogitOrg/neogit', 4 | dependencies = { 5 | 'nvim-lua/plenary.nvim', 6 | 'sindrets/diffview.nvim', 7 | }, 8 | opts = { 9 | remember_settings = false, 10 | }, 11 | keys = { 12 | { 13 | 'gg', 14 | function() 15 | require('neogit').open() 16 | end, 17 | desc = 'Neogit', 18 | }, 19 | }, 20 | }, 21 | { 22 | 'sindrets/diffview.nvim', 23 | opts = { 24 | enhanced_diff_hl = true, 25 | view = { 26 | diff_view = { 27 | layout = 'diff2_horizontal', 28 | }, 29 | file_history_view = { 30 | layout = 'diff2_horizontal', 31 | }, 32 | merge_tool = { 33 | layout = 'diff3_mixed', 34 | }, 35 | }, 36 | }, 37 | cmd = { 'DiffviewOpen', 'DiffviewFileHistory' }, 38 | keys = { 39 | { 'gd', 'DiffviewOpen', desc = 'Diff worktree' }, 40 | { 'g.', 'DiffviewFileHistory', desc = 'Diffview file history' }, 41 | { 42 | 'gD', 43 | function() 44 | local target = vim.fn.input('Target branch name: ') 45 | local status = vim.system({ 'git', 'merge-base', 'HEAD', target }, { text = true }):wait() 46 | 47 | if status.code ~= 0 then 48 | error( 49 | string.format( 50 | 'Error code %d while running git merge-base. STDERR: %s', 51 | status.code, 52 | status.stderr 53 | ) 54 | ) 55 | end 56 | 57 | vim.cmd.DiffviewOpen(status.stdout) 58 | end, 59 | desc = 'Diff from common ancestor of target branch and current branch', 60 | }, 61 | { 62 | 'g', 63 | function() 64 | vim.cmd.DiffviewOpen(vim.fn.input('Diff rev: ')) 65 | end, 66 | desc = 'Diff rev', 67 | }, 68 | { 'gt', 'DiffviewToggleFiles', desc = 'Toggle diffview files panel' }, 69 | { 70 | ']C', 71 | function() 72 | require('diffview.config').actions.next_conflict() 73 | end, 74 | desc = 'Jump to next conflict marker', 75 | }, 76 | { 77 | '[C', 78 | function() 79 | require('diffview.config').actions.prev_conflict() 80 | end, 81 | desc = 'Jump to previous conflict marker', 82 | }, 83 | }, 84 | }, 85 | { 86 | 'lewis6991/gitsigns.nvim', 87 | opts = { 88 | signs = { 89 | add = { text = '│' }, 90 | change = { text = '│' }, 91 | delete = { text = '_' }, 92 | topdelete = { text = '‾' }, 93 | changedelete = { text = '~' }, 94 | untracked = { text = '┆' }, 95 | }, 96 | signcolumn = true, -- Toggle with `:Gitsigns toggle_signs` 97 | numhl = false, -- Toggle with `:Gitsigns toggle_numhl` 98 | linehl = false, -- Toggle with `:Gitsigns toggle_linehl` 99 | word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff` 100 | watch_gitdir = { 101 | interval = 1000, 102 | follow_files = true, 103 | }, 104 | attach_to_untracked = true, 105 | current_line_blame = true, -- Toggle with `:Gitsigns toggle_current_line_blame` 106 | current_line_blame_opts = { 107 | virt_text = true, 108 | virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align' 109 | delay = 100, 110 | ignore_whitespace = false, 111 | virt_text_priority = 100, 112 | use_focus = true, 113 | }, 114 | current_line_blame_formatter = '  , - ', 115 | current_line_blame_formatter_nc = '', 116 | sign_priority = 6, 117 | update_debounce = 100, 118 | status_formatter = nil, -- Use default 119 | max_file_length = 40000, -- Disable if file is longer than this (in lines) 120 | preview_config = { 121 | -- Options passed to nvim_open_win 122 | border = 'single', 123 | style = 'minimal', 124 | relative = 'cursor', 125 | row = 0, 126 | col = 1, 127 | }, 128 | on_attach = function(bufnr) 129 | local gs = package.loaded.gitsigns 130 | 131 | local function map(mode, l, r, opts) 132 | opts = opts or {} 133 | opts.buffer = bufnr 134 | vim.keymap.set(mode, l, r, opts) 135 | end 136 | 137 | -- Navigation 138 | map('n', ']h', function() 139 | if vim.wo.diff then 140 | return ']h' 141 | end 142 | vim.schedule(function() 143 | gs.next_hunk() 144 | end) 145 | return '' 146 | end, { expr = true }) 147 | 148 | map('n', '[h', function() 149 | if vim.wo.diff then 150 | return '[h' 151 | end 152 | vim.schedule(function() 153 | gs.prev_hunk() 154 | end) 155 | return '' 156 | end, { expr = true }) 157 | 158 | -- Actions 159 | map('n', 'H', gs.toggle_deleted) 160 | map('n', 'hw', gs.toggle_word_diff) 161 | map('n', 'hb', gs.blame_line) 162 | map('n', 'hB', gs.toggle_current_line_blame) 163 | map('n', 'hs', gs.stage_hunk) 164 | map('n', 'hr', gs.reset_hunk) 165 | map('x', 'hs', function() 166 | gs.stage_hunk({ vim.fn.line('.'), vim.fn.line('x') }) 167 | end) 168 | map('x', 'hr', function() 169 | gs.reset_hunk({ vim.fn.line('.'), vim.fn.line('x') }) 170 | end) 171 | map('n', 'hu', gs.undo_stage_hunk) 172 | map('n', 'hS', gs.stage_buffer) 173 | map('n', 'hR', gs.reset_buffer) 174 | map('n', 'hp', gs.preview_hunk) 175 | map('n', 'hd', function() 176 | gs.diffthis('', { split = 'botright' }) 177 | end) 178 | map('n', 'hD', function() 179 | gs.diffthis('~', { split = 'botright' }) 180 | end) 181 | 182 | -- Text object 183 | map({ 'o', 'x' }, 'ih', ':Gitsigns select_hunk') 184 | end, 185 | }, 186 | }, 187 | } 188 | -------------------------------------------------------------------------------- /lua/plugins/language.lua: -------------------------------------------------------------------------------- 1 | -- Language specific plugins 2 | return { import = 'plugins.language' } 3 | -------------------------------------------------------------------------------- /lua/plugins/language/markdown.lua: -------------------------------------------------------------------------------- 1 | return { 2 | 'OXY2DEV/markview.nvim', 3 | ft = 'markdown', 4 | dependencies = { 5 | 'nvim-treesitter/nvim-treesitter', 6 | 'nvim-tree/nvim-web-devicons', 7 | }, 8 | opts = {}, 9 | } 10 | -------------------------------------------------------------------------------- /lua/plugins/lsp/clients.lua: -------------------------------------------------------------------------------- 1 | return { 2 | lua_ls = { 3 | cmd = { 'lua-language-server' }, 4 | settings = { 5 | Lua = {}, 6 | }, 7 | on_init = function(client) 8 | if not client.workspace_folders then 9 | return 10 | end 11 | 12 | local path = vim.fs.normalize(client.workspace_folders[1].name) 13 | 14 | if path == nil or vim.uv.fs_stat(path .. '/.luarc.json') or vim.uv.fs_stat(path .. '/.luarc.jsonc') then 15 | return 16 | end 17 | 18 | client.config.settings.Lua = vim.tbl_deep_extend('force', client.config.settings.Lua, { 19 | runtime = { 20 | -- Use LuaJIT for Neovim. 21 | version = 'LuaJIT', 22 | }, 23 | -- Make the server aware of Neovim runtime files. 24 | workspace = { 25 | checkThirdParty = false, 26 | library = { 27 | '${3rd}/luv/library', 28 | '${3rd}/busted/library', 29 | unpack(vim.api.nvim_list_runtime_paths()), 30 | }, 31 | }, 32 | }) 33 | end, 34 | }, 35 | clangd = { 36 | cmd = { 'clangd', '--background-index', '--clang-tidy' }, 37 | }, 38 | rust_analyzer = { 39 | settings = { 40 | ['rust-analyzer'] = { 41 | check = { 42 | command = 'clippy', 43 | }, 44 | diagnostics = { 45 | experimental = { 46 | enable = true, 47 | }, 48 | }, 49 | rustfmt = { 50 | extraArgs = { '+nightly' }, 51 | rangeFormatting = { 52 | enable = true, 53 | }, 54 | }, 55 | }, 56 | }, 57 | }, 58 | texlab = { 59 | settings = { 60 | texlab = { 61 | bibtexFormatter = 'texlab', 62 | build = { 63 | args = { 64 | '-pdf', 65 | '-pdflatex=lualatex', 66 | '-aux-directory=aux', 67 | '-interaction=nonstopmode', 68 | '-synctex=1', 69 | '%f', 70 | }, 71 | executable = 'latexmk', 72 | forwardSearchAfter = true, 73 | onSave = true, 74 | }, 75 | chktex = { 76 | onEdit = false, 77 | onOpenAndSave = false, 78 | }, 79 | diagnosticsDelay = 300, 80 | formatterLineLength = 80, 81 | forwardSearch = { 82 | executable = 'zathura', 83 | args = { '--synctex-forward', '%l:1:%f', '%p' }, 84 | onSave = true, 85 | }, 86 | forwardSearchAfter = true, 87 | latexFormatter = 'latexindent', 88 | latexindent = { 89 | modifyLineBreaks = false, 90 | }, 91 | }, 92 | }, 93 | }, 94 | sourcekit = { 95 | filetypes = { 'swift' }, 96 | settings = { 97 | sourcekit = { 98 | backgroundIndexing = true, 99 | backgroundPreparationMode = 'enabled', 100 | }, 101 | }, 102 | }, 103 | cmake = {}, 104 | basedpyright = {}, 105 | ruff = {}, 106 | gopls = {}, 107 | asm_lsp = {}, 108 | ts_ls = {}, 109 | cssls = {}, 110 | html = {}, 111 | svelte = {}, 112 | } 113 | -------------------------------------------------------------------------------- /lua/plugins/lsp/init.lua: -------------------------------------------------------------------------------- 1 | local lsp_client_configs = require('plugins.lsp.clients') 2 | 3 | return { 4 | { 5 | 'saghen/blink.cmp', 6 | dependencies = { 'rafamadriz/friendly-snippets' }, 7 | build = 'cargo build --release', 8 | opts = { 9 | keymap = { preset = 'default' }, 10 | appearance = { 11 | nerd_font_variant = 'normal', 12 | }, 13 | signature = { enabled = true }, 14 | }, 15 | }, 16 | { 17 | 'neovim/nvim-lspconfig', 18 | config = function() 19 | local lspconfig = require('lspconfig') 20 | 21 | -- Diagnostics configuration 22 | vim.diagnostic.config({ 23 | virtual_text = { 24 | spacing = 4, 25 | prefix = '~', 26 | }, 27 | severity_sort = true, 28 | signs = { 29 | text = { 30 | [vim.diagnostic.severity.ERROR] = '', 31 | [vim.diagnostic.severity.WARN] = '', 32 | [vim.diagnostic.severity.INFO] = '', 33 | [vim.diagnostic.severity.HINT] = '', 34 | }, 35 | }, 36 | }) 37 | 38 | -- LSP configuration 39 | vim.api.nvim_create_autocmd('LspAttach', { 40 | desc = 'LSP configuration', 41 | group = vim.api.nvim_create_augroup('lsp-settings', {}), 42 | callback = function(args) 43 | local client = vim.lsp.get_client_by_id(args.data.client_id) 44 | assert(client ~= nil) 45 | 46 | -- If client supports folding, use the client for folding. 47 | if client.server_capabilities.foldingRangeProvider then 48 | vim.wo.foldmethod = 'expr' 49 | vim.wo.foldexpr = 'v:lua.vim.lsp.foldexpr()' 50 | end 51 | 52 | -- Enable document color for supported clients. 53 | if client:supports_method('textDocument/documentColor') then 54 | vim.lsp.document_color.enable(true, args.buf) 55 | end 56 | end, 57 | }) 58 | 59 | -- Load LSP client configurations. 60 | for client, config in pairs(lsp_client_configs) do 61 | lspconfig[client].setup(config) 62 | end 63 | end, 64 | }, 65 | { 66 | 'mfussenegger/nvim-lint', 67 | init = function() 68 | require('lint').linters_by_ft = { 69 | python = { 'mypy' }, 70 | } 71 | 72 | vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost', 'InsertLeave' }, { 73 | desc = 'Lint configuration', 74 | group = vim.api.nvim_create_augroup('NvimLint', {}), 75 | callback = function() 76 | require('lint').try_lint() 77 | end, 78 | }) 79 | end, 80 | }, 81 | } 82 | -------------------------------------------------------------------------------- /lua/plugins/misc.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'mason-org/mason.nvim', 4 | opts = { 5 | PATH = 'append', 6 | max_concurrent_installers = require('utilities.os').pu_count(), 7 | }, 8 | }, 9 | { 10 | 'stevearc/oil.nvim', 11 | lazy = false, 12 | opts = { 13 | -- Make split mappings consistent with Telescope. 14 | keymaps = { 15 | [''] = false, 16 | [''] = false, 17 | [''] = 'actions.select_vsplit', 18 | [''] = 'actions.select_split', 19 | }, 20 | }, 21 | keys = { { '-', 'Oil' } }, 22 | dependencies = { 'nvim-tree/nvim-web-devicons' }, 23 | }, 24 | 'tpope/vim-eunuch', 25 | 'OXY2DEV/helpview.nvim', 26 | } 27 | -------------------------------------------------------------------------------- /lua/plugins/navigation.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'folke/flash.nvim', 4 | opts = { 5 | jump = { 6 | autojump = true, 7 | }, 8 | label = { 9 | uppercase = false, 10 | }, 11 | modes = { 12 | search = { 13 | enabled = false, 14 | }, 15 | char = { 16 | jump_labels = function(_) 17 | return vim.v.count == 0 18 | end, 19 | }, 20 | }, 21 | }, 22 | keys = { 23 | { 24 | '', 25 | mode = { 'n', 'x', 'o' }, 26 | function() 27 | -- default options: exact mode, multi window, all directions, with a backdrop 28 | require('flash').jump() 29 | end, 30 | desc = 'Flash', 31 | }, 32 | { 33 | '', 34 | mode = { 'n', 'x', 'o' }, 35 | function() 36 | require('flash').treesitter() 37 | end, 38 | desc = 'Flash Treesitter', 39 | }, 40 | { 41 | 'r', 42 | mode = 'o', 43 | function() 44 | require('flash').remote() 45 | end, 46 | desc = 'Remote Flash', 47 | }, 48 | { 49 | '', 50 | mode = 'c', 51 | function() 52 | require('flash').toggle() 53 | end, 54 | desc = 'Toggle Flash Search', 55 | }, 56 | { 'f', mode = { 'n', 'x', 'o' } }, 57 | { 'F', mode = { 'n', 'x', 'o' } }, 58 | { 't', mode = { 'n', 'x', 'o' } }, 59 | { 'T', mode = { 'n', 'x', 'o' } }, 60 | }, 61 | init = function() 62 | -- Unmap in quickfix and command-line windows 63 | local flash_unmap_augroup = vim.api.nvim_create_augroup('FlashUnmapCR', {}) 64 | local flash_unmap_fn = function() 65 | vim.keymap.set('n', '', '', { buffer = 0 }) 66 | end 67 | 68 | vim.api.nvim_create_autocmd('FileType', { 69 | pattern = 'qf', 70 | callback = flash_unmap_fn, 71 | group = flash_unmap_augroup, 72 | desc = 'Unmap for quickfix windows', 73 | }) 74 | 75 | vim.api.nvim_create_autocmd('CmdwinEnter', { 76 | callback = flash_unmap_fn, 77 | group = flash_unmap_augroup, 78 | desc = 'Unmap for command-line windows', 79 | }) 80 | end, 81 | config = function(_, opts) 82 | require('flash').setup(opts) 83 | 84 | -- Always toggle flash search jump labels off after entering cmdline 85 | -- So that the keybind only applies for the current search 86 | vim.api.nvim_create_autocmd('CmdlineEnter', { 87 | callback = function(_) 88 | if vim.v.event.cmdtype:match('[/?]') then 89 | require('flash').toggle(false) 90 | end 91 | end, 92 | group = vim.api.nvim_create_augroup('FlashCmdlineToggle', {}), 93 | desc = 'Toggle Flash search jump labels off when entering cmdline', 94 | }) 95 | end, 96 | }, 97 | { 98 | 'ThePrimeagen/harpoon', 99 | branch = 'harpoon2', 100 | dependencies = { 'nvim-lua/plenary.nvim' }, 101 | opts = {}, 102 | config = function(_, opts) 103 | local harpoon = require('harpoon') 104 | local extensions = require('harpoon.extensions') 105 | harpoon:setup(opts) 106 | 107 | harpoon:extend(extensions.builtins.highlight_current_file()) 108 | harpoon:extend(extensions.builtins.navigate_with_number()) 109 | 110 | harpoon:extend({ 111 | UI_CREATE = function(cx) 112 | vim.keymap.set('n', '', function() 113 | harpoon.ui:select_menu_item({ vsplit = true }) 114 | end, { buffer = cx.bufnr }) 115 | 116 | vim.keymap.set('n', '', function() 117 | harpoon.ui:select_menu_item({ split = true }) 118 | end, { buffer = cx.bufnr }) 119 | 120 | vim.keymap.set('n', '', function() 121 | harpoon.ui:select_menu_item({ tabedit = true }) 122 | end, { buffer = cx.bufnr }) 123 | end, 124 | }) 125 | end, 126 | keys = { 127 | { 128 | 'za', 129 | function() 130 | require('harpoon'):list():add() 131 | end, 132 | }, 133 | { 134 | 'zc', 135 | function() 136 | require('harpoon'):list():clear() 137 | end, 138 | }, 139 | { 140 | 'zz', 141 | function() 142 | require('harpoon').ui:toggle_quick_menu(require('harpoon'):list()) 143 | end, 144 | }, 145 | { 146 | ']', 147 | function() 148 | require('harpoon'):list():next() 149 | end, 150 | }, 151 | { 152 | '[', 153 | function() 154 | require('harpoon'):list():prev() 155 | end, 156 | }, 157 | { 158 | '1', 159 | function() 160 | require('harpoon'):list():select(1) 161 | end, 162 | }, 163 | { 164 | '2', 165 | function() 166 | require('harpoon'):list():select(2) 167 | end, 168 | }, 169 | { 170 | '3', 171 | function() 172 | require('harpoon'):list():select(3) 173 | end, 174 | }, 175 | { 176 | '4', 177 | function() 178 | require('harpoon'):list():select(4) 179 | end, 180 | }, 181 | }, 182 | }, 183 | } 184 | -------------------------------------------------------------------------------- /lua/plugins/snacks.lua: -------------------------------------------------------------------------------- 1 | return { 2 | 'folke/snacks.nvim', 3 | priority = 1000, 4 | lazy = false, 5 | ---@type snacks.Config 6 | opts = { 7 | bigfile = { enabled = true }, 8 | input = { enabled = true }, 9 | notifier = { 10 | enabled = true, 11 | timeout = 3000, 12 | }, 13 | picker = { enabled = true }, 14 | quickfile = { enabled = true }, 15 | scope = { enabled = true }, 16 | words = { enabled = true }, 17 | styles = { 18 | notification = { 19 | wo = { wrap = true }, -- Wrap notifications 20 | }, 21 | }, 22 | }, 23 | keys = { 24 | -- Top Pickers & Explorer 25 | { 26 | '', 27 | function() 28 | Snacks.picker.smart() 29 | end, 30 | desc = 'Smart Find Files', 31 | }, 32 | { 33 | ',', 34 | function() 35 | Snacks.picker.buffers() 36 | end, 37 | desc = 'Buffers', 38 | }, 39 | { 40 | '/', 41 | function() 42 | Snacks.picker.grep() 43 | end, 44 | desc = 'Grep', 45 | }, 46 | { 47 | ':', 48 | function() 49 | Snacks.picker.command_history() 50 | end, 51 | desc = 'Command History', 52 | }, 53 | { 54 | 'n', 55 | function() 56 | Snacks.picker.notifications() 57 | end, 58 | desc = 'Notification History', 59 | }, 60 | { 61 | 'fc', 62 | function() 63 | Snacks.picker.files({ cwd = vim.fn.stdpath('config') }) 64 | end, 65 | desc = 'Find Config File', 66 | }, 67 | { 68 | 'ff', 69 | function() 70 | Snacks.picker.files() 71 | end, 72 | desc = 'Find Files', 73 | }, 74 | { 75 | 'fg', 76 | function() 77 | Snacks.picker.git_files() 78 | end, 79 | desc = 'Find Git Files', 80 | }, 81 | { 82 | 'fr', 83 | function() 84 | Snacks.picker.recent() 85 | end, 86 | desc = 'Recent', 87 | }, 88 | -- git 89 | { 90 | 'gb', 91 | function() 92 | Snacks.picker.git_branches() 93 | end, 94 | desc = 'Git Branches', 95 | }, 96 | { 97 | 'gl', 98 | function() 99 | Snacks.picker.git_log() 100 | end, 101 | desc = 'Git Log', 102 | }, 103 | { 104 | 'gL', 105 | function() 106 | Snacks.picker.git_log_line() 107 | end, 108 | desc = 'Git Log Line', 109 | }, 110 | { 111 | 'gf', 112 | function() 113 | Snacks.picker.git_log_file() 114 | end, 115 | desc = 'Git Log File', 116 | }, 117 | -- Grep 118 | { 119 | 'sb', 120 | function() 121 | Snacks.picker.lines() 122 | end, 123 | desc = 'Buffer Lines', 124 | }, 125 | { 126 | 'sB', 127 | function() 128 | Snacks.picker.grep_buffers() 129 | end, 130 | desc = 'Grep Open Buffers', 131 | }, 132 | { 133 | 'sg', 134 | function() 135 | Snacks.picker.grep() 136 | end, 137 | desc = 'Grep', 138 | }, 139 | { 140 | 'sw', 141 | function() 142 | Snacks.picker.grep_word() 143 | end, 144 | desc = 'Visual selection or word', 145 | mode = { 'n', 'x' }, 146 | }, 147 | -- search 148 | { 149 | 's"', 150 | function() 151 | Snacks.picker.registers() 152 | end, 153 | desc = 'Registers', 154 | }, 155 | { 156 | 's/', 157 | function() 158 | Snacks.picker.search_history() 159 | end, 160 | desc = 'Search History', 161 | }, 162 | { 163 | 'sa', 164 | function() 165 | Snacks.picker.autocmds() 166 | end, 167 | desc = 'Autocmds', 168 | }, 169 | { 170 | 'sc', 171 | function() 172 | Snacks.picker.commands() 173 | end, 174 | desc = 'Commands', 175 | }, 176 | { 177 | 'sd', 178 | function() 179 | Snacks.picker.diagnostics() 180 | end, 181 | desc = 'Diagnostics', 182 | }, 183 | { 184 | 'sD', 185 | function() 186 | Snacks.picker.diagnostics_buffer() 187 | end, 188 | desc = 'Buffer Diagnostics', 189 | }, 190 | { 191 | 'sh', 192 | function() 193 | Snacks.picker.help() 194 | end, 195 | desc = 'Help Pages', 196 | }, 197 | { 198 | 'sH', 199 | function() 200 | Snacks.picker.highlights() 201 | end, 202 | desc = 'Highlights', 203 | }, 204 | { 205 | 'si', 206 | function() 207 | Snacks.picker.icons() 208 | end, 209 | desc = 'Icons', 210 | }, 211 | { 212 | 'sj', 213 | function() 214 | Snacks.picker.jumps() 215 | end, 216 | desc = 'Jumps', 217 | }, 218 | { 219 | 'sk', 220 | function() 221 | Snacks.picker.keymaps() 222 | end, 223 | desc = 'Keymaps', 224 | }, 225 | { 226 | 'sl', 227 | function() 228 | Snacks.picker.loclist() 229 | end, 230 | desc = 'Location List', 231 | }, 232 | { 233 | 'sm', 234 | function() 235 | Snacks.picker.marks() 236 | end, 237 | desc = 'Marks', 238 | }, 239 | { 240 | 'sM', 241 | function() 242 | Snacks.picker.man() 243 | end, 244 | desc = 'Man Pages', 245 | }, 246 | { 247 | 'sp', 248 | function() 249 | Snacks.picker.lazy() 250 | end, 251 | desc = 'Search for Plugin Spec', 252 | }, 253 | { 254 | 'sq', 255 | function() 256 | Snacks.picker.qflist() 257 | end, 258 | desc = 'Quickfix List', 259 | }, 260 | { 261 | 'sR', 262 | function() 263 | Snacks.picker.resume() 264 | end, 265 | desc = 'Resume', 266 | }, 267 | { 268 | 'su', 269 | function() 270 | Snacks.picker.undo() 271 | end, 272 | desc = 'Undo History', 273 | }, 274 | { 275 | 'uC', 276 | function() 277 | Snacks.picker.colorschemes() 278 | end, 279 | desc = 'Colorschemes', 280 | }, 281 | -- LSP 282 | { 283 | 'ss', 284 | function() 285 | Snacks.picker.lsp_symbols() 286 | end, 287 | desc = 'LSP Symbols', 288 | }, 289 | { 290 | 'sS', 291 | function() 292 | Snacks.picker.lsp_workspace_symbols() 293 | end, 294 | desc = 'LSP Workspace Symbols', 295 | }, 296 | -- Other 297 | { 298 | 'n', 299 | function() 300 | Snacks.notifier.show_history() 301 | end, 302 | desc = 'Notification History', 303 | }, 304 | { 305 | 'bd', 306 | function() 307 | Snacks.bufdelete() 308 | end, 309 | desc = 'Delete Buffer', 310 | }, 311 | { 312 | 'cR', 313 | function() 314 | Snacks.rename.rename_file() 315 | end, 316 | desc = 'Rename File', 317 | }, 318 | { 319 | 'un', 320 | function() 321 | Snacks.notifier.hide() 322 | end, 323 | desc = 'Dismiss All Notifications', 324 | }, 325 | { 326 | 'N', 327 | desc = 'Neovim News', 328 | function() 329 | Snacks.win({ 330 | file = vim.api.nvim_get_runtime_file('doc/news.txt', false)[1], 331 | width = 0.6, 332 | height = 0.6, 333 | wo = { 334 | spell = false, 335 | wrap = false, 336 | signcolumn = 'yes', 337 | statuscolumn = ' ', 338 | conceallevel = 3, 339 | }, 340 | }) 341 | end, 342 | }, 343 | { 344 | '', 345 | function() 346 | Snacks.terminal() 347 | end, 348 | desc = 'Toggle Terminal', 349 | }, 350 | { 351 | ']r', 352 | function() 353 | Snacks.words.jump(vim.v.count1) 354 | end, 355 | desc = 'Next Reference', 356 | mode = { 'n', 't' }, 357 | }, 358 | { 359 | '[r', 360 | function() 361 | Snacks.words.jump(-vim.v.count1) 362 | end, 363 | desc = 'Prev Reference', 364 | mode = { 'n', 't' }, 365 | }, 366 | }, 367 | init = function() 368 | vim.api.nvim_create_autocmd('User', { 369 | pattern = 'VeryLazy', 370 | callback = function() 371 | -- Setup some globals for debugging (lazy-loaded) 372 | _G.dd = function(...) 373 | Snacks.debug.inspect(...) 374 | end 375 | _G.bt = function() 376 | Snacks.debug.backtrace() 377 | end 378 | vim.print = _G.dd -- Override print to use snacks for `:=` command 379 | 380 | -- Create some toggle mappings 381 | Snacks.toggle.option('spell', { name = 'Spelling' }):map('us') 382 | Snacks.toggle.option('wrap', { name = 'Wrap' }):map('uw') 383 | Snacks.toggle.option('relativenumber', { name = 'Relative Number' }):map('uL') 384 | Snacks.toggle.diagnostics():map('ud') 385 | Snacks.toggle.line_number():map('ul') 386 | Snacks.toggle 387 | .option('conceallevel', { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 }) 388 | :map('uc') 389 | Snacks.toggle.treesitter():map('uT') 390 | Snacks.toggle 391 | .option('background', { off = 'light', on = 'dark', name = 'Dark Background' }) 392 | :map('ub') 393 | Snacks.toggle.inlay_hints():map('uh') 394 | Snacks.toggle.indent():map('ug') 395 | Snacks.toggle.dim():map('uD') 396 | 397 | -- Rename support for Oil.nvim 398 | vim.api.nvim_create_autocmd('User', { 399 | pattern = 'OilActionsPost', 400 | callback = function(event) 401 | if event.data.actions.type == 'move' then 402 | Snacks.rename.on_rename_file(event.data.actions.src_url, event.data.actions.dest_url) 403 | end 404 | end, 405 | }) 406 | end, 407 | }) 408 | end, 409 | } 410 | -------------------------------------------------------------------------------- /lua/plugins/status_items.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'nvim-lualine/lualine.nvim', 4 | dependencies = { 5 | 'nvim-tree/nvim-web-devicons', 6 | }, 7 | opts = { 8 | options = { 9 | theme = 'catppuccin', 10 | component_separators = { left = '╱', right = '╲' }, 11 | section_separators = { left = '', right = '' }, 12 | }, 13 | sections = { 14 | lualine_a = { 'mode' }, 15 | lualine_b = { 'branch', 'diff', 'diagnostics' }, 16 | lualine_c = { 'filename', 'searchcount' }, 17 | lualine_x = { 18 | 'encoding', 19 | 'fileformat', 20 | 'filetype', 21 | }, 22 | lualine_y = { 'progress' }, 23 | lualine_z = { 'location' }, 24 | }, 25 | inactive_sections = { 26 | lualine_a = {}, 27 | lualine_b = {}, 28 | lualine_c = { 'filename' }, 29 | lualine_x = { 'location' }, 30 | lualine_y = {}, 31 | lualine_z = {}, 32 | }, 33 | extensions = { 34 | 'man', 35 | 'quickfix', 36 | 'fugitive', 37 | 'lazy', 38 | 'nvim-dap-ui', 39 | 'toggleterm', 40 | }, 41 | }, 42 | }, 43 | { 'Bekaboo/dropbar.nvim' }, 44 | } 45 | -------------------------------------------------------------------------------- /lua/plugins/treesitter.lua: -------------------------------------------------------------------------------- 1 | return { 2 | { 3 | 'nvim-treesitter/nvim-treesitter', 4 | dependencies = { 5 | 'nvim-treesitter/nvim-treesitter-textobjects', 6 | }, 7 | build = ':TSUpdate', 8 | main = 'nvim-treesitter.configs', 9 | config = function(_, opts) 10 | require('nvim-treesitter.install').compilers = { 'cl', 'clang', 'gcc' } 11 | require('nvim-treesitter.configs').setup(opts) 12 | end, 13 | opts = { 14 | ensure_installed = { 15 | 'c', 16 | 'cpp', 17 | 'cmake', 18 | 'rust', 19 | 'python', 20 | 'lua', 21 | 'vim', 22 | 'javascript', 23 | 'typescript', 24 | 'html', 25 | 'css', 26 | 'svelte', 27 | 'bash', 28 | 'json', 29 | 'toml', 30 | 'yaml', 31 | 'markdown', 32 | 'vimdoc', 33 | 'regex', 34 | }, 35 | highlight = { 36 | enable = true, 37 | disable = { 'latex' }, 38 | }, 39 | textobjects = { 40 | select = { 41 | enable = true, 42 | -- Automatically jump forward to textobj, similar to targets.vim 43 | lookahead = true, 44 | keymaps = { 45 | ['aa'] = '@parameter.outer', 46 | ['ia'] = '@parameter.inner', 47 | ['af'] = '@function.outer', 48 | ['if'] = '@function.inner', 49 | ['ac'] = '@class.outer', 50 | ['ic'] = '@class.inner', 51 | ['ad'] = '@conditional.outer', 52 | ['id'] = '@conditional.inner', 53 | ['ao'] = '@loop.outer', 54 | ['io'] = '@loop.inner', 55 | ['as'] = { query = '@scope', query_group = 'locals' }, 56 | }, 57 | }, 58 | swap = { 59 | enable = true, 60 | swap_next = { 61 | ['a'] = '@parameter.inner', 62 | }, 63 | swap_previous = { 64 | ['A'] = '@parameter.inner', 65 | }, 66 | }, 67 | move = { 68 | enable = true, 69 | set_jumps = true, -- whether to set jumps in the jumplist 70 | goto_next_start = { 71 | [']a'] = '@parameter.inner', 72 | [']m'] = '@function.outer', 73 | [']]'] = '@class.outer', 74 | [']i'] = '@conditional.outer', 75 | [']o'] = '@loop.outer', 76 | [']s'] = { query = '@scope', query_group = 'locals' }, 77 | [']z'] = { query = '@fold', query_group = 'folds' }, 78 | }, 79 | goto_next_end = { 80 | [']A'] = '@parameter.inner', 81 | [']M'] = '@function.outer', 82 | [']['] = '@class.outer', 83 | [']I'] = '@conditional.outer', 84 | [']O'] = '@loop.outer', 85 | [']S'] = { query = '@scope', query_group = 'locals' }, 86 | [']Z'] = { query = '@fold', query_group = 'folds' }, 87 | }, 88 | goto_previous_start = { 89 | ['[a'] = '@parameter.inner', 90 | ['[m'] = '@function.outer', 91 | ['[['] = '@class.outer', 92 | ['[i'] = '@conditional.outer', 93 | ['[o'] = '@loop.outer', 94 | ['[s'] = { query = '@scope', query_group = 'locals' }, 95 | ['[z'] = { query = '@fold', query_group = 'folds' }, 96 | }, 97 | goto_previous_end = { 98 | ['[A'] = '@parameter.inner', 99 | ['[M'] = '@function.outer', 100 | ['[]'] = '@class.outer', 101 | ['[I'] = '@conditional.outer', 102 | ['[O'] = '@loop.outer', 103 | ['[S'] = { query = '@scope', query_group = 'locals' }, 104 | ['[Z'] = { query = '@fold', query_group = 'folds' }, 105 | }, 106 | goto_next = {}, 107 | goto_previous = {}, 108 | }, 109 | }, 110 | }, 111 | }, 112 | } 113 | -------------------------------------------------------------------------------- /lua/plugins/ui.lua: -------------------------------------------------------------------------------- 1 | return { 2 | -- Colorscheme 3 | { 4 | 'catppuccin/nvim', 5 | name = 'catppuccin', 6 | priority = 1000, 7 | opts = { 8 | transparent_background = true, 9 | integrations = { 10 | blink_cmp = true, 11 | diffview = true, 12 | dropbar = true, 13 | fidget = true, 14 | harpoon = true, 15 | mason = true, 16 | noice = true, 17 | snacks = { 18 | enabled = true, 19 | }, 20 | }, 21 | }, 22 | config = function(_, opts) 23 | require('catppuccin').setup(opts) 24 | vim.cmd.colorscheme('catppuccin') 25 | end, 26 | }, 27 | { 28 | 'luukvbaal/statuscol.nvim', 29 | config = function() 30 | local builtin = require('statuscol.builtin') 31 | 32 | require('statuscol').setup({ 33 | segments = { 34 | { text = { builtin.foldfunc }, click = 'v:lua.ScFa' }, 35 | { 36 | sign = { 37 | namespace = { 'diagnostic' }, 38 | maxwidth = 1, 39 | colwidth = 2, 40 | auto = true, 41 | foldclosed = true, 42 | }, 43 | click = 'v:lua.ScSa', 44 | }, 45 | { text = { builtin.lnumfunc }, click = 'v:lua.ScLa' }, 46 | { 47 | sign = { 48 | name = { '.*' }, 49 | text = { '.*' }, 50 | maxwidth = 2, 51 | colwidth = 1, 52 | auto = true, 53 | foldclosed = true, 54 | }, 55 | click = 'v:lua.ScSa', 56 | }, 57 | { 58 | sign = { 59 | namespace = { 'gitsigns' }, 60 | fillchar = '│', 61 | maxwidth = 1, 62 | colwidth = 1, 63 | wrap = true, 64 | foldclosed = true, 65 | }, 66 | click = 'v:lua.ScSa', 67 | }, 68 | }, 69 | }) 70 | end, 71 | }, 72 | { 'yorickpeterse/nvim-pqf', opts = {} }, 73 | { 74 | 'mbbill/undotree', 75 | keys = { 76 | { 'u', 'UndotreeToggle' }, 77 | }, 78 | }, 79 | { 80 | 'folke/todo-comments.nvim', 81 | opts = { 82 | highlight = { 83 | pattern = { [[.*<(KEYWORDS)\s*:]], [[.*<(KEYWORDS)\s*\(\w+\)\s*:]] }, 84 | }, 85 | search = { 86 | pattern = [[\b(KEYWORDS)\s*(\(\w+\))?\s*:]], 87 | }, 88 | }, 89 | }, 90 | { 'j-hui/fidget.nvim', opts = {} }, 91 | { 92 | 'lewis6991/hover.nvim', 93 | config = function() 94 | require('hover').setup({ 95 | init = function() 96 | -- Require providers 97 | require('hover.providers.lsp') 98 | require('hover.providers.gh') 99 | -- require('hover.providers.gh_user') 100 | -- require('hover.providers.jira') 101 | require('hover.providers.dap') 102 | require('hover.providers.fold_preview') 103 | require('hover.providers.diagnostic') 104 | -- require('hover.providers.man') 105 | require('hover.providers.dictionary') 106 | end, 107 | preview_opts = { 108 | border = 'single', 109 | }, 110 | -- Whether the contents of a currently open hover window should be moved 111 | -- to a :h preview-window when pressing the hover keymap. 112 | preview_window = true, 113 | title = true, 114 | -- mouse_providers = { 'LSP', 'Diagnostic' }, 115 | -- mouse_delay = 1000, 116 | }) 117 | 118 | -- Setup keymaps 119 | vim.keymap.set('n', 'K', require('hover').hover, { desc = 'hover.nvim' }) 120 | vim.keymap.set('n', 'gK', require('hover').hover_select, { desc = 'hover.nvim (select)' }) 121 | vim.keymap.set('n', '', function() 122 | require('hover').hover_switch('previous') 123 | end, { desc = 'hover.nvim (previous source)' }) 124 | vim.keymap.set('n', '', function() 125 | require('hover').hover_switch('next') 126 | end, { desc = 'hover.nvim (next source)' }) 127 | 128 | -- Mouse support 129 | vim.keymap.set('n', '', require('hover').hover_mouse, { desc = 'hover.nvim (mouse)' }) 130 | vim.o.mousemoveevent = true 131 | end, 132 | }, 133 | } 134 | -------------------------------------------------------------------------------- /lua/settings.lua: -------------------------------------------------------------------------------- 1 | -- Set mapleader and maplocalloader 2 | vim.g.mapleader = ' ' 3 | vim.g.maplocalleader = ' ' 4 | 5 | -- Better command line completion 6 | vim.o.wildmode = 'longest,full' 7 | 8 | -- Persistent undo, prevent Neovim from having Alzheimer's 9 | vim.o.undofile = true 10 | 11 | -- Use global statusline, not because I made it or anything 12 | vim.o.laststatus = 3 13 | 14 | -- Use statusline area for cmdline 15 | vim.o.cmdheight = 0 16 | 17 | -- Allow virtual editing 18 | vim.o.virtualedit = 'all' 19 | 20 | -- Spaces > Tabs 21 | vim.o.tabstop = 4 22 | vim.o.softtabstop = 4 23 | vim.o.shiftwidth = 4 24 | vim.o.expandtab = true 25 | 26 | -- Linebreak and wrap behavior 27 | vim.o.wrap = true 28 | vim.o.linebreak = true 29 | vim.o.breakindent = true 30 | vim.o.showbreak = '↪ ' 31 | 32 | -- Fill column indicator 33 | vim.o.colorcolumn = '+1' 34 | 35 | -- Show inccommand preview with split 36 | vim.o.inccommand = 'split' 37 | 38 | -- Use transparent fold and use treesitter for folding 39 | vim.o.foldtext = '' 40 | vim.o.foldmethod = 'expr' 41 | vim.o.foldexpr = 'v:lua.vim.treesitter.foldexpr()' 42 | vim.o.foldlevelstart = 20 43 | 44 | -- Session options 45 | vim.o.sessionoptions = 'blank,folds,help,tabpages,winsize,winpos,terminal,localoptions' 46 | 47 | -- Use smartcase for searching 48 | vim.o.ignorecase = true 49 | vim.o.smartcase = true 50 | 51 | -- Make substitute global by default 52 | vim.o.gdefault = true 53 | 54 | -- Settings for insert mode completion 55 | vim.o.completeopt = 'menuone,popup,noinsert,fuzzy' 56 | vim.o.shortmess = vim.o.shortmess .. 'c' 57 | 58 | -- Split behavior 59 | vim.o.splitbelow = true 60 | vim.o.splitright = true 61 | 62 | -- Faster update time 63 | vim.o.updatetime = 100 64 | 65 | -- Highlight current line 66 | vim.o.cursorline = true 67 | 68 | -- Scroll offsets 69 | vim.o.scrolloff = 10 70 | vim.o.sidescrolloff = 5 71 | 72 | -- Scroll based on screen lines instead of logical lines 73 | vim.o.smoothscroll = true 74 | 75 | -- Allow project specific configuration 76 | vim.o.exrc = true 77 | 78 | -- Show hybrid line numbers 79 | vim.o.number = true 80 | vim.o.relativenumber = true 81 | 82 | -- Allow signcolumn to show up to 2 signs 83 | vim.o.signcolumn = 'auto:2' 84 | 85 | -- Enable foldcolumn 86 | vim.o.foldcolumn = '1' 87 | 88 | -- Allow conceal to use replacement characters to hide text 89 | vim.o.conceallevel = 2 90 | 91 | -- Better listchars 92 | vim.o.list = true 93 | vim.o.listchars = 'tab:» ,extends:›,precedes:‹,nbsp:␣' 94 | 95 | -- Remove "How-to disable mouse" from right-click menu 96 | pcall(vim.cmd.aunmenu, [[PopUp.How-to\ disable\ mouse]]) 97 | pcall(vim.cmd.aunmenu, [[PopUp.-2-]]) 98 | 99 | -- Enable experimental ext_cmdline/messages for the TUI 100 | require('vim._extui').enable({}) 101 | -------------------------------------------------------------------------------- /lua/utilities/os.lua: -------------------------------------------------------------------------------- 1 | return { 2 | pu_count = function() 3 | return #vim.uv.cpu_info() 4 | end, 5 | is_linux = function() 6 | return vim.uv.os_uname().sysname == 'Linux' 7 | end, 8 | is_macos = function() 9 | return vim.uv.os_uname().sysname == 'Darwin' 10 | end, 11 | is_windows = function() 12 | return vim.uv.os_uname().sysname == 'Windows_NT' 13 | end, 14 | } 15 | -------------------------------------------------------------------------------- /plugin/autocd.lua: -------------------------------------------------------------------------------- 1 | local root_patterns = { '.git' } 2 | local augroup = vim.api.nvim_create_augroup('AutoCD', {}) 3 | 4 | vim.api.nvim_create_autocmd({ 'VimEnter', 'BufEnter', 'BufReadPost' }, { 5 | desc = 'Automatically change current directory by matching root pattern', 6 | group = augroup, 7 | callback = function(args) 8 | local root = vim.fs.root(vim.api.nvim_buf_get_name(args.buf), root_patterns) 9 | 10 | if root ~= nil then 11 | vim.api.nvim_set_current_dir(root) 12 | end 13 | end, 14 | }) 15 | 16 | vim.api.nvim_create_autocmd('LspAttach', { 17 | desc = 'Automatically change current directory to LSP root', 18 | group = augroup, 19 | callback = function(args) 20 | local client = vim.lsp.get_client_by_id(args.data.client_id) 21 | assert(client ~= nil) 22 | 23 | if client.name ~= 'copilot' and client.config.root_dir then 24 | vim.api.nvim_set_current_dir(client.config.root_dir) 25 | end 26 | end, 27 | }) 28 | -------------------------------------------------------------------------------- /plugin/autocmds.lua: -------------------------------------------------------------------------------- 1 | local augroup = vim.api.nvim_create_augroup('MyConfig', {}) 2 | 3 | vim.api.nvim_create_autocmd('TextYankPost', { 4 | desc = 'Highlight on yank', 5 | group = augroup, 6 | callback = function(opts) 7 | vim.hl.on_yank({ 8 | event = opts.data or vim.v.event, 9 | }) 10 | end, 11 | }) 12 | 13 | -- Automatically create missing directories before save 14 | vim.api.nvim_create_autocmd('BufWritePre', { 15 | callback = function(args) 16 | local path = vim.fs.dirname(vim.api.nvim_buf_get_name(args.buf)) 17 | 18 | if vim.fn.isdirectory(path) == 0 then 19 | vim.fn.mkdir(path, 'p') 20 | end 21 | end, 22 | desc = 'Mkdir on save', 23 | }) 24 | -------------------------------------------------------------------------------- /plugin/delete_hidden_buffers.lua: -------------------------------------------------------------------------------- 1 | --- Command to delete hidden buffers that are not attached to any window. 2 | vim.api.nvim_create_user_command('DeleteHiddenBuffers', function() 3 | --- @type table 4 | local buffers_with_windows = {} 5 | 6 | for _, tp in ipairs(vim.api.nvim_list_tabpages()) do 7 | for _, win in ipairs(vim.api.nvim_tabpage_list_wins(tp)) do 8 | buffers_with_windows[vim.api.nvim_win_get_buf(win)] = true 9 | end 10 | end 11 | 12 | for _, buf in ipairs(vim.api.nvim_list_bufs()) do 13 | if not buffers_with_windows[buf] and vim.api.nvim_buf_is_loaded(buf) and not vim.bo[buf].modified then 14 | vim.api.nvim_buf_delete(buf, { force = true }) 15 | end 16 | end 17 | end, { nargs = 0 }) 18 | -------------------------------------------------------------------------------- /plugin/duplicate_and_comment.lua: -------------------------------------------------------------------------------- 1 | -- Duplicate selection and comment out the first instance. 2 | function _G.duplicate_and_comment_lines() 3 | local start_line, end_line = vim.api.nvim_buf_get_mark(0, '[')[1], vim.api.nvim_buf_get_mark(0, ']')[1] 4 | 5 | -- NOTE: `nvim_buf_get_mark()` is 1-indexed, but `nvim_buf_get_lines()` is 0-indexed. Adjust accordingly. 6 | local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false) 7 | 8 | -- Store cursor position because it might move when commenting out the lines. 9 | local cursor = vim.api.nvim_win_get_cursor(0) 10 | 11 | -- Comment out the selection using the builtin gc operator. 12 | vim.cmd.normal({ 'gcc', range = { start_line, end_line } }) 13 | 14 | -- Append a duplicate of the selected lines to the end of selection. 15 | vim.api.nvim_buf_set_lines(0, end_line, end_line, false, lines) 16 | 17 | -- Move cursor to the start of the duplicate lines. 18 | vim.api.nvim_win_set_cursor(0, { end_line + 1, cursor[2] }) 19 | end 20 | 21 | vim.keymap.set({ 'n', 'x' }, 'yc', function() 22 | vim.opt.operatorfunc = 'v:lua.duplicate_and_comment_lines' 23 | return 'g@' 24 | end, { 25 | desc = 'Duplicate selection and comment out the first instance', 26 | expr = true, 27 | silent = true, 28 | }) 29 | 30 | vim.keymap.set('n', 'ycc', function() 31 | vim.opt.operatorfunc = 'v:lua.duplicate_and_comment_lines' 32 | return 'g@_' 33 | end, { 34 | desc = 'Duplicate [count] lines and comment out the first instance', 35 | expr = true, 36 | silent = true, 37 | }) 38 | -------------------------------------------------------------------------------- /plugin/lastplace.lua: -------------------------------------------------------------------------------- 1 | local no_lastplace_buftypes = { 2 | 'quickfix', 3 | 'nofile', 4 | 'help', 5 | 'terminal', 6 | } 7 | 8 | local no_lastplace_filetypes = { 9 | 'gitcommit', 10 | 'gitrebase', 11 | } 12 | 13 | -- Remember last location in file 14 | vim.api.nvim_create_autocmd('BufReadPost', { 15 | callback = function() 16 | if 17 | vim.fn.line([['"]]) >= 1 18 | and vim.fn.line([['"]]) <= vim.fn.line('$') 19 | and not vim.tbl_contains(no_lastplace_buftypes, vim.o.buftype) 20 | and not vim.tbl_contains(no_lastplace_filetypes, vim.o.filetype) 21 | then 22 | vim.cmd('normal! g`" | zv') 23 | end 24 | end, 25 | desc = 'Remember last place in files', 26 | }) 27 | -------------------------------------------------------------------------------- /plugin/project_manager.lua: -------------------------------------------------------------------------------- 1 | local config = { 2 | storage_file = vim.fn.stdpath('data') .. '/projects.json', 3 | } 4 | 5 | local M = { 6 | projects = {}, 7 | } 8 | 9 | -- Get the current time in seconds since some point in time. 10 | -- Do not rely on the exact epoch time, only on the relative values. 11 | local function get_time() 12 | return math.floor(vim.uv.now() / 1000) 13 | end 14 | 15 | -- Load projects from storage. 16 | local function load_projects() 17 | local file = io.open(config.storage_file, 'r') 18 | if file then 19 | local content = file:read('*all') 20 | file:close() 21 | if content and content ~= '' then 22 | M.projects = vim.json.decode(content) or {} 23 | end 24 | end 25 | end 26 | 27 | -- Save projects to storage. 28 | local function save_projects() 29 | local file = io.open(config.storage_file, 'w') 30 | if file then 31 | file:write(vim.json.encode(M.projects)) 32 | file:close() 33 | else 34 | vim.notify('Failed to save projects', vim.log.levels.ERROR) 35 | end 36 | end 37 | 38 | -- Calculate frecency score for a single path entry. 39 | local function calculate_frecency(path_entry, current_time) 40 | local lambda = 0.01 -- Decay factor 41 | local delta_t = current_time - path_entry.last_access 42 | local recency_factor = 1 / (1 + lambda * delta_t) 43 | return recency_factor * path_entry.frequency 44 | end 45 | 46 | -- Sort projects table by frecency. 47 | local function sort_by_frecency() 48 | local current_time = os.time() 49 | 50 | table.sort(M.projects, function(a, b) 51 | return calculate_frecency(a, current_time) > calculate_frecency(b, current_time) 52 | end) 53 | end 54 | 55 | -- Add a project to the list. 56 | local function add_project(path, manual) 57 | path = vim.fs.normalize(vim.fs.abspath(path or vim.fn.getcwd())) 58 | 59 | -- Check if project already exists. 60 | for _, project in ipairs(M.projects) do 61 | if project.path == path then 62 | if manual then 63 | vim.notify('Project already exists: ' .. path, vim.log.levels.WARN) 64 | end 65 | return 66 | end 67 | end 68 | 69 | table.insert(M.projects, { path = path, frequency = 0, last_access = 0 }) 70 | save_projects() 71 | 72 | if manual then 73 | vim.notify('Added project: ' .. path) 74 | end 75 | end 76 | 77 | -- Delete a project from the list. 78 | local function delete_project(path, manual) 79 | path = vim.fs.normalize(vim.fs.abspath(path or vim.fn.getcwd())) 80 | 81 | for i, project_path in ipairs(M.projects) do 82 | if project_path.path == path then 83 | table.remove(M.projects, i) 84 | save_projects() 85 | 86 | if manual then 87 | vim.notify('Removed project: ' .. path) 88 | end 89 | 90 | return 91 | end 92 | end 93 | 94 | vim.notify('Project not found: ' .. path, vim.log.levels.WARN) 95 | end 96 | 97 | -- Update project metadata when a project is accessed. 98 | local function access_project(path) 99 | for _, project in ipairs(M.projects) do 100 | if project.path == path then 101 | project.frequency = project.frequency + 1 102 | project.last_access = get_time() 103 | save_projects() 104 | return 105 | end 106 | end 107 | end 108 | 109 | -- Show the list of projects, sorted by frecency. 110 | local function show_projects() 111 | if #M.projects == 0 then 112 | error("No projects to show") 113 | end 114 | 115 | sort_by_frecency() 116 | 117 | local project_paths = vim.tbl_map(function(p) 118 | return p.path 119 | end, M.projects) 120 | 121 | local project_name_maxlen = math.max(unpack(vim.tbl_map(function(p) 122 | return #vim.fs.basename(p) 123 | end, project_paths))) 124 | 125 | vim.ui.select(project_paths, { 126 | prompt = 'Select a project:', 127 | format_item = function(item) 128 | return ('%%-%ds\t%%s'):format(project_name_maxlen):format(vim.fs.basename(item), item) 129 | end, 130 | }, function(selected_project) 131 | if selected_project then 132 | access_project(selected_project) 133 | vim.api.nvim_set_current_dir(selected_project) 134 | 135 | Snacks.picker.files() 136 | end 137 | end) 138 | end 139 | 140 | -- Load project list on startup. 141 | load_projects() 142 | 143 | -- Define commands for managing projects. 144 | vim.api.nvim_create_user_command('ProjectList', show_projects, { 145 | desc = 'List all projects', 146 | force = true, 147 | }) 148 | 149 | vim.api.nvim_create_user_command('ProjectAdd', function(opts) 150 | add_project(opts.args ~= '' and opts.args or nil, true) 151 | end, { 152 | desc = 'Add a project (defaults to current directory)', 153 | nargs = '?', 154 | complete = 'dir', 155 | force = true, 156 | }) 157 | 158 | vim.api.nvim_create_user_command('ProjectDelete', function(opts) 159 | delete_project(opts.args ~= '' and opts.args or nil, true) 160 | end, { 161 | desc = 'Delete a project', 162 | nargs = '?', 163 | complete = function(_, _, _) 164 | return vim.tbl_map(function(p) 165 | return p.path 166 | end, M.projects) 167 | end, 168 | force = true, 169 | }) 170 | 171 | vim.api.nvim_create_user_command('ProjectClear', function() 172 | M.projects = {} 173 | save_projects() 174 | vim.notify('Cleared all projects') 175 | end, { 176 | desc = 'Clear all projects', 177 | force = true, 178 | }) 179 | 180 | -- Default keybinding for listing projects. 181 | vim.keymap.set('n', 'w', 'ProjectList', { 182 | desc = 'List all projects', 183 | }) 184 | 185 | return M 186 | -------------------------------------------------------------------------------- /plugin/range_hl.lua: -------------------------------------------------------------------------------- 1 | --- Highlight command ranges 2 | local augroup = vim.api.nvim_create_augroup('HighlightCommandRange', {}) 3 | local hl_ns = vim.api.nvim_create_namespace('HighlightCommandRange') 4 | local timer --- @type uv.uv_timer_t|nil 5 | 6 | local function destroy_timer() 7 | if timer ~= nil and timer:has_ref() then 8 | timer:stop() 9 | if not timer:is_closing() then 10 | timer:close() 11 | end 12 | end 13 | 14 | timer = nil 15 | end 16 | 17 | --- @param buf integer 18 | --- @param range integer[] 19 | local function highlight_range(buf, range) 20 | local line1 = range[1] 21 | local line2 = range[2] ~= nil and range[2] or line1 22 | 23 | if vim.api.nvim_buf_is_loaded(buf) then 24 | vim.hl.range(buf, hl_ns, 'Visual', { line1 - 1, 0 }, { line2 - 1, 0 }, { regtype = 'V' }) 25 | end 26 | end 27 | 28 | --- @param buf integer 29 | local function clear_highlights(buf) 30 | destroy_timer() 31 | 32 | if vim.api.nvim_buf_is_loaded(buf) then 33 | vim.api.nvim_buf_clear_namespace(buf, hl_ns, 0, -1) 34 | end 35 | end 36 | 37 | vim.api.nvim_create_autocmd('CmdlineChanged', { 38 | desc = 'Highlight range of current command', 39 | group = augroup, 40 | callback = function(args) 41 | if vim.v.event.cmdtype ~= ':' then 42 | return 43 | end 44 | 45 | local cmdline = vim.fn.getcmdline() 46 | local ok, parsed_cmdline = pcall(vim.api.nvim_parse_cmd, cmdline, {}) 47 | 48 | if not ok or parsed_cmdline.range == nil or vim.tbl_isempty(parsed_cmdline.range) then 49 | clear_highlights(args.buf) 50 | return 51 | end 52 | 53 | if timer ~= nil then 54 | timer:again() 55 | else 56 | timer = vim.uv.new_timer() 57 | assert(timer ~= nil) 58 | 59 | timer:start( 60 | 50, 61 | 0, 62 | vim.schedule_wrap(function() 63 | clear_highlights(args.buf) 64 | highlight_range(args.buf, parsed_cmdline.range) 65 | end) 66 | ) 67 | end 68 | end, 69 | }) 70 | 71 | vim.api.nvim_create_autocmd('CmdlineLeave', { 72 | desc = 'Clear highlights after leaving cmdline', 73 | group = augroup, 74 | callback = function(args) 75 | clear_highlights(args.buf) 76 | end, 77 | }) 78 | -------------------------------------------------------------------------------- /plugin/tabline.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local fn = vim.fn 3 | 4 | local close_icon = '' 5 | 6 | --- Create tabline component for a single buffer. 7 | --- 8 | --- @param buf integer 9 | --- @param tp_nr integer 10 | --- @return string 11 | local function tabline_buf_component(buf, tp_nr) 12 | local bufname = api.nvim_buf_get_name(buf) 13 | local ok, devicons = pcall(require, 'nvim-web-devicons') 14 | local icon, hl, is_current_tab 15 | 16 | if ok then 17 | icon = devicons.get_icon(bufname, fn.fnamemodify(bufname, ':e'), { default = true }) .. ' ' 18 | else 19 | icon = '' 20 | end 21 | 22 | if bufname == '' then 23 | bufname = '[No Name]' 24 | else 25 | bufname = fn.fnamemodify(bufname, ':.') --[[ @as string ]] 26 | end 27 | 28 | local curr_tp_nr = api.nvim_tabpage_get_number(api.nvim_get_current_tabpage()) 29 | is_current_tab = tp_nr == curr_tp_nr 30 | 31 | if is_current_tab then 32 | hl = 'TabLineSel' 33 | else 34 | hl = 'TabLine' 35 | end 36 | 37 | return string.format( 38 | [[%%#%s#%%%dT [%d] %s%s%s%%T%%%dX%s %%X]], 39 | hl, 40 | tp_nr, 41 | tp_nr, 42 | icon, 43 | bufname, 44 | ' ' .. (vim.bo[buf].modified and '[+]' or '') .. (vim.bo[buf].readonly and '[-]' or ''), 45 | tp_nr, 46 | close_icon 47 | ) 48 | end 49 | 50 | --- Generate tabline tabline to use for the 'tabline' option. 51 | --- The tabline contains a list of all tabs on the left, and a list of all buffers contained in the 52 | --- current working directory of the current tab on the right. 53 | --- 54 | --- @return string 55 | function _G.mytabline() 56 | local tp_elems = {} 57 | 58 | for _, tp in ipairs(api.nvim_list_tabpages()) do 59 | local buf = api.nvim_win_get_buf(api.nvim_tabpage_get_win(tp)) 60 | local tp_nr = api.nvim_tabpage_get_number(tp) 61 | 62 | table.insert(tp_elems, tabline_buf_component(buf, tp_nr)) 63 | end 64 | 65 | return table.concat(tp_elems) .. '%#TabLineFill#' 66 | end 67 | 68 | vim.o.tabline = [[%{%v:lua.mytabline()%}]] 69 | -------------------------------------------------------------------------------- /plugin/to_buffer.lua: -------------------------------------------------------------------------------- 1 | local function to_buffer_complete(arg_lead, cmd_line, cursor_pos) 2 | local to_buffer_pos, wrapped_command_pos = cmd_line:find('ToBuffer%s+') 3 | 4 | if not to_buffer_pos then 5 | return {} 6 | end 7 | 8 | local wrapped_command = cmd_line:sub(wrapped_command_pos + 1, cursor_pos) 9 | local command_name_end = wrapped_command:find('%s') or math.huge 10 | 11 | if wrapped_command == '' or cursor_pos <= command_name_end then 12 | -- If no command yet, complete available Ex commands. 13 | return vim.fn.getcompletion(arg_lead, 'command') 14 | else 15 | -- If a command is present, complete its arguments. 16 | return vim.fn.getcompletion(wrapped_command, 'cmdline') 17 | end 18 | end 19 | 20 | --- @param args vim.api.keyset.create_user_command.command_args 21 | local function to_buffer(args) 22 | local command = table.concat(args.fargs, ' ') 23 | 24 | -- Execute the provided command and capture its output. 25 | local ok, result = pcall(vim.api.nvim_exec2, command, { output = true }) 26 | if not ok then 27 | vim.notify('Error executing command: ' .. command .. '\n' .. result, 'error') 28 | return 29 | end 30 | 31 | -- Create a new scratch buffer and populate it with the command output. 32 | local buf = vim.api.nvim_create_buf(false, true) -- Create a scratch buffer 33 | vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf }) -- Buffer disappears when unloaded 34 | vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(result.output, '\n')) -- Set buffer lines 35 | 36 | -- Open the buffer in a new window. 37 | vim.api.nvim_cmd({ cmd = 'new', mods = args.smods }, {}) 38 | vim.api.nvim_set_current_buf(buf) 39 | end 40 | 41 | -- `:ToBuffer` command. 42 | -- Execute a command and display its output in a new buffer. 43 | vim.api.nvim_create_user_command('ToBuffer', to_buffer, { 44 | nargs = '+', 45 | complete = to_buffer_complete, 46 | force = true, 47 | }) 48 | -------------------------------------------------------------------------------- /plugin/yank_without_indent.lua: -------------------------------------------------------------------------------- 1 | -- Yank selection without leading indent. 2 | function _G.my_yank_without_leading_indent(type) 3 | local tab_width = vim.o.tabstop 4 | local start_line, end_line 5 | 6 | if type == 'char' or type == 'line' then 7 | start_line, end_line = vim.api.nvim_buf_get_mark(0, '[')[1], vim.api.nvim_buf_get_mark(0, ']')[1] 8 | else 9 | start_line, end_line = vim.api.nvim_buf_get_mark(0, '<')[1], vim.api.nvim_buf_get_mark(0, '>')[1] 10 | end 11 | 12 | -- NOTE: `nvim_buf_get_mark()` is 1-indexed, but `nvim_buf_get_lines()` is 0-indexed. Adjust accordingly. 13 | local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false) 14 | 15 | -- Nothing to yank, return early. 16 | if #lines == 0 then 17 | return 18 | end 19 | 20 | -- Find minimum leading whitespace. 21 | local min_indent = math.huge 22 | 23 | for lnum = start_line, end_line do 24 | -- Ignore empty lines 25 | if not lines[lnum - start_line + 1]:match('^%s*$') then 26 | local line_indent = vim.fn.indent(lnum) 27 | min_indent = math.min(min_indent, line_indent) 28 | 29 | -- Break early if no leading whitespace 30 | if min_indent == 0 then 31 | break 32 | end 33 | end 34 | end 35 | 36 | if min_indent ~= 0 then 37 | -- Remove leading whitespace from each line. Make sure to handle tabs correctly. 38 | for i, line in ipairs(lines) do 39 | local lnum = start_line + i - 1 40 | local line_indent = vim.fn.indent(lnum) 41 | 42 | if line_indent >= min_indent then 43 | -- Start by removing tabs, then remove spaces if more indentation needs to be removed. 44 | local tab_count = line:match('^[\t]*'):len() 45 | local tabs_to_remove = math.min(tab_count, math.ceil(min_indent / tab_width)) 46 | line = line:sub(tabs_to_remove + 1) 47 | 48 | if tabs_to_remove * tab_width > min_indent then 49 | -- Removing tabs removed more indentation than necessary, add spaces to compensate. 50 | -- Make sure to add the spaces after the tabs. 51 | local tabs = line:sub(1, line:match('^[\t]*'):len()) 52 | local spaces = string.rep(' ', tabs_to_remove * tab_width - min_indent) 53 | line = tabs .. spaces .. line:sub(tabs:len() + 1) 54 | elseif tabs_to_remove * tab_width < min_indent then 55 | -- Removing tabs did not remove enough indentation, remove spaces if there are any left. 56 | local spaces_to_remove = math.min(line:match('^ *'):len(), min_indent - tabs_to_remove * tab_width) 57 | line = line:sub(spaces_to_remove + 1) 58 | end 59 | 60 | lines[i] = line 61 | end 62 | end 63 | end 64 | 65 | local new_lines = table.concat(lines, '\n') .. '\n' 66 | vim.fn.setreg(vim.v.register, new_lines) 67 | 68 | -- Set yank marks 69 | vim.api.nvim_buf_set_mark(0, '[', start_line, 0, {}) 70 | vim.api.nvim_buf_set_mark(0, ']', end_line, 0, {}) 71 | 72 | vim.api.nvim_exec_autocmds('TextYankPost', { 73 | modeline = false, 74 | data = { 75 | operator = 'y', 76 | regname = vim.v.register, 77 | regtype = vim.fn.getregtype(vim.v.register), 78 | regcontents = new_lines, 79 | }, 80 | }) 81 | end 82 | 83 | vim.keymap.set({ 'n', 'x' }, 'gy', function() 84 | vim.o.operatorfunc = 'v:lua.my_yank_without_leading_indent' 85 | return 'g@' 86 | end, { 87 | desc = 'Yank selection without leading indent', 88 | expr = true, 89 | silent = true, 90 | }) 91 | 92 | vim.keymap.set('n', 'gyy', function() 93 | vim.o.operatorfunc = 'v:lua.my_yank_without_leading_indent' 94 | return 'g@_' 95 | end, { 96 | desc = 'Yank line without leading indent', 97 | expr = true, 98 | silent = true, 99 | }) 100 | --------------------------------------------------------------------------------