return { "folke/snacks.nvim", priority = 1000, lazy = false, opts = { -- your configuration comes here -- or leave it empty to use the default settings -- refer to the configuration section below bigfile = { enabled = true }, notifier = { enabled = true }, quickfile = { enabled = true }, statuscolumn = { enabled = true }, words = { enabled = true }, picker = {}, explorer = {}, }, keys = { { ".", function() Snacks.scratch() end, desc = "Toggle Scratch Buffer", }, { "S", function() Snacks.scratch.select() end, desc = "Select Scratch Buffer", }, { "n", function() Snacks.notifier.show_history() end, desc = "Notification History", }, { "gB", function() Snacks.gitbrowse() end, desc = "Git Browse", }, -- { -- "gb", -- function() -- Snacks.git.blame_line() -- end, -- desc = "Git Blame Line", -- }, -- -- Commented out LazyGit in favor of separated jj -- { -- "gf", -- function() -- Snacks.lazygit.log_file() -- end, -- desc = "Lazygit Current File History", -- }, -- { -- "lg", -- function() -- Snacks.lazygit() -- end, -- desc = "Lazygit", -- }, -- { -- "gl", -- function() -- Snacks.lazygit.log() -- end, -- desc = "Lazygit Log (cwd)", -- }, { "dn", function() Snacks.notifier.hide() end, desc = "Dismiss All Notifications", }, { "", function() Snacks.terminal() end, desc = "Toggle Terminal", }, { "", function() Snacks.terminal() end, desc = "which_key_ignore", }, { "]]", function() Snacks.words.jump(vim.v.count1) end, desc = "Next Reference", mode = { "n", "t" }, }, { "[[", function() Snacks.words.jump(-vim.v.count1) end, desc = "Prev Reference", mode = { "n", "t" }, }, { "N", desc = "Neovim News", function() Snacks.win({ file = vim.api.nvim_get_runtime_file("doc/news.txt", false)[1], width = 0.6, height = 0.6, wo = { spell = false, wrap = false, signcolumn = "yes", statuscolumn = " ", conceallevel = 3, }, }) end, }, -- Snacks Picker -- Top Pickers & Explorer { "", function() Snacks.picker.smart() end, desc = "Smart Find Files", }, { ",", function() Snacks.picker.buffers() end, desc = "Buffers", }, { "/", function() Snacks.picker.grep() end, desc = "Grep", }, { ":", function() Snacks.picker.command_history() end, desc = "Command History", }, { "n", function() Snacks.picker.notifications() end, desc = "Notification History", }, { "e", function() Snacks.explorer() end, desc = "File Explorer", }, -- find { "fb", function() Snacks.picker.buffers() end, desc = "Buffers", }, { "fc", function() Snacks.picker.files({ cwd = vim.fn.stdpath("config") }) end, desc = "Find Config File", }, { "ff", function() Snacks.picker.files() end, desc = "Find Files", }, { "fg", function() Snacks.picker.git_files() end, desc = "Find Git Files", }, { "fp", function() Snacks.picker.projects() end, desc = "Projects", }, { "fr", function() Snacks.picker.recent() end, desc = "Recent", }, -- git { "jc", function() local cwd = vim.fn.getcwd() -- Helper to run commands and capture both stdout and stderr local function run_cmd(cmd) local full_cmd = "cd " .. vim.fn.shellescape(cwd) .. " && " .. cmd .. " 2>&1" local handle = io.popen(full_cmd) local result = handle and handle:read("*a") or "" if handle then handle:close() end return result:gsub("%s+$", "") end -- Check if in a git repo local git_dir = run_cmd("git rev-parse --git-dir") if git_dir == "" or git_dir:match("^fatal") then vim.notify("Not in a git repository", vim.log.levels.WARN) return end -- Get the default branch local function branch_exists(branch) local result = run_cmd("git rev-parse --verify refs/remotes/origin/" .. branch) -- If branch exists, rev-parse returns a hash; if not, it returns fatal error return not result:match("^fatal") end local default_branch = nil if branch_exists("main") then default_branch = "main" elseif branch_exists("master") then default_branch = "master" end if not default_branch then vim.notify("No origin/main or origin/master found", vim.log.levels.WARN) return end -- Get files that differ from origin/main (includes committed + uncommitted changes) local result = run_cmd("jj diff --from " .. default_branch .. "@origin --to @ --summary | awk '{print $2}'") -- Combine results local all_files = {} local seen = {} for line in result:gmatch("[^\r\n]+") do if line ~= "" and not seen[line] then seen[line] = true table.insert(all_files, { text = line, file = line }) end end if #all_files == 0 then vim.notify("No modified files", vim.log.levels.INFO) return end Snacks.picker({ title = "Modified Files", items = all_files, layout = { preset = "default" }, confirm = function(picker, item) picker:close() if item and item.text then -- Strip [untracked] suffix if present local file = item.text:gsub(" %[untracked%]$", "") vim.cmd("edit " .. vim.fn.fnameescape(file)) end end, }) end, desc = "Git Modified Files (vs origin/main)", }, { "gb", function() Snacks.picker.git_branches() end, desc = "Git Branches", }, { "gl", function() Snacks.picker.git_log() end, desc = "Git Log", }, { "gL", function() Snacks.picker.git_log_line() end, desc = "Git Log Line", }, { "gs", function() Snacks.picker.git_status() end, desc = "Git Status", }, { "gS", function() Snacks.picker.git_stash() end, desc = "Git Stash", }, { "gd", function() Snacks.picker.git_diff() end, desc = "Git Diff (Hunks)", }, { "gf", function() Snacks.picker.git_log_file() end, desc = "Git Log File", }, -- gh { "gi", function() Snacks.picker.gh_issue() end, desc = "GitHub Issues (open)", }, { "gI", function() Snacks.picker.gh_issue({ state = "all" }) end, desc = "GitHub Issues (all)", }, { "gp", function() Snacks.picker.gh_pr() end, desc = "GitHub Pull Requests (open)", }, { "gP", function() Snacks.picker.gh_pr({ state = "all" }) end, desc = "GitHub Pull Requests (all)", }, -- Grep { "sb", function() Snacks.picker.lines() end, desc = "Buffer Lines", }, { "sB", function() Snacks.picker.grep_buffers() end, desc = "Grep Open Buffers", }, { "sg", function() Snacks.picker.grep() end, desc = "Grep", }, { "sw", function() Snacks.picker.grep_word() end, desc = "Visual selection or word", mode = { "n", "x" }, }, -- search { 's"', function() Snacks.picker.registers() end, desc = "Registers", }, { "s/", function() Snacks.picker.search_history() end, desc = "Search History", }, { "sa", function() Snacks.picker.autocmds() end, desc = "Autocmds", }, { "sb", function() Snacks.picker.lines() end, desc = "Buffer Lines", }, { "sc", function() Snacks.picker.command_history() end, desc = "Command History", }, { "sC", function() Snacks.picker.commands() end, desc = "Commands", }, { "sd", function() Snacks.picker.diagnostics() end, desc = "Diagnostics", }, { "sD", function() Snacks.picker.diagnostics_buffer() end, desc = "Buffer Diagnostics", }, { "sh", function() Snacks.picker.help() end, desc = "Help Pages", }, { "sH", function() Snacks.picker.highlights() end, desc = "Highlights", }, { "si", function() Snacks.picker.icons() end, desc = "Icons", }, { "sj", function() Snacks.picker.jumps() end, desc = "Jumps", }, { "sk", function() Snacks.picker.keymaps() end, desc = "Keymaps", }, { "sl", function() Snacks.picker.loclist() end, desc = "Location List", }, { "sm", function() Snacks.picker.marks() end, desc = "Marks", }, { "sM", function() Snacks.picker.man() end, desc = "Man Pages", }, { "sp", function() Snacks.picker.lazy() end, desc = "Search for Plugin Spec", }, { "sq", function() Snacks.picker.qflist() end, desc = "Quickfix List", }, { "sr", function() Snacks.picker.resume() end, desc = "Resume", }, { "su", function() Snacks.picker.undo() end, desc = "Undo History", }, { "uC", function() Snacks.picker.colorschemes() end, desc = "Colorschemes", }, -- LSP { "gd", function() Snacks.picker.lsp_definitions() end, desc = "Goto Definition", }, { "gD", function() Snacks.picker.lsp_declarations() end, desc = "Goto Declaration", }, { "gref", function() Snacks.picker.lsp_references() end, nowait = true, desc = "References", }, { "gI", function() Snacks.picker.lsp_implementations() end, desc = "Goto Implementation", }, { "gy", function() Snacks.picker.lsp_type_definitions() end, desc = "Goto T[y]pe Definition", }, { "gai", function() Snacks.picker.lsp_incoming_calls() end, desc = "C[a]lls Incoming", }, { "gao", function() Snacks.picker.lsp_outgoing_calls() end, desc = "C[a]lls Outgoing", }, { "ss", function() Snacks.picker.lsp_symbols() end, desc = "LSP Symbols", }, { "sS", function() Snacks.picker.lsp_workspace_symbols() end, desc = "LSP Workspace Symbols", }, }, }