nix/home/neovim/config/init.lua
2025-11-19 21:25:54 -06:00

497 lines
16 KiB
Lua

vim.g.mapleader = ' '
vim.g.maplocalleader = ' '
vim.opt.number = 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.list = true
vim.opt.listchars = { tab = '» ', trail = '·', nbsp = '' }
vim.opt.inccommand = 'split'
vim.opt.autoread = true
vim.opt.cursorline = true
vim.opt.scrolloff = 10
local path_package = vim.fn.stdpath('data') .. '/site/'
local mini_path = path_package .. 'pack/deps/start/mini.nvim'
if not vim.uv.fs_stat(mini_path) then
vim.cmd('echo "Installing mini.nvim" | redraw')
vim.fn.system({
'git', 'clone', '--filter=blob:none',
'https://github.com/echasnovski/mini.nvim', mini_path
})
vim.cmd('packadd mini.nvim | helptags ALL')
end
require('mini.deps').setup({ path = { package = path_package } })
local add, now, later = MiniDeps.add, MiniDeps.now, MiniDeps.later
add('folke/snacks.nvim')
add('stevearc/quicker.nvim')
add('stevearc/oil.nvim')
add('A7Lavinraj/fyler.nvim')
add('EdenEast/nightfox.nvim')
add('williamboman/mason.nvim')
add('williamboman/mason-lspconfig.nvim')
add('neovim/nvim-lspconfig')
add('folke/lazydev.nvim')
add('Bilal2453/luvit-meta')
add('NeogitOrg/neogit')
add('nvim-lua/plenary.nvim')
add('sindrets/diffview.nvim')
add('folke/which-key.nvim')
add({
source = 'saghen/blink.cmp',
checkout = 'v1.8.0',
})
add('stevearc/conform.nvim')
add('coder/claudecode.nvim')
now(function()
require('mini.icons').setup({
extension = {
lua = { glyph = '󰢱', hl = 'MiniIconsAzure' },
},
file = {
['init.lua'] = { glyph = '󰢱', hl = 'MiniIconsAzure' },
},
default = {
file = { glyph = '󰈔', hl = 'MiniIconsGrey' },
directory = { glyph = '󰉋', hl = 'MiniIconsBlue' },
},
})
require('nightfox').setup({})
vim.cmd('colorscheme nightfox')
end)
later(function()
require('snacks').setup({
picker = {
prompt = '󰍉 ',
icons = {
enabled = true,
},
win = {
input = {
keys = {
['<C-h>'] = false,
},
},
},
},
})
require('quicker').setup({
keys = {
{
'>',
function() require('quicker').expand({ before = 2, after = 2, add_to_existing = true }) end,
desc = 'Expand quickfix context',
},
{
'<',
function() require('quicker').collapse() end,
desc = 'Collapse quickfix context',
},
},
})
-- Function to show current directory in oil winbar
_G.get_oil_winbar = function()
local bufnr = vim.api.nvim_win_get_buf(vim.g.statusline_winid or 0)
local dir = require('oil').get_current_dir(bufnr)
if dir then
return vim.fn.fnamemodify(dir, ':~')
else
return vim.api.nvim_buf_get_name(bufnr)
end
end
require('oil').setup({
view_options = { show_hidden = true },
columns = {
'icon',
'permissions',
'size',
'mtime',
},
win_options = {
winbar = '%!v:lua.get_oil_winbar()',
},
float = {
padding = 2,
max_width = 0,
max_height = 10,
border = 'rounded',
win_options = {
winblend = 0,
},
override = function(conf)
conf.anchor = 'SW'
conf.row = vim.o.lines - vim.o.cmdheight - 1
conf.col = 0
conf.width = vim.o.columns
return conf
end,
},
keymaps = {
['q'] = 'actions.close',
['<C-c>'] = 'actions.close',
-- Emacs-like navigation (up/down)
['<C-p>'] = 'k',
['<C-n>'] = 'j',
-- Preview with Ctrl+Space
['<C-Space>'] = 'actions.preview',
-- Copy current directory path to clipboard
['gy'] = {
callback = function()
local oil = require('oil')
local dir = oil.get_current_dir()
if dir then
vim.fn.setreg('+', dir)
vim.notify('Copied: ' .. dir, vim.log.levels.INFO)
end
end,
desc = 'Copy current directory path',
},
},
})
require('fyler').setup({
views = {
finder = {
confirm_simple = true,
close_on_select = false,
},
},
})
require('emacs-find-file').setup({
max_matches = 8,
on_directory_enter = function(dir)
require('oil').open(dir)
end,
})
require('neogit').setup({
integrations = {
diffview = true,
snacks = true,
},
})
require('diffview').setup({
enhanced_diff_hl = true,
})
require('which-key').setup({
preset = 'modern',
icons = {
separator = '',
group = '',
},
win = {
border = 'rounded',
},
})
require('claudecode').setup({})
require('conform').setup({
formatters_by_ft = {
lua = { 'stylua' },
python = { 'isort', 'black' },
rust = { 'rustfmt' },
javascript = { 'prettierd', 'prettier', stop_after_first = true },
typescript = { 'prettierd', 'prettier', stop_after_first = true },
javascriptreact = { 'prettierd', 'prettier', stop_after_first = true },
typescriptreact = { 'prettierd', 'prettier', stop_after_first = true },
json = { 'prettierd', 'prettier', stop_after_first = true },
yaml = { 'prettierd', 'prettier', stop_after_first = true },
markdown = { 'prettierd', 'prettier', stop_after_first = true },
html = { 'prettierd', 'prettier', stop_after_first = true },
css = { 'prettierd', 'prettier', stop_after_first = true },
c = { 'clang_format' },
cpp = { 'clang_format' },
},
})
-- Format command
vim.api.nvim_create_user_command('Format', function(_)
require('conform').format({ async = true, lsp_format = 'fallback' })
end, { desc = 'Format current buffer' })
end)
later(function()
local has_words_before = function()
local col = vim.api.nvim_win_get_cursor(0)[2]
if col == 0 then
return false
end
local line = vim.api.nvim_get_current_line()
return line:sub(col, col):match("%s") == nil
end
require('blink.cmp').setup({
keymap = {
preset = 'none',
['<C-space>'] = { 'show', 'show_documentation', 'hide_documentation' },
['<C-e>'] = { 'hide' },
['<CR>'] = { 'accept', 'fallback' },
['<Tab>'] = {
function(cmp)
if has_words_before() then
return cmp.insert_next()
end
end,
'fallback',
},
['<S-Tab>'] = { 'insert_prev' },
['<C-n>'] = { 'select_next', 'fallback' },
['<C-p>'] = { 'select_prev', 'fallback' },
},
appearance = {
use_nvim_cmp_as_default = true,
nerd_font_variant = 'mono',
},
sources = {
default = { 'lsp', 'path', 'buffer' },
},
completion = {
menu = {
enabled = true,
border = 'rounded',
},
list = {
selection = {
preselect = false,
},
cycle = {
from_top = false,
},
},
documentation = {
window = {
border = 'rounded',
},
auto_show = true,
auto_show_delay_ms = 200,
},
},
signature = {
enabled = true,
window = {
border = 'rounded',
},
},
})
end)
later(function()
require('mason').setup({
ui = {
border = 'rounded',
icons = {
package_installed = '',
package_pending = '',
package_uninstalled = '',
},
},
})
require('mason-lspconfig').setup({
ensure_installed = {
'lua_ls', -- Lua
'pyright', -- Python
'rust_analyzer', -- Rust
'clangd', -- C/C++
},
automatic_installation = true,
})
-- LSP keybindings and options
vim.api.nvim_create_autocmd('LspAttach', {
group = vim.api.nvim_create_augroup('UserLspConfig', {}),
callback = function(ev)
local opts = { buffer = ev.buf }
vim.keymap.set('n', 'gd', function() require('snacks').picker.lsp_definitions() end, vim.tbl_extend('force', opts, { desc = 'Goto Definition' }))
vim.keymap.set('n', 'gD', function() require('snacks').picker.lsp_declarations() end, vim.tbl_extend('force', opts, { desc = 'Goto Declaration' }))
vim.keymap.set('n', 'gr', function() require('snacks').picker.lsp_references() end, vim.tbl_extend('force', opts, { desc = 'References', nowait = true }))
vim.keymap.set('n', 'gI', function() require('snacks').picker.lsp_implementations() end, vim.tbl_extend('force', opts, { desc = 'Goto Implementation' }))
vim.keymap.set('n', 'gy', function() require('snacks').picker.lsp_type_definitions() end, vim.tbl_extend('force', opts, { desc = 'Goto T[y]pe Definition' }))
vim.keymap.set('n', 'gai', function() require('snacks').picker.lsp_incoming_calls() end, vim.tbl_extend('force', opts, { desc = 'C[a]lls Incoming' }))
vim.keymap.set('n', 'gao', function() require('snacks').picker.lsp_outgoing_calls() end, vim.tbl_extend('force', opts, { desc = 'C[a]lls Outgoing' }))
vim.keymap.set('n', 'K', vim.lsp.buf.hover, vim.tbl_extend('force', opts, { desc = 'Hover documentation (press K again to focus)' }))
vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, vim.tbl_extend('force', opts, { desc = 'Signature help' }))
vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, vim.tbl_extend('force', opts, { desc = 'Rename symbol' }))
vim.keymap.set({ 'n', 'v' }, '<leader>ca', vim.lsp.buf.code_action, vim.tbl_extend('force', opts, { desc = 'Code action' }))
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, vim.tbl_extend('force', opts, { desc = 'Previous diagnostic' }))
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, vim.tbl_extend('force', opts, { desc = 'Next diagnostic' }))
vim.keymap.set('n', '<leader>d', vim.diagnostic.open_float, vim.tbl_extend('force', opts, { desc = 'Show diagnostic' }))
end,
})
-- Diagnostic UI improvements
vim.diagnostic.config({
virtual_text = true,
signs = true,
underline = true,
update_in_insert = false,
severity_sort = true,
float = {
border = 'rounded',
source = 'always',
},
})
-- Add borders to hover windows
local orig_util_open_floating_preview = vim.lsp.util.open_floating_preview
function vim.lsp.util.open_floating_preview(contents, syntax, opts, ...)
opts = opts or {}
opts.border = opts.border or 'rounded'
return orig_util_open_floating_preview(contents, syntax, opts, ...)
end
-- Setup lazydev for better plugin type support
require('lazydev').setup({
library = {
{ path = 'luvit-meta/library', words = { 'vim%.uv' } },
},
})
-- Setup language servers using vim.lsp.config
-- Lua
local runtime_path = vim.split(package.path, ';')
table.insert(runtime_path, 'lua/?.lua')
table.insert(runtime_path, 'lua/?/init.lua')
vim.lsp.config.lua_ls = {
cmd = { 'lua-language-server' },
root_markers = { '.luarc.json', '.luarc.jsonc', '.luacheckrc', '.stylua.toml', 'stylua.toml', 'selene.toml', 'selene.yml', '.git' },
settings = {
Lua = {
runtime = {
version = 'LuaJIT',
path = runtime_path,
},
diagnostics = {
globals = { 'vim' },
},
workspace = {
library = {
vim.env.VIMRUNTIME,
'${3rd}/luv/library',
vim.fn.stdpath('data') .. '/site/pack/deps/start',
},
checkThirdParty = false,
},
completion = {
callSnippet = 'Replace',
},
telemetry = { enable = false },
},
},
}
-- TypeScript/JavaScript
vim.lsp.config.ts_ls = {
cmd = { 'typescript-language-server', '--stdio' },
root_markers = { 'package.json', 'tsconfig.json', 'jsconfig.json', '.git' },
}
-- Python
vim.lsp.config.pyright = {
cmd = { 'pyright-langserver', '--stdio' },
root_markers = { 'pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt', 'Pipfile', '.git' },
}
-- Rust
vim.lsp.config.rust_analyzer = {
cmd = { 'rust-analyzer' },
root_markers = { 'Cargo.toml', 'rust-project.json', '.git' },
}
-- C/C++
vim.lsp.config.clangd = {
cmd = { 'clangd', '--background-index', '--clang-tidy', '--header-insertion=iwyu' },
root_markers = { 'compile_commands.json', 'compile_flags.txt', '.clangd', '.git' },
}
-- Enable language servers
vim.lsp.enable({ 'lua_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'clangd' })
end)
local map = function(mode, lhs, rhs, opts)
if type(opts) == 'string' then opts = { desc = opts } end
require('snacks').keymap.set(mode, lhs, rhs, opts)
end
map('n', '<leader>ex', function() require('fyler').toggle({ kind = 'split_left_most' }) end, 'Toggle file tree')
map('n', '<leader>fs', '<cmd>w<cr>', 'Save file')
map('n', '<leader>ff', function() require('emacs-find-file').open() end, 'Find file (emacs-style)')
map('n', '<leader>bb', function() require('snacks').picker.buffers() end, 'List buffers')
map('n', '<leader>bd', function() require('snacks').bufdelete() end, 'Buffer delete')
map('n', '<leader>.', function() require('snacks').scratch() end, 'Scratch buffer')
map('n', '<leader>sf', function() require('snacks').picker.files() end, 'Search file')
map('n', '<leader>sg', function() require('snacks').picker.grep() end, 'Search grep')
map('n', '<leader>sw', function() require('snacks').picker.grep_word() end, 'Search grep word')
map('n', '<leader>ls', function() require('snacks').picker.lsp_symbols() end, 'LSP Symbols')
map('n', '<leader>lS', function() require('snacks').picker.lsp_workspace_symbols() end, 'LSP Workspace Symbols')
map('n', '<leader>zz', function() require('snacks').zen() end, 'Toggle zen mode')
map('n', '<leader>zm', function() require('snacks').zen.zoom() end, 'Toggle Zoom')
map('n', '<leader>qq', function() require('quicker').open({ focus = true }) end, 'Open/focus quickfix')
map('n', '<leader>ql', function() require('quicker').toggle({ loclist = true }) end, 'Toggle loclist')
map('n', '<leader>cr', '<cmd>source $MYVIMRC<cr>', 'Reload config')
map('n', '<leader>ce', '<cmd>edit $MYVIMRC<cr>', 'Edit config')
map('n', '<leader>ch', '<cmd>checkhealth<cr>', 'Check health')
map('n', '<leader>cf', '<cmd>Format<cr>', 'Format buffer')
map('n', '<leader>gg', function() require('neogit').open() end, 'Open Neogit')
-- Claude Code keymaps
map('n', '<leader>ac', '<cmd>ClaudeCode<cr>', 'Toggle Claude')
map('n', '<leader>af', '<cmd>ClaudeCodeFocus<cr>', 'Focus Claude')
map('n', '<leader>ar', '<cmd>ClaudeCode --resume<cr>', 'Resume Claude')
map('n', '<leader>aC', '<cmd>ClaudeCode --continue<cr>', 'Continue Claude')
map('n', '<leader>am', '<cmd>ClaudeCodeSelectModel<cr>', 'Select Claude model')
map('n', '<leader>ab', '<cmd>ClaudeCodeAdd %<cr>', 'Add current buffer')
map('v', '<leader>as', '<cmd>ClaudeCodeSend<cr>', 'Send to Claude')
map('n', '<leader>aa', '<cmd>ClaudeCodeDiffAccept<cr>', 'Accept diff')
map('n', '<leader>ad', '<cmd>ClaudeCodeDiffDeny<cr>', 'Deny diff')
-- File tree specific keymap for Claude Code
vim.api.nvim_create_autocmd('FileType', {
pattern = { 'oil' },
callback = function()
vim.keymap.set('n', '<leader>as', '<cmd>ClaudeCodeTreeAdd<cr>', { buffer = true, desc = 'Add file to Claude' })
end,
})
map('n', '<leader>wh', '<C-w>h', 'Window left')
map('n', '<leader>wj', '<C-w>j', 'Window down')
map('n', '<leader>wk', '<C-w>k', 'Window up')
map('n', '<leader>wl', '<C-w>l', 'Window right')
map('n', '<leader>ws', '<cmd>split<cr>', 'Split window horizontally')
map('n', '<leader>wv', '<cmd>vsplit<cr>', 'Split window vertically')
map('n', '<leader>wq', '<C-w>q', 'Close window')
vim.api.nvim_create_autocmd('QuickFixCmdPost', {
callback = function()
vim.cmd('redraw!')
vim.schedule(function()
require('quicker').open({ focus = true })
end)
end,
})
vim.opt.shortmess:append('c')