# Batteries-included Neovim configuration for developer productivity { inputs, lib, config, pkgs, ... }: { programs.neovim = { enable = true; viAlias = true; vimAlias = true; defaultEditor = true; # External tools needed by plugins extraPackages = with pkgs; [ # LSP servers lua-language-server nil # Nix LSP nodePackages.typescript-language-server nodePackages.vscode-langservers-extracted # HTML/CSS/JSON/ESLint pyright rust-analyzer gopls # Formatters & linters stylua prettierd nixpkgs-fmt black isort shfmt # Telescope dependencies ripgrep fd # Other tools tree-sitter ]; plugins = with pkgs.vimPlugins; [ # =================== # Core / Dependencies # =================== plenary-nvim nvim-web-devicons # =================== # Treesitter (syntax highlighting & more) # =================== { plugin = nvim-treesitter.withAllGrammars; type = "lua"; config = '' require('nvim-treesitter.configs').setup({ highlight = { enable = true }, indent = { enable = true }, incremental_selection = { enable = true, keymaps = { init_selection = "", node_incremental = "", scope_incremental = false, node_decremental = "", }, }, }) ''; } nvim-treesitter-textobjects nvim-treesitter-context # Show context at top of screen # =================== # LSP & Completion # =================== { plugin = nvim-lspconfig; type = "lua"; config = '' local lspconfig = require('lspconfig') local capabilities = require('cmp_nvim_lsp').default_capabilities() -- Common on_attach function local on_attach = function(client, bufnr) local opts = { buffer = bufnr, noremap = true, silent = true } vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts) vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts) vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts) vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts) vim.keymap.set('n', '', vim.lsp.buf.signature_help, opts) vim.keymap.set('n', 'rn', vim.lsp.buf.rename, opts) vim.keymap.set('n', 'ca', vim.lsp.buf.code_action, opts) vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts) vim.keymap.set('n', 'f', function() vim.lsp.buf.format({ async = true }) end, opts) vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, opts) vim.keymap.set('n', ']d', vim.diagnostic.goto_next, opts) vim.keymap.set('n', 'e', vim.diagnostic.open_float, opts) end -- LSP servers local servers = { 'lua_ls', 'nil_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'gopls', 'html', 'cssls', 'jsonls' } for _, lsp in ipairs(servers) do lspconfig[lsp].setup({ on_attach = on_attach, capabilities = capabilities, }) end -- Lua specific settings lspconfig.lua_ls.setup({ on_attach = on_attach, capabilities = capabilities, settings = { Lua = { diagnostics = { globals = { 'vim' } }, workspace = { checkThirdParty = false }, telemetry = { enable = false }, }, }, }) ''; } # Completion engine { plugin = nvim-cmp; type = "lua"; config = '' local cmp = require('cmp') local luasnip = require('luasnip') cmp.setup({ snippet = { expand = function(args) luasnip.lsp_expand(args.body) end, }, mapping = cmp.mapping.preset.insert({ [''] = cmp.mapping.scroll_docs(-4), [''] = cmp.mapping.scroll_docs(4), [''] = cmp.mapping.complete(), [''] = cmp.mapping.abort(), [''] = cmp.mapping.confirm({ select = true }), [''] = cmp.mapping(function(fallback) if cmp.visible() then cmp.select_next_item() elseif luasnip.expand_or_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.jumpable(-1) then luasnip.jump(-1) else fallback() end end, { 'i', 's' }), }), sources = cmp.config.sources({ { name = 'nvim_lsp' }, { name = 'luasnip' }, { name = 'buffer' }, { name = 'path' }, }), window = { completion = cmp.config.window.bordered(), documentation = cmp.config.window.bordered(), }, }) ''; } cmp-nvim-lsp cmp-buffer cmp-path cmp_luasnip luasnip friendly-snippets # =================== # Telescope (fuzzy finder) # =================== { plugin = telescope-nvim; type = "lua"; config = '' local telescope = require('telescope') local builtin = require('telescope.builtin') telescope.setup({ defaults = { file_ignore_patterns = { "node_modules", ".git/", "target/", "dist/" }, mappings = { i = { [""] = "move_selection_next", [""] = "move_selection_previous", }, }, }, pickers = { find_files = { hidden = true }, }, }) telescope.load_extension('fzf') -- Keymaps vim.keymap.set('n', 'ff', builtin.find_files, { desc = 'Find files' }) vim.keymap.set('n', 'fg', builtin.live_grep, { desc = 'Live grep' }) vim.keymap.set('n', 'fb', builtin.buffers, { desc = 'Find buffers' }) vim.keymap.set('n', 'fh', builtin.help_tags, { desc = 'Help tags' }) vim.keymap.set('n', 'fr', builtin.oldfiles, { desc = 'Recent files' }) vim.keymap.set('n', 'fs', builtin.lsp_document_symbols, { desc = 'Document symbols' }) vim.keymap.set('n', 'fw', builtin.grep_string, { desc = 'Grep word under cursor' }) vim.keymap.set('n', '', builtin.find_files, { desc = 'Find files' }) ''; } telescope-fzf-native-nvim # =================== # File Explorer # =================== { plugin = neo-tree-nvim; type = "lua"; config = '' require('neo-tree').setup({ close_if_last_window = true, filesystem = { follow_current_file = { enabled = true }, use_libuv_file_watcher = true, filtered_items = { hide_dotfiles = false, hide_gitignored = false, }, }, window = { width = 35, mappings = { [""] = "none", }, }, }) vim.keymap.set('n', 'e', ':Neotree toggle', { silent = true, desc = 'Toggle file explorer' }) vim.keymap.set('n', 'o', ':Neotree focus', { silent = true, desc = 'Focus file explorer' }) ''; } # =================== # Git Integration # =================== { plugin = gitsigns-nvim; type = "lua"; config = '' require('gitsigns').setup({ signs = { add = { text = '│' }, change = { text = '│' }, delete = { text = '_' }, topdelete = { text = '‾' }, changedelete = { text = '~' }, }, on_attach = function(bufnr) local gs = package.loaded.gitsigns local opts = { buffer = bufnr } vim.keymap.set('n', ']c', function() if vim.wo.diff then return ']c' end vim.schedule(function() gs.next_hunk() end) return '' end, { expr = true, buffer = bufnr }) vim.keymap.set('n', '[c', function() if vim.wo.diff then return '[c' end vim.schedule(function() gs.prev_hunk() end) return '' end, { expr = true, buffer = bufnr }) vim.keymap.set('n', 'hs', gs.stage_hunk, opts) vim.keymap.set('n', 'hr', gs.reset_hunk, opts) vim.keymap.set('n', 'hS', gs.stage_buffer, opts) vim.keymap.set('n', 'hu', gs.undo_stage_hunk, opts) vim.keymap.set('n', 'hp', gs.preview_hunk, opts) vim.keymap.set('n', 'hb', function() gs.blame_line({ full = true }) end, opts) end, }) ''; } vim-fugitive # =================== # UI Enhancements # =================== { plugin = lualine-nvim; type = "lua"; config = '' require('lualine').setup({ options = { theme = 'auto', component_separators = { left = "", right = "" }, section_separators = { left = "", right = "" }, globalstatus = true, }, sections = { lualine_a = { 'mode' }, lualine_b = { 'branch', 'diff', 'diagnostics' }, lualine_c = { { 'filename', path = 1 } }, lualine_x = { 'encoding', 'fileformat', 'filetype' }, lualine_y = { 'progress' }, lualine_z = { 'location' } }, }) ''; } { plugin = bufferline-nvim; type = "lua"; config = '' require('bufferline').setup({ options = { diagnostics = "nvim_lsp", offsets = { { filetype = "neo-tree", text = "File Explorer", highlight = "Directory" } }, show_buffer_close_icons = true, show_close_icon = false, }, }) vim.keymap.set('n', '', ':BufferLineCycleNext', { silent = true }) vim.keymap.set('n', '', ':BufferLineCyclePrev', { silent = true }) vim.keymap.set('n', 'bp', ':BufferLineTogglePin', { silent = true }) vim.keymap.set('n', 'bc', ':BufferLinePickClose', { silent = true }) ''; } { plugin = indent-blankline-nvim; type = "lua"; config = '' require('ibl').setup({ indent = { char = "│" }, scope = { enabled = true }, }) ''; } # Color scheme { plugin = catppuccin-nvim; type = "lua"; config = '' require('catppuccin').setup({ flavour = 'mocha', integrations = { cmp = true, gitsigns = true, treesitter = true, telescope = { enabled = true }, neo_tree = true, indent_blankline = { enabled = true }, native_lsp = { enabled = true }, }, }) vim.cmd.colorscheme('catppuccin') ''; } # =================== # Editor Enhancements # =================== { plugin = which-key-nvim; type = "lua"; config = '' require('which-key').setup({}) ''; } { plugin = comment-nvim; type = "lua"; config = '' require('Comment').setup() ''; } { plugin = nvim-autopairs; type = "lua"; config = '' require('nvim-autopairs').setup({ check_ts = true, }) -- Integrate with nvim-cmp local cmp_autopairs = require('nvim-autopairs.completion.cmp') local cmp = require('cmp') cmp.event:on('confirm_done', cmp_autopairs.on_confirm_done()) ''; } { plugin = nvim-surround; type = "lua"; config = '' require('nvim-surround').setup({}) ''; } { plugin = todo-comments-nvim; type = "lua"; config = '' require('todo-comments').setup({}) vim.keymap.set('n', 'ft', ':TodoTelescope', { silent = true, desc = 'Find TODOs' }) ''; } { plugin = trouble-nvim; type = "lua"; config = '' require('trouble').setup({}) vim.keymap.set('n', 'xx', ':Trouble diagnostics toggle', { silent = true, desc = 'Diagnostics' }) vim.keymap.set('n', 'xd', ':Trouble diagnostics toggle filter.buf=0', { silent = true, desc = 'Buffer diagnostics' }) ''; } # Flash for quick navigation { plugin = flash-nvim; type = "lua"; config = '' require('flash').setup({}) vim.keymap.set({ 'n', 'x', 'o' }, 's', function() require('flash').jump() end, { desc = 'Flash' }) vim.keymap.set({ 'n', 'x', 'o' }, 'S', function() require('flash').treesitter() end, { desc = 'Flash Treesitter' }) ''; } # Mini.nvim utilities { plugin = mini-nvim; type = "lua"; config = '' require('mini.ai').setup() -- Better text objects require('mini.splitjoin').setup() -- Split/join arguments require('mini.move').setup() -- Move lines/selections ''; } # Formatting { plugin = conform-nvim; type = "lua"; config = '' require('conform').setup({ formatters_by_ft = { lua = { 'stylua' }, python = { 'isort', 'black' }, javascript = { 'prettierd' }, typescript = { 'prettierd' }, javascriptreact = { 'prettierd' }, typescriptreact = { 'prettierd' }, json = { 'prettierd' }, html = { 'prettierd' }, css = { 'prettierd' }, nix = { 'nixpkgs_fmt' }, sh = { 'shfmt' }, bash = { 'shfmt' }, }, format_on_save = { timeout_ms = 500, lsp_fallback = true, }, }) ''; } ]; # Core Neovim options extraLuaConfig = '' -- Leader key vim.g.mapleader = ' ' vim.g.maplocalleader = ' ' -- Basic options vim.opt.number = true vim.opt.relativenumber = true vim.opt.expandtab = true vim.opt.tabstop = 2 vim.opt.shiftwidth = 2 vim.opt.smartindent = true vim.opt.clipboard = 'unnamedplus' vim.opt.mouse = 'a' vim.opt.ignorecase = true vim.opt.smartcase = true vim.opt.hlsearch = true vim.opt.incsearch = true vim.opt.wrap = false vim.opt.scrolloff = 8 vim.opt.sidescrolloff = 8 vim.opt.signcolumn = 'yes' vim.opt.updatetime = 250 vim.opt.timeoutlen = 300 vim.opt.splitright = true vim.opt.splitbelow = true vim.opt.cursorline = true vim.opt.termguicolors = true vim.opt.undofile = true vim.opt.completeopt = 'menu,menuone,noselect' -- Better diagnostics display vim.diagnostic.config({ virtual_text = true, signs = true, underline = true, update_in_insert = false, severity_sort = true, float = { border = 'rounded', source = 'always', }, }) -- Essential keymaps local opts = { noremap = true, silent = true } -- Better window navigation vim.keymap.set('n', '', 'h', opts) vim.keymap.set('n', '', 'j', opts) vim.keymap.set('n', '', 'k', opts) vim.keymap.set('n', '', 'l', opts) -- Resize windows vim.keymap.set('n', '', ':resize +2', opts) vim.keymap.set('n', '', ':resize -2', opts) vim.keymap.set('n', '', ':vertical resize -2', opts) vim.keymap.set('n', '', ':vertical resize +2', opts) -- Move text up and down in visual mode vim.keymap.set('v', 'J', ":m '>+1gv=gv", opts) vim.keymap.set('v', 'K', ":m '<-2gv=gv", opts) -- Stay in indent mode vim.keymap.set('v', '<', '', '>gv', opts) -- Clear search highlight vim.keymap.set('n', '', ':nohlsearch', opts) -- Buffer management vim.keymap.set('n', 'bd', ':bdelete', opts) vim.keymap.set('n', 'bn', ':bnext', opts) vim.keymap.set('n', 'bp', ':bprevious', opts) -- Quick save/quit vim.keymap.set('n', 'w', ':w', opts) vim.keymap.set('n', 'q', ':q', opts) vim.keymap.set('n', 'Q', ':qa!', opts) -- Better paste (don't yank replaced text) vim.keymap.set('v', 'p', '"_dP', opts) -- Center screen after jumps vim.keymap.set('n', '', 'zz', opts) vim.keymap.set('n', '', 'zz', opts) vim.keymap.set('n', 'n', 'nzzzv', opts) vim.keymap.set('n', 'N', 'Nzzzv', opts) ''; }; }