-- init.lua -- Based on kickstart.nvim — mirrors emacs setup vim.g.mapleader = ' ' vim.g.maplocalleader = '\\' -- vimtex uses \ll, \lv, etc. vim.g.have_nerd_font = true -- Load work-language extras when the work stow package is active local _work = {} pcall(function() _work = require('work-languages') end) ------------------------------------------------------------------------------- -- Options ------------------------------------------------------------------------------- vim.opt.number = true vim.opt.relativenumber = true vim.opt.mouse = 'a' vim.opt.showmode = false vim.opt.clipboard = 'unnamedplus' vim.opt.breakindent = true vim.opt.undofile = true vim.opt.ignorecase = true vim.opt.smartcase = true vim.opt.signcolumn = 'yes' vim.opt.updatetime = 250 vim.opt.timeoutlen = 300 vim.opt.splitright = true vim.opt.splitbelow = true vim.opt.inccommand = 'split' vim.opt.cursorline = true vim.opt.scrolloff = 8 vim.opt.backup = false vim.opt.swapfile = false vim.opt.writebackup = false vim.opt.expandtab = true vim.opt.shiftwidth = 4 vim.opt.tabstop = 4 vim.opt.smartindent = true vim.opt.list = true vim.opt.listchars = { tab = '» ', trail = '·', nbsp = '␣' } ------------------------------------------------------------------------------- -- Keymaps ------------------------------------------------------------------------------- vim.keymap.set('n', '', 'nohlsearch') vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Prev diagnostic' }) vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Next diagnostic' }) vim.keymap.set('n', 'e', vim.diagnostic.open_float, { desc = 'Show diagnostic' }) vim.keymap.set('n', '', 'h', { desc = 'Move to left window' }) vim.keymap.set('n', '', 'l', { desc = 'Move to right window' }) vim.keymap.set('n', '', 'j', { desc = 'Move to lower window' }) vim.keymap.set('n', '', 'k', { desc = 'Move to upper window' }) vim.keymap.set('t', '', '', { desc = 'Exit terminal mode' }) ------------------------------------------------------------------------------- -- Bootstrap lazy.nvim ------------------------------------------------------------------------------- local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' if not (vim.uv or vim.loop).fs_stat(lazypath) then local out = vim.fn.system({ 'git', 'clone', '--filter=blob:none', '--branch=stable', 'https://github.com/folke/lazy.nvim.git', lazypath, }) if vim.v.shell_error ~= 0 then error('Error cloning lazy.nvim:\n' .. out) end end vim.opt.rtp:prepend(lazypath) ------------------------------------------------------------------------------- -- Plugins ------------------------------------------------------------------------------- require('lazy').setup({ -------------------------------------------------------------------------- -- Theme -------------------------------------------------------------------------- { 'catppuccin/nvim', name = 'catppuccin', priority = 1000, opts = { flavour = 'mocha', integrations = { cmp = true, gitsigns = true, telescope = { enabled = true }, treesitter = true, which_key = true, mason = true, rainbow_delimiters = true, indent_blankline = { enabled = true }, dap = true, dap_ui = true, native_lsp = { enabled = true, underlines = { errors = { 'underline' }, hints = { 'underline' }, warnings = { 'underline' }, information = { 'underline' }, }, }, }, }, config = function(_, opts) require('catppuccin').setup(opts) vim.cmd.colorscheme('catppuccin') end, }, -------------------------------------------------------------------------- -- Status line (doom-modeline equivalent) -------------------------------------------------------------------------- { 'nvim-lualine/lualine.nvim', dependencies = { 'nvim-tree/nvim-web-devicons' }, opts = { options = { theme = 'catppuccin', component_separators = '|', section_separators = '', }, }, }, -------------------------------------------------------------------------- -- UI -------------------------------------------------------------------------- { 'HiPhish/rainbow-delimiters.nvim', config = function() local rainbow = require('rainbow-delimiters') vim.g.rainbow_delimiters = { strategy = { [''] = rainbow.strategy['global'] }, query = { [''] = 'rainbow-delimiters', lua = 'rainbow-blocks' }, highlight = { 'RainbowDelimiterRed', 'RainbowDelimiterYellow', 'RainbowDelimiterBlue', 'RainbowDelimiterOrange', 'RainbowDelimiterGreen', 'RainbowDelimiterViolet', 'RainbowDelimiterCyan', }, } end, }, { 'lukas-reineke/indent-blankline.nvim', main = 'ibl', opts = {}, }, -------------------------------------------------------------------------- -- Which-key (identical to emacs which-key) -------------------------------------------------------------------------- { 'folke/which-key.nvim', event = 'VimEnter', opts = { delay = 1000, icons = { mappings = vim.g.have_nerd_font }, spec = { { 'f', group = 'find' }, { 'l', group = 'lsp' }, { 'd', group = 'debug' }, { 'g', group = 'git' }, { 'b', group = 'buffer' }, { 'x', group = 'latex' }, { 'h', group = 'harpoon' }, }, }, }, -------------------------------------------------------------------------- -- Telescope (vertico + consult + projectile) -------------------------------------------------------------------------- { 'nvim-telescope/telescope.nvim', event = 'VimEnter', branch = '0.1.x', dependencies = { 'nvim-lua/plenary.nvim', { 'nvim-telescope/telescope-fzf-native.nvim', build = 'make', cond = function() return vim.fn.executable('make') == 1 end, }, 'nvim-tree/nvim-web-devicons', }, config = function() require('telescope').setup({ defaults = { file_ignore_patterns = { 'node_modules', '.git/' }, }, }) pcall(require('telescope').load_extension, 'fzf') local b = require('telescope.builtin') vim.keymap.set('n', '', b.find_files, { desc = 'Find files' }) vim.keymap.set('n', 'ff', b.find_files, { desc = 'Find files' }) vim.keymap.set('n', 'fg', b.live_grep, { desc = 'Live grep' }) vim.keymap.set('n', 'fb', b.buffers, { desc = 'Buffers' }) vim.keymap.set('n', 'fh', b.help_tags, { desc = 'Help tags' }) vim.keymap.set('n', 'fr', b.oldfiles, { desc = 'Recent files' }) vim.keymap.set('n', 'fd', b.diagnostics, { desc = 'Diagnostics' }) vim.keymap.set('n', 'fs', b.grep_string, { desc = 'Grep word under cursor' }) vim.keymap.set('n', '/', b.current_buffer_fuzzy_find, { desc = 'Search buffer' }) end, }, -------------------------------------------------------------------------- -- Git — lazygit float + gutter signs -------------------------------------------------------------------------- { 'folke/snacks.nvim', priority = 1000, lazy = false, opts = { lazygit = { enabled = true } }, keys = { { 'gg', function() Snacks.lazygit() end, desc = 'Lazygit' }, }, }, { 'lewis6991/gitsigns.nvim', opts = { signs = { add = { text = '+' }, change = { text = '~' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, }, on_attach = function(bufnr) local gs = require('gitsigns') local map = function(l, r, desc) vim.keymap.set('n', l, r, { buffer = bufnr, desc = desc }) end map(']h', gs.next_hunk, 'Next hunk') map('[h', gs.prev_hunk, 'Prev hunk') map('gs', gs.stage_hunk, 'Stage hunk') map('gr', gs.reset_hunk, 'Reset hunk') map('gS', gs.stage_buffer, 'Stage buffer') map('gp', gs.preview_hunk, 'Preview hunk') map('gb', gs.blame_line, 'Blame line') map('gd', gs.diffthis, 'Diff this') end, }, }, -------------------------------------------------------------------------- -- Treesitter (treesit-auto equivalent) -------------------------------------------------------------------------- { 'nvim-treesitter/nvim-treesitter', build = ':TSUpdate', main = 'nvim-treesitter.configs', opts = { ensure_installed = vim.list_extend( { 'bash', 'c', 'cpp', 'go', 'json', 'lua', 'luadoc', 'markdown', 'markdown_inline', 'rust', 'vim', 'vimdoc', 'yaml', 'latex' }, _work.parsers or {} ), auto_install = true, highlight = { enable = true }, indent = { enable = true }, }, }, -------------------------------------------------------------------------- -- Snippets (yasnippet + yasnippet-snippets equivalent) -------------------------------------------------------------------------- { 'L3MON4D3/LuaSnip', build = 'make install_jsregexp', dependencies = { 'rafamadriz/friendly-snippets' }, config = function() require('luasnip.loaders.from_vscode').lazy_load() end, }, -------------------------------------------------------------------------- -- Completion (company equivalent) -------------------------------------------------------------------------- { 'hrsh7th/nvim-cmp', event = 'InsertEnter', dependencies = { 'saadparwaiz1/cmp_luasnip', 'hrsh7th/cmp-nvim-lsp', 'hrsh7th/cmp-path', 'hrsh7th/cmp-buffer', }, config = function() local cmp = require('cmp') local luasnip = require('luasnip') cmp.setup({ snippet = { expand = function(args) luasnip.lsp_expand(args.body) end, }, completion = { completeopt = 'menu,menuone,noinsert' }, mapping = cmp.mapping.preset.insert({ [''] = cmp.mapping.select_next_item(), [''] = cmp.mapping.select_prev_item(), [''] = cmp.mapping.scroll_docs(-4), [''] = cmp.mapping.scroll_docs(4), [''] = cmp.mapping.complete(), [''] = cmp.mapping.confirm({ select = true }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_next_item() elseif luasnip.expand_or_locally_jumpable() then luasnip.expand_or_jump() else fallback() end end, { 'i', 's' }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_prev_item() elseif luasnip.locally_jumpable(-1) then luasnip.jump(-1) else fallback() end end, { 'i', 's' }), }), sources = cmp.config.sources( { { name = 'nvim_lsp' }, { name = 'luasnip' }, { name = 'path' } }, { { name = 'buffer' } } ), formatting = { format = function(entry, item) item.menu = ({ nvim_lsp = '[LSP]', luasnip = '[Snip]', buffer = '[Buf]', path = '[Path]', })[entry.source.name] return item end, }, }) end, }, -------------------------------------------------------------------------- -- Autopairs (smartparens equivalent) -------------------------------------------------------------------------- { 'windwp/nvim-autopairs', event = 'InsertEnter', config = function() require('nvim-autopairs').setup({}) require('cmp').event:on('confirm_done', require('nvim-autopairs.completion.cmp').on_confirm_done() ) end, }, -------------------------------------------------------------------------- -- Formatting (prettier, stylua, clang-format, etc.) -------------------------------------------------------------------------- { 'stevearc/conform.nvim', event = { 'BufWritePre' }, cmd = { 'ConformInfo' }, keys = { { 'lf', function() require('conform').format({ async = true }) end, mode = { 'n', 'v' }, desc = 'Format buffer', }, }, opts = { formatters_by_ft = vim.tbl_extend('force', { lua = { 'stylua' }, go = { 'goimports', 'gofmt' }, rust = { 'rustfmt' }, c = { 'clang_format' }, cpp = { 'clang_format' }, }, _work.formatters or {}), format_on_save = { timeout_ms = 500, lsp_fallback = true }, }, }, -------------------------------------------------------------------------- -- LSP (eglot equivalent — nvim-lspconfig + mason) -------------------------------------------------------------------------- { 'neovim/nvim-lspconfig', dependencies = { { 'williamboman/mason.nvim', opts = {} }, 'williamboman/mason-lspconfig.nvim', 'WhoIsSethDaniel/mason-tool-installer.nvim', { 'j-hui/fidget.nvim', opts = {} }, }, config = function() vim.api.nvim_create_autocmd('LspAttach', { group = vim.api.nvim_create_augroup('lsp-attach', { clear = true }), callback = function(event) local map = function(keys, func, desc) vim.keymap.set('n', keys, func, { buffer = event.buf, desc = desc }) end local b = require('telescope.builtin') -- mirrors emacs C-c l bindings map('gd', b.lsp_definitions, 'Go to definition') map('gr', b.lsp_references, 'Find references') map('gi', b.lsp_implementations, 'Go to implementation') map('lt', b.lsp_type_definitions, 'Type definition') map('ls', b.lsp_document_symbols, 'Document symbols') map('K', vim.lsp.buf.hover, 'Hover documentation') map('la', vim.lsp.buf.code_action, 'Code action') map('ln', vim.lsp.buf.rename, 'Rename') map('le', vim.diagnostic.open_float, 'Show diagnostics') map('lh', vim.lsp.buf.signature_help, 'Signature help') local client = vim.lsp.get_client_by_id(event.data.client_id) if client then -- highlight references to word under cursor if client.supports_method('textDocument/documentHighlight') then local grp = vim.api.nvim_create_augroup('lsp-highlight', { clear = false }) vim.api.nvim_create_autocmd({ 'CursorHold', 'CursorHoldI' }, { buffer = event.buf, group = grp, callback = vim.lsp.buf.document_highlight, }) vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, { buffer = event.buf, group = grp, callback = vim.lsp.buf.clear_references, }) end -- inlay hints toggle if client.supports_method('textDocument/inlayHint') then map('li', function() vim.lsp.inlay_hint.enable( not vim.lsp.inlay_hint.is_enabled({ bufnr = event.buf }) ) end, 'Toggle inlay hints') end end end, }) local capabilities = vim.tbl_deep_extend('force', vim.lsp.protocol.make_client_capabilities(), require('cmp_nvim_lsp').default_capabilities() ) local servers = vim.tbl_extend('force', { clangd = {}, gopls = {}, rust_analyzer = {}, lua_ls = { settings = { Lua = { completion = { callSnippet = 'Replace' }, diagnostics = { globals = { 'vim', 'Snacks' } }, workspace = { library = vim.api.nvim_get_runtime_file('', true), checkThirdParty = false, }, }, }, }, }, _work.servers or {}) require('mason-tool-installer').setup({ ensure_installed = vim.list_extend({ 'stylua', 'clang-format' }, _work.tools or {}), }) require('mason-lspconfig').setup({ ensure_installed = vim.tbl_keys(servers), handlers = { function(server_name) local cfg = servers[server_name] or {} cfg.capabilities = vim.tbl_deep_extend('force', capabilities, cfg.capabilities or {}) require('lspconfig')[server_name].setup(cfg) end, }, }) end, }, -------------------------------------------------------------------------- -- Debugging (dap-mode equivalent) -------------------------------------------------------------------------- { 'mfussenegger/nvim-dap', dependencies = { { 'rcarriga/nvim-dap-ui', dependencies = { 'nvim-neotest/nvim-nio' }, config = function() local dap, dapui = require('dap'), require('dapui') dapui.setup() -- auto-open/close UI with debug sessions dap.listeners.after.event_initialized['dapui_config'] = dapui.open dap.listeners.before.event_terminated['dapui_config'] = dapui.close dap.listeners.before.event_exited['dapui_config'] = dapui.close end, }, { 'theHamsta/nvim-dap-virtual-text', opts = {} }, { 'leoluz/nvim-dap-go', config = function() require('dap-go').setup() end }, }, config = function() local dap = require('dap') -- codelldb for C / C++ / Rust (installed via mason) dap.adapters.codelldb = { type = 'server', port = '${port}', executable = { command = vim.fn.stdpath('data') .. '/mason/bin/codelldb', args = { '--port', '${port}' }, }, } local codelldb_cfg = {{ name = 'Launch', type = 'codelldb', request = 'launch', program = function() return vim.fn.input('Executable: ', vim.fn.getcwd() .. '/', 'file') end, cwd = '${workspaceFolder}', stopOnEntry = false, }} dap.configurations.c = codelldb_cfg dap.configurations.cpp = codelldb_cfg dap.configurations.rust = codelldb_cfg end, keys = { { 'dd', function() require('dap').continue() end, desc = 'Debug: start/continue' }, { 'db', function() require('dap').toggle_breakpoint() end, desc = 'Debug: toggle breakpoint' }, { 'dn', function() require('dap').step_over() end, desc = 'Debug: step over' }, { 'di', function() require('dap').step_into() end, desc = 'Debug: step into' }, { 'do', function() require('dap').step_out() end, desc = 'Debug: step out' }, { 'dr', function() require('dap').restart() end, desc = 'Debug: restart' }, { 'dq', function() require('dap').disconnect() end, desc = 'Debug: disconnect' }, { 'du', function() require('dapui').toggle() end, desc = 'Debug: toggle UI' }, { 'de', function() require('dapui').eval(vim.fn.input('Eval: ')) end, desc = 'Debug: eval expression' }, }, }, -- install codelldb via mason { 'williamboman/mason.nvim', opts = function(_, opts) opts.ensure_installed = opts.ensure_installed or {} vim.list_extend(opts.ensure_installed, { 'codelldb' }) end, }, -------------------------------------------------------------------------- -- LaTeX (AUCTeX + latexmk + zathura equivalent) -------------------------------------------------------------------------- { 'lervag/vimtex', lazy = false, init = function() vim.g.vimtex_view_method = 'zathura' vim.g.vimtex_compiler_method = 'latexmk' vim.g.vimtex_compiler_latexmk = { options = { '-pdf', '-interaction=nonstopmode', '-synctex=1' }, } end, }, -------------------------------------------------------------------------- -- Harpoon -------------------------------------------------------------------------- { 'ThePrimeagen/harpoon', branch = 'harpoon2', dependencies = { 'nvim-lua/plenary.nvim' }, config = function() local harpoon = require('harpoon') harpoon:setup() vim.keymap.set('n', 'a', function() harpoon:list():add() end, { desc = 'Harpoon: add file' }) vim.keymap.set('n', 'h', function() harpoon.ui:toggle_quick_menu(harpoon:list()) end, { desc = 'Harpoon: menu' }) vim.keymap.set('n', '1', function() harpoon:list():select(1) end, { desc = 'Harpoon: file 1' }) vim.keymap.set('n', '2', function() harpoon:list():select(2) end, { desc = 'Harpoon: file 2' }) vim.keymap.set('n', '3', function() harpoon:list():select(3) end, { desc = 'Harpoon: file 3' }) vim.keymap.set('n', '4', function() harpoon:list():select(4) end, { desc = 'Harpoon: file 4' }) end, }, -------------------------------------------------------------------------- -- Misc -------------------------------------------------------------------------- { 'tpope/vim-sleuth' }, -- automatic indent detection { 'folke/todo-comments.nvim', event = 'VimEnter', dependencies = { 'nvim-lua/plenary.nvim' }, opts = { signs = false }, }, }, { ui = { icons = vim.g.have_nerd_font and {} or { cmd = '⌘', config = '🛠', event = '📅', ft = '📂', init = '⚙', keys = '🗝', plugin = '🔌', runtime = '💻', source = '📄', start = '🚀', task = '📌', lazy = '💤', }, }, })