2026-05-29 18:35:10 -04:00
|
|
|
--- AstroCore configuration (AstroNvim v6 / Neovim 0.12)
|
|
|
|
|
--- Core settings, keymappings, and autocmds
|
|
|
|
|
--- @module plugins.astrocore
|
|
|
|
|
|
|
|
|
|
local ignore_patterns = require "config.ignore_patterns"
|
|
|
|
|
|
|
|
|
|
local file_filter = ignore_patterns.create_picker_filter()
|
|
|
|
|
|
2026-08-13 20:55:26 -04:00
|
|
|
-- <C-f>e: two-stage find-by-extension. Stage 1 scans the repo with fd
|
|
|
|
|
-- (respects .gitignore) and shows every file extension ranked by count;
|
2026-08-20 10:43:28 -04:00
|
|
|
-- stage 2 opens fff prefilled with a `*.<ext> ` glob constraint so the
|
|
|
|
|
-- rest of the query fuzzy-matches within that extension.
|
2026-08-13 20:55:26 -04:00
|
|
|
local function find_files_by_extension()
|
|
|
|
|
local snacks = require "snacks"
|
|
|
|
|
local root = snacks.git.get_root() or vim.uv.cwd()
|
|
|
|
|
local files = vim.fn.systemlist { "fd", "--type", "f", "--base-directory", root }
|
|
|
|
|
if vim.v.shell_error ~= 0 then
|
|
|
|
|
vim.notify("fd failed to list files in " .. root, vim.log.levels.ERROR)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local counts = {}
|
|
|
|
|
for _, path in ipairs(files) do
|
|
|
|
|
-- last dot of the basename; skips dotfiles (.gitignore) and bare names
|
|
|
|
|
local ext = path:match "[^/]%.([%w_%-]+)$"
|
|
|
|
|
if ext then counts[ext] = (counts[ext] or 0) + 1 end
|
|
|
|
|
end
|
|
|
|
|
local items = {}
|
|
|
|
|
for ext, count in pairs(counts) do
|
|
|
|
|
items[#items + 1] = { text = ext, ext = ext, count = count }
|
|
|
|
|
end
|
|
|
|
|
if #items == 0 then
|
|
|
|
|
vim.notify("No file extensions found in " .. root, vim.log.levels.WARN)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
table.sort(items, function(a, b)
|
|
|
|
|
if a.count ~= b.count then return a.count > b.count end
|
|
|
|
|
return a.ext < b.ext
|
|
|
|
|
end)
|
|
|
|
|
snacks.picker {
|
|
|
|
|
title = "File Extensions",
|
|
|
|
|
layout = { preset = "select" },
|
|
|
|
|
items = items,
|
|
|
|
|
format = function(item)
|
|
|
|
|
return {
|
|
|
|
|
{ ("%-12s"):format(item.ext), "SnacksPickerLabel" },
|
|
|
|
|
{ ("%d file%s"):format(item.count, item.count == 1 and "" or "s"), "SnacksPickerComment" },
|
|
|
|
|
}
|
|
|
|
|
end,
|
|
|
|
|
confirm = function(picker, item)
|
|
|
|
|
picker:close()
|
|
|
|
|
if not item then return end
|
2026-08-20 10:43:28 -04:00
|
|
|
require("fff").find_files { cwd = root, title = "*." .. item.ext .. " files", query = "*." .. item.ext .. " " }
|
2026-08-13 20:55:26 -04:00
|
|
|
end,
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
2026-09-21 15:14:41 -04:00
|
|
|
-- <C-f>x: every URL in the current buffer, in file order; confirm hands the
|
|
|
|
|
-- URL to vim.ui.open (xdg-open). Reuses astrocore's url_matcher so the
|
|
|
|
|
-- picker agrees with the highlighturl feature about what counts as a URL.
|
|
|
|
|
local function find_buffer_urls()
|
|
|
|
|
local buf = vim.api.nvim_get_current_buf()
|
|
|
|
|
local re = vim.regex(require("astrocore").url_matcher)
|
|
|
|
|
local items = {}
|
|
|
|
|
for lnum, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
|
|
|
|
local offset = 0
|
|
|
|
|
while offset < #line do
|
|
|
|
|
-- match_str returns 0-indexed byte offsets relative to the substring
|
|
|
|
|
local s, e = re:match_str(line:sub(offset + 1))
|
|
|
|
|
if not s then break end
|
|
|
|
|
local url = line:sub(offset + s + 1, offset + e)
|
|
|
|
|
items[#items + 1] = { text = url, url = url, buf = buf, pos = { lnum, offset + s }, lnum = lnum }
|
|
|
|
|
offset = offset + e
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
if #items == 0 then
|
|
|
|
|
vim.notify("No URLs found in buffer", vim.log.levels.WARN)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
require("snacks").picker {
|
|
|
|
|
title = "Buffer URLs",
|
|
|
|
|
items = items,
|
|
|
|
|
format = function(item)
|
|
|
|
|
return {
|
|
|
|
|
{ ("%4d "):format(item.lnum), "SnacksPickerRow" },
|
|
|
|
|
{ item.url, "SnacksPickerLink" },
|
|
|
|
|
}
|
|
|
|
|
end,
|
|
|
|
|
confirm = function(picker, item)
|
|
|
|
|
picker:close()
|
|
|
|
|
if not item then return end
|
|
|
|
|
vim.ui.open(item.url)
|
|
|
|
|
end,
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
-- <C-f>m: files under the git root whose mtime is today (local midnight
|
|
|
|
|
-- onward). fd's --changed-within accepts a YYYY-MM-DD date as "since".
|
|
|
|
|
local function find_files_modified_today()
|
|
|
|
|
local snacks = require "snacks"
|
|
|
|
|
snacks.picker.files {
|
|
|
|
|
title = "Modified today",
|
|
|
|
|
cwd = snacks.git.get_root() or vim.uv.cwd(),
|
|
|
|
|
cmd = "fd",
|
|
|
|
|
args = { "--changed-within", os.date "%Y-%m-%d" },
|
|
|
|
|
transform = file_filter,
|
|
|
|
|
}
|
|
|
|
|
end
|
|
|
|
|
|
2026-05-29 18:35:10 -04:00
|
|
|
return {
|
|
|
|
|
"AstroNvim/astrocore",
|
|
|
|
|
---@type AstroCoreOpts
|
|
|
|
|
opts = {
|
|
|
|
|
sessions = {
|
2026-08-15 15:15:22 -04:00
|
|
|
-- cwd autosave is handled by the git_branch_sessions autocmd below so
|
|
|
|
|
-- the session name carries the worktree/branch, see config.session
|
2026-05-29 18:35:10 -04:00
|
|
|
autosave = {
|
|
|
|
|
last = true,
|
2026-08-15 15:15:22 -04:00
|
|
|
cwd = false,
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
ignore = {
|
|
|
|
|
dirs = {},
|
|
|
|
|
filetypes = { "gitcommit", "gitrebase" },
|
|
|
|
|
buftypes = {},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
rooter = {
|
|
|
|
|
detector = {
|
|
|
|
|
"lsp",
|
|
|
|
|
{ ".git", "_darcs", ".hg", ".bzr", ".svn" },
|
|
|
|
|
{
|
|
|
|
|
"package.json",
|
|
|
|
|
"lua",
|
|
|
|
|
"Cargo.toml",
|
|
|
|
|
"Pipfile",
|
|
|
|
|
"docs.json",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
ignore = {
|
2026-08-13 16:14:14 -04:00
|
|
|
-- golangci_lint_ls roots at the nearest go.mod, not the repo
|
|
|
|
|
-- dev-tools attaches with an empty root_dir which rooter treats as "/"
|
|
|
|
|
servers = { "golangci_lint_ls", "dev-tools" },
|
2026-05-29 18:35:10 -04:00
|
|
|
dirs = {},
|
|
|
|
|
},
|
|
|
|
|
autochdir = true,
|
2026-06-21 13:25:29 -04:00
|
|
|
scope = "global",
|
2026-05-29 18:35:10 -04:00
|
|
|
notify = false,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
-- v6: features schema changed
|
|
|
|
|
-- diagnostics_mode (0-3) -> diagnostics = { virtual_text, virtual_lines }
|
|
|
|
|
-- large_buf and cmp are new keys
|
|
|
|
|
features = {
|
|
|
|
|
large_buf = { size = 1024 * 256, lines = 10000 },
|
|
|
|
|
autopairs = true,
|
|
|
|
|
cmp = true,
|
|
|
|
|
diagnostics = { virtual_text = false, virtual_lines = false },
|
|
|
|
|
highlighturl = true,
|
|
|
|
|
notifications = false,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
-- vim.diagnostic.config() values applied when diagnostics are toggled on
|
|
|
|
|
diagnostics = {
|
|
|
|
|
virtual_text = true,
|
|
|
|
|
underline = true,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
autocmds = {
|
|
|
|
|
custom_title = {
|
|
|
|
|
{
|
|
|
|
|
event = { "BufEnter", "DirChanged" },
|
2026-08-13 16:04:29 -04:00
|
|
|
desc = "Update terminal title with org/repo format",
|
|
|
|
|
callback = function()
|
|
|
|
|
local filepath = vim.fn.expand "%:p"
|
|
|
|
|
local filename = vim.fn.expand "%:t"
|
|
|
|
|
|
|
|
|
|
if filename == "" then
|
|
|
|
|
local cwd = vim.fn.getcwd()
|
|
|
|
|
local github_match = cwd:match "github%.com/([^/]+/[^/]+)"
|
|
|
|
|
if github_match then
|
|
|
|
|
vim.opt.titlestring = github_match
|
|
|
|
|
else
|
|
|
|
|
vim.opt.titlestring = "Nvim"
|
|
|
|
|
end
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
local github_match = filepath:match "github%.com/([^/]+/[^/]+)"
|
|
|
|
|
|
|
|
|
|
if github_match then
|
|
|
|
|
vim.opt.titlestring = string.format("%s | %s", filename, github_match)
|
|
|
|
|
else
|
|
|
|
|
vim.opt.titlestring = string.format("%s | Nvim", filename)
|
|
|
|
|
end
|
|
|
|
|
end,
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
},
|
2026-08-15 15:15:22 -04:00
|
|
|
git_branch_sessions = {
|
|
|
|
|
{
|
|
|
|
|
event = "VimLeavePre",
|
|
|
|
|
desc = "Save the worktree/branch directory session on close",
|
|
|
|
|
callback = vim.schedule_wrap(function()
|
|
|
|
|
if require("astrocore.buffer").is_valid_session() then
|
|
|
|
|
require("config.session").save { notify = false }
|
|
|
|
|
end
|
|
|
|
|
end),
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
{
|
|
|
|
|
event = "VimEnter",
|
2026-08-15 15:15:22 -04:00
|
|
|
desc = "Restore worktree/branch session, else open the git files picker",
|
2026-05-29 18:35:10 -04:00
|
|
|
nested = true, -- trigger other autocommands as buffers open
|
2026-08-15 15:43:32 -04:00
|
|
|
-- no-op when nvim was started with file arguments, see config.session
|
|
|
|
|
callback = function() require("config.session").restore() end,
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
options = {
|
|
|
|
|
opt = {
|
|
|
|
|
relativenumber = false,
|
|
|
|
|
number = true,
|
|
|
|
|
spell = false,
|
|
|
|
|
wrap = true,
|
|
|
|
|
clipboard = "unnamedplus",
|
|
|
|
|
title = true,
|
|
|
|
|
titlestring = "",
|
2026-08-20 15:26:43 -04:00
|
|
|
laststatus = 3, -- global statusline (custom heirline in heirline.lua)
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
g = {},
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
mappings = {
|
|
|
|
|
n = {
|
2026-07-16 12:41:43 -04:00
|
|
|
-- overrides AstroNvim's default <C-q> = force quit (:q!)
|
|
|
|
|
["<C-q>"] = { "<Cmd>wqa<CR>", desc = "Save all buffers and quit Neovim" },
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-!>"] = { "<Cmd>suspend<CR>", desc = "Suspend nvim (return with 'fg')" },
|
2026-07-11 18:46:49 -04:00
|
|
|
["ZZ"] = { "<Cmd>wqa<CR>", desc = "Save all buffers and quit Neovim" },
|
2026-07-05 11:15:53 -04:00
|
|
|
["<A-q>"] = { "<Cmd>wqa<CR>", desc = "Save all buffers and quit Neovim" },
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-c>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "wa"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save and close buffer",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["F"] = { "za", desc = "Toggle fold under cursor" },
|
|
|
|
|
-- v6: vim.diagnostic.goto_next/goto_prev deprecated in 0.11, use jump()
|
|
|
|
|
["J"] = {
|
|
|
|
|
function() vim.diagnostic.jump { count = 1, float = true } end,
|
|
|
|
|
desc = "Next diagnostic",
|
|
|
|
|
},
|
|
|
|
|
["K"] = {
|
|
|
|
|
function() vim.diagnostic.jump { count = -1, float = true } end,
|
|
|
|
|
desc = "Previous diagnostic",
|
|
|
|
|
},
|
|
|
|
|
["L"] = { "<Cmd>BufferLineCycleNext<CR>", desc = "Next buffer" },
|
|
|
|
|
["H"] = { "<Cmd>BufferLineCyclePrev<CR>", desc = "Previous buffer" },
|
2026-07-11 16:43:30 -04:00
|
|
|
["<Leader>e"] = {
|
2026-07-22 12:44:17 -04:00
|
|
|
function() require("snacks").picker.git_files { transform = file_filter } end,
|
|
|
|
|
desc = "Find git files",
|
2026-07-11 16:43:30 -04:00
|
|
|
},
|
2026-08-15 15:15:22 -04:00
|
|
|
-- AstroNvim's dirsession maps key off the bare cwd; use the
|
|
|
|
|
-- worktree/branch-aware name instead
|
|
|
|
|
["<Leader>SS"] = {
|
|
|
|
|
function() require("config.session").save() end,
|
|
|
|
|
desc = "Save this dirsession",
|
|
|
|
|
},
|
|
|
|
|
["<Leader>S."] = {
|
|
|
|
|
function() require("config.session").load() end,
|
|
|
|
|
desc = "Load current dirsession",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["<leader><leader>"] = {
|
|
|
|
|
function() require("snacks").picker.smart() end,
|
|
|
|
|
desc = "Find files",
|
|
|
|
|
},
|
|
|
|
|
["<C-b>b"] = { "<Cmd>BufferLinePick<CR>", desc = "Pick buffer" },
|
|
|
|
|
["<C-b>f"] = {
|
|
|
|
|
function() require("snacks").picker.buffers() end,
|
|
|
|
|
desc = "Find buffers",
|
|
|
|
|
},
|
|
|
|
|
["<C-b><C-c>"] = {
|
2026-08-20 15:26:43 -04:00
|
|
|
function() require("snacks").bufdelete.other() end,
|
2026-05-29 18:35:10 -04:00
|
|
|
desc = "Close all buffers except the current one",
|
|
|
|
|
},
|
|
|
|
|
["<C-b>g"] = {
|
|
|
|
|
function()
|
|
|
|
|
local groups = { "DEX", "DID", "DWN", "SVC", "Tests", "Actions", "Claude", "Docs", "Config" }
|
|
|
|
|
for _, group in ipairs(groups) do
|
|
|
|
|
vim.cmd("BufferLineGroupToggle " .. group)
|
|
|
|
|
end
|
|
|
|
|
end,
|
|
|
|
|
desc = "Toggle all buffer groups",
|
|
|
|
|
},
|
|
|
|
|
["<C-b>l"] = {
|
|
|
|
|
function() require("snacks").picker.lines() end,
|
|
|
|
|
desc = "Find in line",
|
|
|
|
|
},
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-g>o"] = { function() require("snacks").gitbrowse() end, desc = "Open Repo on Web" },
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-s>"] = { "<cmd>w<CR>", desc = "Save buffer" },
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-w>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "w"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save and close buffer",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-Tab>"] = { "<cmd>tabnext<CR>", desc = "Next Tab" },
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-g><C-i>"] = {
|
|
|
|
|
function() require("snacks").picker.gh_issue() end,
|
|
|
|
|
desc = "GitHub issues (open)",
|
|
|
|
|
},
|
|
|
|
|
["<C-g>I"] = {
|
|
|
|
|
function() require("snacks").picker.gh_issue { state = "all" } end,
|
|
|
|
|
desc = "GitHub issues (all)",
|
|
|
|
|
},
|
|
|
|
|
["<C-g><C-p>"] = {
|
|
|
|
|
function() require("snacks").picker.gh_pr() end,
|
|
|
|
|
desc = "GitHub PRs (open)",
|
|
|
|
|
},
|
|
|
|
|
["<C-g>P"] = {
|
|
|
|
|
function() require("snacks").picker.gh_pr { state = "all" } end,
|
|
|
|
|
desc = "GitHub PRs (all)",
|
|
|
|
|
},
|
|
|
|
|
["<C-g><C-g>"] = {
|
|
|
|
|
function() require("snacks").lazygit() end,
|
|
|
|
|
desc = "Toggle lazygit (float)",
|
|
|
|
|
},
|
|
|
|
|
["<C-d><C-d>"] = {
|
|
|
|
|
function() require("snacks").terminal.toggle("lazydocker", { win = { style = "float" } }) end,
|
|
|
|
|
desc = "Toggle lazydocker (float)",
|
|
|
|
|
},
|
2026-08-25 14:47:15 -04:00
|
|
|
-- grug-far.nvim: buffer-based find & replace (plugins/grug-far.lua).
|
|
|
|
|
-- `open` reads the visual selection itself, so the same callback
|
|
|
|
|
-- serves both modes.
|
|
|
|
|
["<C-r>"] = { function() require("grug-far").open() end, desc = "Find & replace (grug-far)" },
|
2026-08-20 15:26:43 -04:00
|
|
|
-- which-key prefix group labels
|
|
|
|
|
["<C-g>"] = { desc = "Git + GitHub" },
|
|
|
|
|
["<C-f>"] = { desc = "Find" },
|
|
|
|
|
["<C-b>"] = { desc = "Buffers" },
|
|
|
|
|
["<C-a>"] = { desc = "Assist" },
|
|
|
|
|
-- ghq project switcher
|
|
|
|
|
["<C-f>P"] = {
|
|
|
|
|
function()
|
|
|
|
|
local repos = vim.fn.systemlist "ghq list -p"
|
|
|
|
|
vim.ui.select(repos, { prompt = "Select repository" }, function(dir)
|
|
|
|
|
if not dir then return end
|
|
|
|
|
vim.cmd.cd(dir)
|
|
|
|
|
require("snacks").picker.files()
|
|
|
|
|
end)
|
|
|
|
|
end,
|
|
|
|
|
desc = "Find ghq projects",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-g><C-r>"] = { "<cmd>!gh release list<CR>", desc = "List Releases" },
|
|
|
|
|
["<C-g>d"] = {
|
|
|
|
|
function() require("snacks").picker.git_diff() end,
|
|
|
|
|
desc = "Search git diffs",
|
|
|
|
|
},
|
|
|
|
|
["<C-g><C-b>"] = {
|
|
|
|
|
function() require("snacks").picker.git_branches() end,
|
|
|
|
|
desc = "Search git branches",
|
|
|
|
|
},
|
|
|
|
|
["vv"] = { "gg0VG$", desc = "Select all contents in buffer" },
|
|
|
|
|
["T"] = { "gg", desc = "Go to top of file" },
|
|
|
|
|
["<C-f>d"] = {
|
|
|
|
|
function() require("snacks").picker.diagnostics() end,
|
|
|
|
|
desc = "Find diagnostics",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>l"] = {
|
|
|
|
|
function() require("snacks").picker.lines() end,
|
|
|
|
|
desc = "Find in line",
|
|
|
|
|
},
|
2026-08-20 10:43:28 -04:00
|
|
|
-- NOTE: <C-i> and <Tab> are the same key at the mapping level, so
|
|
|
|
|
-- this also shadows <Tab> (jumplist-forward) in normal mode
|
|
|
|
|
["<C-i>"] = {
|
2026-08-17 14:17:02 -04:00
|
|
|
function() require("config.templates").pick() end,
|
|
|
|
|
desc = "Find templates",
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
2026-09-21 15:14:41 -04:00
|
|
|
-- NOTE: <C-m> and <CR> are likewise the same key, so Enter in normal
|
|
|
|
|
-- mode runs overseer. Buffers with their own <CR> (quickfix, help,
|
|
|
|
|
-- neo-tree, overseer's task list) keep theirs via buffer-local maps.
|
|
|
|
|
["<C-m>"] = { "<Cmd>OverseerRun<CR>", desc = "Run overseer task" },
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-f>w"] = {
|
2026-08-20 10:43:28 -04:00
|
|
|
function() require("fff").live_grep() end,
|
|
|
|
|
desc = "Live grep",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>z"] = {
|
|
|
|
|
function() require("fff").live_grep { grep = { modes = { "fuzzy", "plain" } } } end,
|
|
|
|
|
desc = "Live fuzzy grep",
|
|
|
|
|
},
|
|
|
|
|
["<C-f><C-w>"] = {
|
|
|
|
|
function() require("fff").live_grep_under_cursor() end,
|
|
|
|
|
desc = "Grep word under cursor",
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
["<C-f>f"] = {
|
2026-08-20 10:43:28 -04:00
|
|
|
function() require("fff").find_files() end,
|
2026-07-22 12:45:50 -04:00
|
|
|
desc = "Find files (cwd)",
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
2026-08-20 10:43:28 -04:00
|
|
|
["<C-f>m"] = {
|
2026-09-21 15:14:41 -04:00
|
|
|
find_files_modified_today,
|
|
|
|
|
desc = "Find files modified today",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>x"] = {
|
|
|
|
|
find_buffer_urls,
|
|
|
|
|
desc = "Find URLs in buffer (open in browser)",
|
2026-08-20 10:43:28 -04:00
|
|
|
},
|
|
|
|
|
["<C-f>r"] = {
|
|
|
|
|
function() require("fff").resume() end,
|
|
|
|
|
desc = "Resume last find/grep",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>."] = {
|
|
|
|
|
function() require("fff").find_files_in_dir(vim.fn.expand "%:p:h") end,
|
|
|
|
|
desc = "Find files in buffer directory",
|
|
|
|
|
},
|
2026-08-13 20:55:26 -04:00
|
|
|
["<C-f>e"] = {
|
|
|
|
|
find_files_by_extension,
|
|
|
|
|
desc = "Find files by extension",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-f>o"] = {
|
|
|
|
|
function() require("snacks").picker.recent() end,
|
|
|
|
|
desc = "Find recent files",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>p"] = {
|
|
|
|
|
function() require("snacks").picker.projects() end,
|
|
|
|
|
desc = "Find projects",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>gc"] = {
|
|
|
|
|
function()
|
|
|
|
|
local ai_dir = vim.fn.getcwd() .. "/.ai"
|
|
|
|
|
if vim.fn.isdirectory(ai_dir) == 0 then
|
|
|
|
|
vim.notify("No .ai directory found in current project", vim.log.levels.WARN)
|
|
|
|
|
return
|
|
|
|
|
end
|
2026-08-20 10:43:28 -04:00
|
|
|
require("fff").find_files_in_dir(ai_dir)
|
2026-05-29 18:35:10 -04:00
|
|
|
end,
|
|
|
|
|
desc = "Find AI files",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>g"] = { desc = "Find by Group" },
|
|
|
|
|
["<C-f>gw"] = {
|
|
|
|
|
function()
|
|
|
|
|
local workflows_dir = vim.fn.getcwd() .. "/.github/workflows"
|
|
|
|
|
if vim.fn.isdirectory(workflows_dir) == 0 then
|
|
|
|
|
vim.notify("No .github/workflows directory found in current project", vim.log.levels.WARN)
|
|
|
|
|
return
|
|
|
|
|
end
|
2026-08-20 10:43:28 -04:00
|
|
|
require("fff").find_files_in_dir(workflows_dir)
|
2026-05-29 18:35:10 -04:00
|
|
|
end,
|
|
|
|
|
desc = "Find GitHub Workflow files",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>gd"] = {
|
|
|
|
|
function()
|
2026-08-20 10:43:28 -04:00
|
|
|
require("fff").find_files {
|
|
|
|
|
title = "Devbox/Docker files",
|
|
|
|
|
query = "{devbox.json,process-compose.yaml,Dockerfile,docker-compose.yaml,compose.yaml} ",
|
2026-05-29 18:35:10 -04:00
|
|
|
}
|
|
|
|
|
end,
|
|
|
|
|
desc = "Find Devbox/Docker files",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>gm"] = {
|
2026-08-20 10:43:28 -04:00
|
|
|
function() require("fff").find_files { title = "Makefiles", query = "*Makefile* " } end,
|
2026-05-29 18:35:10 -04:00
|
|
|
desc = "Find Makefile files",
|
|
|
|
|
},
|
|
|
|
|
["<C-f>gp"] = {
|
2026-08-20 10:43:28 -04:00
|
|
|
function() require("fff").find_files { title = "package.json files", query = "*package.json " } end,
|
2026-05-29 18:35:10 -04:00
|
|
|
desc = "Find Package.json files",
|
|
|
|
|
},
|
|
|
|
|
["<C-p>"] = {
|
|
|
|
|
function()
|
|
|
|
|
if vim.b.toggle_number == 99 then
|
|
|
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<C-p>", true, false, true), "t", false)
|
|
|
|
|
return
|
|
|
|
|
end
|
|
|
|
|
local content = vim.fn.getreg "+"
|
|
|
|
|
vim.api.nvim_paste(content, true, -1)
|
|
|
|
|
end,
|
|
|
|
|
desc = "Paste from system clipboard",
|
|
|
|
|
},
|
2026-08-25 14:47:15 -04:00
|
|
|
-- LSP-scoped assist binds (<C-a>a/s/S) are capability-gated in
|
|
|
|
|
-- plugins/astrolsp.lua; diagnostics exist without a server, so this
|
|
|
|
|
-- one stays global
|
2026-05-29 18:35:10 -04:00
|
|
|
["<C-a>d"] = {
|
2026-08-25 14:47:15 -04:00
|
|
|
function() require("config.picker").with_trouble "diagnostics" end,
|
2026-05-29 18:35:10 -04:00
|
|
|
desc = "Search diagnostics (<C-t> sends to Trouble)",
|
|
|
|
|
},
|
2026-09-21 16:32:53 -04:00
|
|
|
["<C-a><C-s>"] = { "<Cmd>SupermavenToggle<CR>", desc = "Toggle Supermaven" },
|
2026-05-29 18:35:10 -04:00
|
|
|
|
2026-08-25 14:47:15 -04:00
|
|
|
-- <Leader>u toggles are mapped through Snacks.toggle in
|
|
|
|
|
-- config.toggles (called from polish.lua) so which-key renders their
|
|
|
|
|
-- enabled/disabled state
|
2026-05-29 18:35:10 -04:00
|
|
|
["<leader>u"] = { desc = "UI Toggles" },
|
|
|
|
|
},
|
|
|
|
|
i = {
|
|
|
|
|
["<C-s>"] = { "<Cmd>wa<CR><Esc>", desc = "Save buffer, and return to normal mode" },
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-c>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "stopinsert"
|
|
|
|
|
vim.cmd "wa"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save, close buffer, and return to normal mode",
|
|
|
|
|
},
|
|
|
|
|
["<C-x>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "stopinsert"
|
|
|
|
|
vim.cmd "wa"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save, close buffer, and return to normal mode",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
},
|
|
|
|
|
v = {
|
2026-08-20 15:26:43 -04:00
|
|
|
["<C-c>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "w"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save and close buffer",
|
|
|
|
|
},
|
|
|
|
|
["<C-x>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd "w"
|
|
|
|
|
require("snacks").bufdelete()
|
|
|
|
|
end,
|
|
|
|
|
desc = "Save and close buffer",
|
|
|
|
|
},
|
2026-08-20 10:43:28 -04:00
|
|
|
["<C-f><C-w>"] = {
|
|
|
|
|
function() require("fff").live_grep_under_cursor() end,
|
|
|
|
|
desc = "Grep selection",
|
|
|
|
|
},
|
2026-08-25 14:47:15 -04:00
|
|
|
-- charwise (v) selection prefills the search input, linewise (V)
|
|
|
|
|
-- limits search & replace to the selected range (visualSelectionUsage
|
|
|
|
|
-- = "auto-detect" in plugins/grug-far.lua)
|
|
|
|
|
["<C-r>"] = {
|
|
|
|
|
function() require("grug-far").open() end,
|
|
|
|
|
desc = "Find & replace selection (grug-far)",
|
|
|
|
|
},
|
2026-05-29 18:35:10 -04:00
|
|
|
["D"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd 'normal! "vy'
|
|
|
|
|
local selection = vim.fn.getreg "v"
|
|
|
|
|
local escaped = vim.fn.escape(selection, [[/\.*$^~[]])
|
|
|
|
|
vim.cmd(string.format([[%%s/%s//ge]], escaped))
|
|
|
|
|
vim.notify(string.format("Deleted all occurrences of: %s", selection), vim.log.levels.INFO)
|
|
|
|
|
end,
|
|
|
|
|
desc = "Delete all occurrences of selected text",
|
|
|
|
|
},
|
|
|
|
|
["R"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.cmd 'normal! "vy'
|
|
|
|
|
local selection = vim.fn.getreg "v"
|
|
|
|
|
local escaped = vim.fn.escape(selection, [[/\.*$^~[]])
|
|
|
|
|
require("snacks").input({
|
|
|
|
|
prompt = string.format("Replace '%s' with:", selection),
|
|
|
|
|
default = "",
|
|
|
|
|
}, function(replacement)
|
|
|
|
|
if replacement then
|
|
|
|
|
local escaped_replacement = vim.fn.escape(replacement, [[/\&~]])
|
|
|
|
|
vim.cmd(string.format([[%%s/%s/%s/ge]], escaped, escaped_replacement))
|
|
|
|
|
vim.notify(string.format("Replaced '%s' with '%s'", selection, replacement), vim.log.levels.INFO)
|
|
|
|
|
end
|
|
|
|
|
end)
|
|
|
|
|
end,
|
|
|
|
|
desc = "Replace all occurrences of selected text",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
t = {
|
|
|
|
|
["<C-p>"] = { function() vim.cmd 'normal! "+p' end, desc = "Paste from system clipboard" },
|
|
|
|
|
["<C-b>f"] = {
|
|
|
|
|
function() require("snacks").picker.buffers() end,
|
|
|
|
|
desc = "Find buffers",
|
|
|
|
|
},
|
|
|
|
|
["<C-q>"] = {
|
|
|
|
|
function()
|
|
|
|
|
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes([[<C-\><C-n>]], true, false, true), "n", false)
|
|
|
|
|
vim.cmd "close"
|
|
|
|
|
end,
|
|
|
|
|
desc = "Exit terminal and close window",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|