nix/config/nvim/init.lua

883 lines
31 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
vim.opt.termguicolors = true
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 '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'
add 'OXY2DEV/markview.nvim'
add { source = 'R-nvim/R.nvim', depends = { 'nvim-treesitter/nvim-treesitter' } }
add 'nvim-treesitter/nvim-treesitter'
add 'nvim-treesitter/nvim-treesitter-textobjects'
add { source = 'nvim-pack/nvim-spectre', depends = { 'nvim-lua/plenary.nvim' } }
-- color themes
add 'EdenEast/nightfox.nvim'
now(function()
require('mail-count').setup {
accounts = {
{ name = 'U', query = 'tag:unread AND path:uchicago/**' },
{ name = 'P', query = 'tag:unread AND path:personal/**' },
},
interval = 60000, -- 60 seconds
}
require('mini.statusline').setup {
use_icons = true,
content = {
active = function()
local mode, mode_hl = MiniStatusline.section_mode { trunc_width = 120 }
local git = MiniStatusline.section_git { trunc_width = 40 }
local diff = MiniStatusline.section_diff { trunc_width = 75 }
local diagnostics = MiniStatusline.section_diagnostics { trunc_width = 75 }
local lsp = MiniStatusline.section_lsp { trunc_width = 75 }
local filename = MiniStatusline.section_filename { trunc_width = 140 }
local fileinfo = MiniStatusline.section_fileinfo { trunc_width = 120 }
local location = MiniStatusline.section_location { trunc_width = 75 }
local search = MiniStatusline.section_searchcount { trunc_width = 75 }
local mail = require('mail-count').get()
return MiniStatusline.combine_groups {
{ hl = mode_hl, strings = { mode } },
{ hl = 'MiniStatuslineDevinfo', strings = { git, diff, diagnostics, lsp } },
'%<',
{ hl = 'MiniStatuslineFilename', strings = { filename } },
'%=',
{ hl = 'MiniStatuslineMail', strings = { mail } },
{ hl = 'MiniStatuslineFileinfo', strings = { fileinfo } },
{ hl = mode_hl, strings = { search, location } },
}
end,
},
}
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'
require('raytheme').setup {
transparent = false,
italic_comments = true,
bold_keywords = false,
}
vim.cmd.colorscheme 'noctis_azureus_ghostty'
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 {}
-- Ensure treesitter parsers are installed
require('nvim-treesitter.configs').setup {
ensure_installed = {
'r',
'rnoweb',
'lua',
'python',
'rust',
'c',
'cpp',
'javascript',
'typescript',
'tsx',
'json',
'yaml',
'html',
'css',
'markdown',
'markdown_inline',
'nix',
'bash',
'vim',
'vimdoc',
'query',
'diff',
'git_rebase',
'gitcommit',
},
auto_install = true,
highlight = { enable = true },
incremental_selection = {
enable = true,
keymaps = {
init_selection = 'vv',
node_incremental = '<Tab>',
scope_incremental = '<C-Tab>',
node_decremental = '<S-Tab>',
},
},
textobjects = {
select = {
enable = true,
lookahead = true,
keymaps = {
['af'] = { query = '@function.outer', desc = 'Select outer function' },
['if'] = { query = '@function.inner', desc = 'Select inner function' },
['ac'] = { query = '@class.outer', desc = 'Select outer class' },
['ic'] = { query = '@class.inner', desc = 'Select inner class' },
['aa'] = { query = '@parameter.outer', desc = 'Select outer argument' },
['ia'] = { query = '@parameter.inner', desc = 'Select inner argument' },
['ai'] = { query = '@conditional.outer', desc = 'Select outer conditional' },
['ii'] = { query = '@conditional.inner', desc = 'Select inner conditional' },
['al'] = { query = '@loop.outer', desc = 'Select outer loop' },
['il'] = { query = '@loop.inner', desc = 'Select inner loop' },
['ab'] = { query = '@block.outer', desc = 'Select outer block' },
['ib'] = { query = '@block.inner', desc = 'Select inner block' },
},
},
move = {
enable = true,
set_jumps = true,
goto_next_start = {
[']f'] = { query = '@function.outer', desc = 'Next function start' },
[']c'] = { query = '@class.outer', desc = 'Next class start' },
[']a'] = { query = '@parameter.inner', desc = 'Next argument' },
},
goto_next_end = {
[']F'] = { query = '@function.outer', desc = 'Next function end' },
[']C'] = { query = '@class.outer', desc = 'Next class end' },
},
goto_previous_start = {
['[f'] = { query = '@function.outer', desc = 'Previous function start' },
['[c'] = { query = '@class.outer', desc = 'Previous class start' },
['[a'] = { query = '@parameter.inner', desc = 'Previous argument' },
},
goto_previous_end = {
['[F'] = { query = '@function.outer', desc = 'Previous function end' },
['[C'] = { query = '@class.outer', desc = 'Previous class end' },
},
},
swap = {
enable = true,
swap_next = {
['<leader>a'] = { query = '@parameter.inner', desc = 'Swap with next argument' },
},
swap_previous = {
['<leader>A'] = { query = '@parameter.inner', desc = 'Swap with previous argument' },
},
},
},
}
require('r').setup {
hook = {
on_filetype = function()
vim.keymap.set('n', '<leader>rs', '<Plug>RStart', { buffer = true, desc = 'Start R' })
vim.keymap.set('n', '<leader>rq', '<Plug>RClose', { buffer = true, desc = 'Close R' })
vim.keymap.set('n', '<leader>rl', '<Plug>RSendLine', { buffer = true, desc = 'Send line to R' })
vim.keymap.set('v', '<leader>rs', '<Plug>RSendSelection', { buffer = true, desc = 'Send selection to R' })
vim.keymap.set('n', '<leader>rf', '<Plug>RSendFile', { buffer = true, desc = 'Send file to R' })
vim.keymap.set('n', '<leader>ro', '<Plug>RShowArgs', { buffer = true, desc = 'Show function args' })
vim.keymap.set('n', '<leader>rh', '<Plug>RHelp', { buffer = true, desc = 'R help' })
end,
},
}
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-y>'] = { '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 = {
trigger = {
show_on_trigger_character = true,
},
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('markview').setup {
markdown_inline = {
checkboxes = {
checked = {
text = '󰗠',
hl = 'MarkviewCheckboxChecked',
scope_hl = 'MarkviewCheckboxStriked',
},
},
},
}
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',
'marksman', -- Markdown + Zettelkasten
},
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' },
}
-- Markdown (marksman)
vim.lsp.config.marksman = {
cmd = { 'marksman', 'server' },
filetypes = { 'markdown', 'markdown.mdx' },
root_markers = { '.marksman.toml', '.git', '.editorconfig' },
}
-- R
vim.lsp.config.r_language_server = {
cmd = { 'R', '--slave', '-e', 'languageserver::run()' },
filetypes = { 'r', 'rmd' },
root_markers = { '.Rproj', 'DESCRIPTION', '.git' },
}
-- Enable language servers
vim.lsp.enable { 'lua_ls', 'ts_ls', 'pyright', 'rust_analyzer', 'clangd', 'copilot', 'nixd', 'marksman', 'r_language_server' }
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
-- SOPS encrypted files (.enc) - decrypt on read, encrypt on save
vim.api.nvim_create_autocmd('BufReadCmd', {
pattern = '*.enc',
callback = function(ev)
local file = vim.fn.expand '%:p'
-- Decrypt with sops
local result = vim.fn.systemlist('sops -d ' .. vim.fn.shellescape(file) .. ' 2>/dev/null')
if vim.v.shell_error ~= 0 then
vim.notify('SOPS decryption failed: ' .. table.concat(result, '\n'), vim.log.levels.ERROR)
return
end
-- Set buffer content
vim.api.nvim_buf_set_lines(ev.buf, 0, -1, false, result)
-- Detect filetype from content
local first_line = result[1] or ''
if first_line:match '^{' then
vim.bo[ev.buf].filetype = 'json'
vim.b[ev.buf].sops_input_type = 'json'
elseif first_line:match '^%-%-%-' or first_line:match '^%w+:' then
vim.bo[ev.buf].filetype = 'yaml'
vim.b[ev.buf].sops_input_type = 'yaml'
elseif first_line:match '^%[' then
vim.bo[ev.buf].filetype = 'dosini'
vim.b[ev.buf].sops_input_type = 'ini'
else
-- Plain text or unknown - use binary mode (sops wraps in {"data": "..."})
vim.bo[ev.buf].filetype = 'text'
vim.b[ev.buf].sops_input_type = 'binary'
end
-- Store original file path and mark as not modified
vim.b[ev.buf].sops_file = file
vim.bo[ev.buf].modified = false
vim.bo[ev.buf].buftype = 'acwrite' -- enable BufWriteCmd
end,
})
vim.api.nvim_create_autocmd('BufWriteCmd', {
pattern = '*.enc',
callback = function(ev)
local file = vim.b[ev.buf].sops_file or vim.fn.expand '%:p'
local tmpfile = vim.fn.tempname()
local input_type = vim.b[ev.buf].sops_input_type or 'binary'
-- Find .sops.yaml config by walking up from the file's directory
local function find_sops_config(start_dir)
local dir = start_dir
while dir ~= '/' do
local config = dir .. '/.sops.yaml'
if vim.fn.filereadable(config) == 1 then return config end
dir = vim.fn.fnamemodify(dir, ':h')
end
return nil
end
local sops_config = find_sops_config(vim.fn.fnamemodify(file, ':h'))
-- Write current buffer to temp file
vim.cmd('silent write! ' .. tmpfile)
-- Encrypt with sops (use config if found, and override filename for path matching)
local config_arg = sops_config and ('SOPS_CONFIG=' .. vim.fn.shellescape(sops_config) .. ' ') or ''
local cmd = string.format(
'%ssops -e --input-type %s --output-type json --filename-override %s %s > %s',
config_arg,
input_type,
vim.fn.shellescape(file),
vim.fn.shellescape(tmpfile),
vim.fn.shellescape(file)
)
local result = vim.fn.system(cmd)
vim.fn.delete(tmpfile)
if vim.v.shell_error ~= 0 then
vim.notify('SOPS encryption failed: ' .. result, vim.log.levels.ERROR)
return
end
vim.bo[ev.buf].modified = false
vim.notify('Saved: ' .. file, vim.log.levels.INFO)
end,
})
-- Command to manually open a file with sops (for non-.enc files)
vim.api.nvim_create_user_command('Sops', function(opts)
local file = opts.args ~= '' and opts.args or vim.fn.expand '%:p'
vim.cmd('terminal sops ' .. vim.fn.shellescape(file))
vim.cmd 'startinsert'
end, { nargs = '?', complete = 'file', desc = 'Edit file with sops' })
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 = '*',
})
-- Restore gitrebase keybindings (overridden by clipboard=unnamedplus)
vim.api.nvim_create_autocmd('FileType', {
pattern = 'gitrebase',
callback = function()
local opts = { buffer = true, silent = true }
vim.keymap.set('n', 'p', '<cmd>Pick<cr>', opts)
vim.keymap.set('n', 'r', '<cmd>Reword<cr>', opts)
vim.keymap.set('n', 'e', '<cmd>Edit<cr>', opts)
vim.keymap.set('n', 's', '<cmd>Squash<cr>', opts)
vim.keymap.set('n', 'f', '<cmd>Fixup<cr>', opts)
vim.keymap.set('n', 'd', '<cmd>Drop<cr>', opts)
vim.keymap.set('n', 'x', '<cmd>Exec<cr>', opts)
vim.keymap.set('n', '<C-n>', '<cmd>move +1<cr>', opts)
vim.keymap.set('n', '<C-p>', '<cmd>move -2<cr>', opts)
vim.keymap.set('n', '<C-j>', '<cmd>move +1<cr>', opts)
vim.keymap.set('n', '<C-k>', '<cmd>move -2<cr>', opts)
end,
})