nix/config/nvim/init.lua
2025-11-29 18:30:09 -06:00

578 lines
20 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'
add 'mbbill/undotree'
add 'cbochs/grapple.nvim'
add 'fang2hou/blink-copilot'
add 'aserowy/tmux.nvim'
add 'NMAC427/guess-indent.nvim'
add 'wakatime/vim-wakatime'
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('guess-indent').setup {}
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',
['D'] = 'dd',
-- 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 = {
---@diagnostic disable
finder = {
confirm_simple = true,
close_on_select = false,
watcher = {
enabled = true,
},
win = {
kind = 'replace',
kinds = {
split_left_most = {
width = '10%',
win_opts = {
winfixwidth = true,
},
},
},
},
},
---@diagnostic enable
},
}
require('find-file').setup {
max_matches = 8,
on_directory_enter = function(dir) require('oil').open(dir) end,
}
require('compile-mode').setup {}
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('grapple').setup {
scope = 'git_branch',
icons = false,
}
require('tmux').setup {}
end)
later(function()
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' },
nix = { 'nixfmt' },
},
}
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', 'snippets', 'buffer', 'copilot' },
providers = {
copilot = {
name = 'copilot',
module = 'blink-copilot',
async = true,
},
},
},
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++
'copilot',
},
automatic_installation = true,
}
-- Ensure formatters are installed
local mason_registry = require 'mason-registry'
local formatters = {
'stylua',
}
for _, formatter in ipairs(formatters) do
local package = mason_registry.get_package(formatter)
if not package:is_installed() then package:install() end
end
-- 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 (settings in .luarc.json, library paths handled by lazydev)
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' },
}
-- 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' },
}
-- Nix
vim.lsp.config.nixd = {
cmd = { 'nixd' },
root_markers = { 'flake.nix', 'default.nix', 'shell.nix', '.git' },
}
-- Enable language servers
vim.lsp.enable { 'lua_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'clangd', 'copilot', 'nixd' }
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('find-file').open() end, 'Find file')
map('n', '<leader>fr', function() require('snacks').picker.recent() end, 'Find recent file')
map('n', '<leader>:', function() require('snacks').picker.command_history() end, 'Command history')
-- map('n', '<leader>ex', function() require('snacks').explorer() end, 'Toggle file tree')
map('n', '<leader>bb', function() require('snacks').picker.buffers() end, 'List buffers')
map('n', '<leader><leader>', 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.git_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>sb', function() require('snacks').picker.lines() end, 'Buffer lines')
map('n', '<leader>sB', function() require('snacks').picker.lines() end, 'Grep open buffers')
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>cc', function() require('compile-mode').compile() end, 'Compile')
map('n', '<leader>cC', function() require('compile-mode').recompile() end, 'Recompile')
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')
-- git
map('n', '<leader>gg', function() require('neogit').open() end, 'Open Neogit')
map('n', '<leader>gb', function() require('snacks').picker.git_branches() end, 'Git Branches')
map('n', '<leader>gl', function() require('snacks').picker.git_log() end, 'Git Log')
map('n', '<leader>gL', function() require('snacks').picker.git_log_file() end, 'Git Log File')
map('n', '<leader>gs', function() require('snacks').picker.git_status() end, 'Git Status')
map('n', '<leader>gS', function() require('snacks').picker.git_stash() end, 'Git Stash')
-- gh
map('n', '<leader>gd', function() require('snacks').picker.git_diff() end, 'Git Diff (Hunks)')
map('n', '<leader>gi', function() require('snacks').picker.gh_issue() end, 'GitHub Issues (open)')
map('n', '<leader>gI', function() require('snacks').picker.gh_issue { state = 'all' } end, 'GitHub Issues (all)')
map('n', '<leader>gp', function() require('snacks').picker.gh_pr() end, 'GitHub Pull Requests (open)')
map('n', '<leader>gP', function() require('snacks').picker.gh_pr { state = 'all' } end, 'GitHub Pull Requests (all)')
map('n', '<leader>ut', '<cmd>UndotreeToggle<cr>', 'Toggle undotree')
-- Grapple keymaps (harpoon-style)
map('n', '<leader>ma', '<cmd>Grapple toggle<cr>', 'Grapple toggle tag')
map('n', '<leader>mm', '<cmd>Grapple toggle_tags<cr>', 'Grapple tags menu')
map('n', '<leader>m1', '<cmd>Grapple select index=1<cr>', 'Grapple select 1')
map('n', '<leader>m2', '<cmd>Grapple select index=2<cr>', 'Grapple select 2')
map('n', '<leader>m3', '<cmd>Grapple select index=3<cr>', 'Grapple select 3')
map('n', '<leader>m4', '<cmd>Grapple select index=4<cr>', 'Grapple select 4')
map('n', '<leader>mn', '<cmd>Grapple cycle_tags next<cr>', 'Grapple next tag')
map('n', '<leader>mp', '<cmd>Grapple cycle_tags prev<cr>', 'Grapple prev tag')
map('n', '<leader>mga', '<cmd>Grapple toggle scope=global<cr>', 'Grapple add global tag')
map('n', '<leader>mgm', '<cmd>Grapple toggle_tags scope=global<cr>', 'Grapple global tags menu')
-- 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')
-- Visual mode
map('v', '<C-h>', function() require('tmux').move_left() end, 'Move to left tmux pane')
map('v', '<C-l>', function() require('tmux').move_right() end, 'Move to right tmux pane')
map('v', '<C-j>', function() require('tmux').move_bottom() end, 'Move to bottom tmux pane')
map('v', '<C-k>', function() require('tmux').move_top() end, 'Move to top tmux pane')
-- Terminal mode
map('t', '<C-h>', function() require('tmux').move_left() end, 'Move to left tmux pane')
map('t', '<C-l>', function() require('tmux').move_right() end, 'Move to right tmux pane')
-- C-j is Shift+Enter in terminal mode, so we avoid mapping it
-- map('t', '<C-j>', function() require('tmux').move_bottom() end, 'Move to bottom tmux pane')
-- map('t', '<C-k>', function() require('tmux').move_top() end, 'Move to top tmux pane')
map('t', '<C-q>', '<C-\\><C-n>', 'Unfocus terminal and return to editor')
-- Auto commands
vim.api.nvim_create_autocmd('QuickFixCmdPost', {
callback = function()
vim.cmd 'redraw!'
vim.schedule(function() require('quicker').open { focus = true } end)
end,
})
vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter' }, {
command = "if mode() != 'c' | checktime | endif",
pattern = '*',
})