583 lines
18 KiB
Nix
583 lines
18 KiB
Nix
# 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 = "<C-space>",
|
|
node_incremental = "<C-space>",
|
|
scope_incremental = false,
|
|
node_decremental = "<bs>",
|
|
},
|
|
},
|
|
})
|
|
'';
|
|
}
|
|
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', '<C-k>', vim.lsp.buf.signature_help, opts)
|
|
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
|
|
vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
|
|
vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
|
|
vim.keymap.set('n', '<leader>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', '<leader>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({
|
|
['<C-b>'] = cmp.mapping.scroll_docs(-4),
|
|
['<C-f>'] = cmp.mapping.scroll_docs(4),
|
|
['<C-Space>'] = cmp.mapping.complete(),
|
|
['<C-e>'] = cmp.mapping.abort(),
|
|
['<CR>'] = cmp.mapping.confirm({ select = true }),
|
|
['<Tab>'] = 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' }),
|
|
['<S-Tab>'] = 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 = {
|
|
["<C-j>"] = "move_selection_next",
|
|
["<C-k>"] = "move_selection_previous",
|
|
},
|
|
},
|
|
},
|
|
pickers = {
|
|
find_files = { hidden = true },
|
|
},
|
|
})
|
|
|
|
telescope.load_extension('fzf')
|
|
|
|
-- Keymaps
|
|
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'Find files' })
|
|
vim.keymap.set('n', '<leader>fg', builtin.live_grep, { desc = 'Live grep' })
|
|
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'Find buffers' })
|
|
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'Help tags' })
|
|
vim.keymap.set('n', '<leader>fr', builtin.oldfiles, { desc = 'Recent files' })
|
|
vim.keymap.set('n', '<leader>fs', builtin.lsp_document_symbols, { desc = 'Document symbols' })
|
|
vim.keymap.set('n', '<leader>fw', builtin.grep_string, { desc = 'Grep word under cursor' })
|
|
vim.keymap.set('n', '<C-p>', 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 = {
|
|
["<space>"] = "none",
|
|
},
|
|
},
|
|
})
|
|
vim.keymap.set('n', '<leader>e', ':Neotree toggle<CR>', { silent = true, desc = 'Toggle file explorer' })
|
|
vim.keymap.set('n', '<leader>o', ':Neotree focus<CR>', { 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 '<Ignore>'
|
|
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 '<Ignore>'
|
|
end, { expr = true, buffer = bufnr })
|
|
|
|
vim.keymap.set('n', '<leader>hs', gs.stage_hunk, opts)
|
|
vim.keymap.set('n', '<leader>hr', gs.reset_hunk, opts)
|
|
vim.keymap.set('n', '<leader>hS', gs.stage_buffer, opts)
|
|
vim.keymap.set('n', '<leader>hu', gs.undo_stage_hunk, opts)
|
|
vim.keymap.set('n', '<leader>hp', gs.preview_hunk, opts)
|
|
vim.keymap.set('n', '<leader>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', '<S-l>', ':BufferLineCycleNext<CR>', { silent = true })
|
|
vim.keymap.set('n', '<S-h>', ':BufferLineCyclePrev<CR>', { silent = true })
|
|
vim.keymap.set('n', '<leader>bp', ':BufferLineTogglePin<CR>', { silent = true })
|
|
vim.keymap.set('n', '<leader>bc', ':BufferLinePickClose<CR>', { 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', '<leader>ft', ':TodoTelescope<CR>', { silent = true, desc = 'Find TODOs' })
|
|
'';
|
|
}
|
|
|
|
{
|
|
plugin = trouble-nvim;
|
|
type = "lua";
|
|
config = ''
|
|
require('trouble').setup({})
|
|
vim.keymap.set('n', '<leader>xx', ':Trouble diagnostics toggle<CR>', { silent = true, desc = 'Diagnostics' })
|
|
vim.keymap.set('n', '<leader>xd', ':Trouble diagnostics toggle filter.buf=0<CR>', { 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', '<C-h>', '<C-w>h', opts)
|
|
vim.keymap.set('n', '<C-j>', '<C-w>j', opts)
|
|
vim.keymap.set('n', '<C-k>', '<C-w>k', opts)
|
|
vim.keymap.set('n', '<C-l>', '<C-w>l', opts)
|
|
|
|
-- Resize windows
|
|
vim.keymap.set('n', '<C-Up>', ':resize +2<CR>', opts)
|
|
vim.keymap.set('n', '<C-Down>', ':resize -2<CR>', opts)
|
|
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', opts)
|
|
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', opts)
|
|
|
|
-- Move text up and down in visual mode
|
|
vim.keymap.set('v', 'J', ":m '>+1<CR>gv=gv", opts)
|
|
vim.keymap.set('v', 'K', ":m '<-2<CR>gv=gv", opts)
|
|
|
|
-- Stay in indent mode
|
|
vim.keymap.set('v', '<', '<gv', opts)
|
|
vim.keymap.set('v', '>', '>gv', opts)
|
|
|
|
-- Clear search highlight
|
|
vim.keymap.set('n', '<Esc>', ':nohlsearch<CR>', opts)
|
|
|
|
-- Buffer management
|
|
vim.keymap.set('n', '<leader>bd', ':bdelete<CR>', opts)
|
|
vim.keymap.set('n', '<leader>bn', ':bnext<CR>', opts)
|
|
vim.keymap.set('n', '<leader>bp', ':bprevious<CR>', opts)
|
|
|
|
-- Quick save/quit
|
|
vim.keymap.set('n', '<leader>w', ':w<CR>', opts)
|
|
vim.keymap.set('n', '<leader>q', ':q<CR>', opts)
|
|
vim.keymap.set('n', '<leader>Q', ':qa!<CR>', opts)
|
|
|
|
-- Better paste (don't yank replaced text)
|
|
vim.keymap.set('v', 'p', '"_dP', opts)
|
|
|
|
-- Center screen after jumps
|
|
vim.keymap.set('n', '<C-d>', '<C-d>zz', opts)
|
|
vim.keymap.set('n', '<C-u>', '<C-u>zz', opts)
|
|
vim.keymap.set('n', 'n', 'nzzzv', opts)
|
|
vim.keymap.set('n', 'N', 'Nzzzv', opts)
|
|
'';
|
|
};
|
|
}
|
|
|