--- 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() -- Open a snacks picker with trouble.nvim send-to-quickfix action bound to . local function with_trouble(source, opts) opts = opts or {} opts.actions = require("trouble.sources.snacks").actions opts.win = opts.win or {} opts.win.input = opts.win.input or {} opts.win.input.keys = vim.tbl_extend("force", opts.win.input.keys or {}, { [""] = { "trouble_open", mode = { "n", "i" } }, }) require("snacks").picker[source](opts) end -- Terminal title: " — ", derived from the buffer's git remote -- (works for any clone location, not just ghq paths). Cached per git root. local repo_label_cache = {} ---@type table local function repo_label(path) local root = vim.fs.root(path, ".git") if not root then return nil end local cached = repo_label_cache[root] if cached ~= nil then return cached or nil end local label = nil local gitdir = root .. "/.git" local stat = vim.uv.fs_stat(gitdir) if stat and stat.type == "file" then -- worktree/submodule: "gitdir: " local f = io.open(gitdir) if f then local line = f:read "*l" or "" f:close() local target = line:match "^gitdir:%s*(.+)$" if target then if not target:match "^/" then target = root .. "/" .. target end -- worktrees: /.git/worktrees/ → strip back to /.git gitdir = target:gsub("/worktrees/[^/]+$", "") end end end local f = io.open(gitdir .. "/config") if f then for line in f:lines() do local url = line:match "^%s*url%s*=%s*(%S+)" if url then label = url:match "[:/]([^/:]+/[^/]+)%.git$" or url:match "[:/]([^/:]+/[^/]+)$" if label then break end end end f:close() end repo_label_cache[root] = label or false return label end local function set_title(buf) if vim.bo[buf].buftype ~= "" then return end -- keep last title in pickers/terminals local filepath = vim.api.nvim_buf_get_name(buf) local filename = vim.fs.basename(filepath) local label = repo_label(filepath ~= "" and filepath or assert(vim.uv.cwd())) or vim.fs.basename(assert(vim.uv.cwd())) local title = (filename ~= "" and filename .. " — " or "") .. label vim.o.titlestring = title:gsub("%%", "%%%%") -- '%' is a statusline escape in titlestring end return { "AstroNvim/astrocore", ---@type AstroCoreOpts opts = { sessions = { autosave = { last = true, cwd = true, }, ignore = { dirs = {}, filetypes = { "gitcommit", "gitrebase" }, buftypes = {}, }, }, rooter = { detector = { "lsp", { ".git", "_darcs", ".hg", ".bzr", ".svn" }, { "package.json", "lua", "Cargo.toml", "Pipfile", "docs.json", }, }, ignore = { servers = { "golangci_lint_ls" }, dirs = {}, }, autochdir = true, scope = "global", 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" }, desc = "Set terminal title to ' — '", callback = function(args) set_title(args.buf) end, }, }, restore_session = { { event = "VimEnter", desc = "Restore previous directory session if neovim opened with no arguments", nested = true, -- trigger other autocommands as buffers open callback = function() if vim.fn.argc(-1) == 0 then require("resession").load( vim.fn.getcwd(), { dir = "dirsession", silence_errors = true } ) end end, }, }, }, options = { opt = { relativenumber = false, number = true, spell = false, wrap = true, clipboard = "unnamedplus", title = true, titlestring = "", laststatus = 3, -- global statusline (custom heirline in heirline.lua) }, g = {}, }, mappings = { n = { -- overrides AstroNvim's default = force quit (:q!) [""] = { "wqa", desc = "Save all buffers and quit Neovim" }, [""] = { "suspend", desc = "Suspend nvim (return with 'fg')" }, ["ZZ"] = { "wqa", desc = "Save all buffers and quit Neovim" }, [""] = { "wqa", desc = "Save all buffers and quit Neovim" }, [""] = { function() vim.cmd "wa" require("snacks").bufdelete() end, desc = "Save and close buffer", }, ["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"] = { "BufferLineCycleNext", desc = "Next buffer" }, ["H"] = { "BufferLineCyclePrev", desc = "Previous buffer" }, ["e"] = { function() require("snacks").picker.git_files { transform = file_filter } end, desc = "Find git files", }, [""] = { function() require("snacks").picker.smart() end, desc = "Find files", }, ["b"] = { "BufferLinePick", desc = "Pick buffer" }, ["f"] = { function() require("snacks").picker.buffers() end, desc = "Find buffers", }, [""] = { function() require("snacks").bufdelete.other() end, desc = "Close all buffers except the current one", }, ["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", }, ["l"] = { function() require("snacks").picker.lines() end, desc = "Find in line", }, ["o"] = { function() require("snacks").gitbrowse() end, desc = "Open Repo on Web" }, [""] = { "w", desc = "Save buffer" }, [""] = { function() vim.cmd "w" require("snacks").bufdelete() end, desc = "Save and close buffer", }, [""] = { "tabnext", desc = "Next Tab" }, [""] = { function() require("snacks").picker.gh_issue() end, desc = "GitHub issues (open)", }, [""] = { function() require("snacks").picker.gh_pr() end, desc = "GitHub PRs (open)", }, ["I"] = { function() require("snacks").picker.gh_issue { state = "all" } end, desc = "GitHub issues (all)", }, ["P"] = { function() require("snacks").picker.gh_pr { state = "all" } end, desc = "GitHub PRs (all)", }, -- which-key prefix group labels [""] = { desc = "Git + GitHub" }, [""] = { desc = "Find" }, [""] = { desc = "Buffers" }, [""] = { desc = "Assist" }, -- ghq project switcher (previously a dashboard action) ["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", }, [""] = { function() require("snacks").lazygit() end, desc = "Toggle lazygit (float)", }, [""] = { function() require("snacks").terminal.toggle("lazydocker", { win = { style = "float" } }) end, desc = "Toggle lazydocker (float)", }, [""] = { "Scooter", desc = "Find & replace (scooter)" }, [""] = { "!gh release list", desc = "List Releases" }, ["d"] = { function() require("snacks").picker.git_diff() end, desc = "Search git diffs", }, [""] = { 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" }, ["d"] = { function() require("snacks").picker.diagnostics() end, desc = "Find diagnostics", }, ["l"] = { function() require("snacks").picker.lines() end, desc = "Find in line", }, ["t"] = { function() require("snacks").picker.lines() end, desc = "Find in line", }, ["w"] = { function() require("snacks").picker.grep() end, desc = "Find word", }, ["f"] = { function() require("snacks").picker.files { transform = file_filter } end, desc = "Find files (cwd)", }, [""] = { function() require("snacks").picker.lsp_symbols { layout = { preset = "vscode", preview = "main" } } end, desc = "Find LSP symbols", }, ["o"] = { function() require("snacks").picker.recent() end, desc = "Find recent files", }, ["p"] = { function() require("snacks").picker.projects() end, desc = "Find projects", }, ["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 require("snacks").picker.files { cwd = ai_dir, transform = file_filter, } end, desc = "Find AI files", }, ["g"] = { desc = "Find by Group" }, ["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 require("snacks").picker.files { cwd = workflows_dir, transform = file_filter, } end, desc = "Find GitHub Workflow files", }, ["gd"] = { function() require("snacks").picker.files { cmd = "fd", args = { "devbox.json|process-compose.yaml|Dockerfile|docker-compose.yaml|compose.yaml" }, } end, desc = "Find Devbox/Docker files", }, ["gm"] = { function() require("snacks").picker.files { cmd = "fd", args = { "Makefile" } } end, desc = "Find Makefile files", }, ["gp"] = { function() require("snacks").picker.files { cmd = "fd", args = { "package.json" } } end, desc = "Find Package.json files", }, [""] = { function() if vim.b.toggle_number == 99 then vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("", 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", }, ["d"] = { function() with_trouble("diagnostics") end, desc = "Search diagnostics ( sends to Trouble)", }, ["a"] = { function() vim.lsp.buf.code_action() end, desc = "Show code actions", }, ["s"] = { function() with_trouble("lsp_symbols") end, desc = "Search LSP symbols ( sends to Trouble)", }, ["S"] = { function() with_trouble("lsp_workspace_symbols") end, desc = "Search LSP workspace symbols ( sends to Trouble)", }, [""] = { "SupermavenToggle", desc = "Toggle Supermaven" }, ["u"] = { desc = "UI Toggles" }, ["ud"] = { function() require("snacks").toggle.diagnostics():toggle() end, desc = "Toggle Diagnostics", }, ["un"] = { function() require("snacks").toggle.line_number():toggle() end, desc = "Toggle Line Numbers", }, ["ur"] = { function() require("snacks").toggle.option("relativenumber"):toggle() end, desc = "Toggle Relative Numbers", }, ["uw"] = { function() require("snacks").toggle.option("wrap"):toggle() end, desc = "Toggle Line Wrap", }, ["us"] = { function() require("snacks").toggle.option("spell"):toggle() end, desc = "Toggle Spelling", }, ["ui"] = { function() require("snacks").toggle.inlay_hints():toggle() end, desc = "Toggle Inlay Hints", }, ["ut"] = { function() require("snacks").toggle.treesitter():toggle() end, desc = "Toggle Treesitter", }, ["uc"] = { function() require("snacks").toggle.option("conceallevel", { off = 0, on = 2 }):toggle() end, desc = "Toggle Conceal", }, ["uI"] = { function() require("snacks").toggle.indent():toggle() end, desc = "Toggle Indent Guides", }, ["uz"] = { function() require("snacks").toggle.zen():toggle() end, desc = "Toggle Zen Mode", }, ["uZ"] = { function() require("snacks").toggle.zoom():toggle() end, desc = "Toggle Zoom", }, ["ua"] = { function() require("snacks").toggle.animate():toggle() end, desc = "Toggle Animations", }, ["uS"] = { function() require("snacks").toggle.scroll():toggle() end, desc = "Toggle Smooth Scroll", }, ["up"] = { function() require("snacks").toggle.profiler():toggle() end, desc = "Toggle Profiler", }, ["uh"] = { function() require("snacks").toggle.profiler_highlights():toggle() end, desc = "Toggle Profiler Highlights", }, ["ux"] = { function() require("snacks").toggle.dim():toggle() end, desc = "Toggle Dim Inactive", }, ["uf"] = { function() vim.g.disable_autoformat = not vim.g.disable_autoformat local status = vim.g.disable_autoformat and "disabled" or "enabled" vim.notify("Format on save " .. status, vim.log.levels.INFO) end, desc = "Toggle Format on Save", }, -- v6: virtual_lines is native in vim.diagnostic.config, lsp_lines plugin no longer needed ["ul"] = { function() local cfg = vim.diagnostic.config() or {} local enabled = not cfg.virtual_lines vim.diagnostic.config { virtual_lines = enabled, virtual_text = not enabled } end, desc = "Toggle Virtual Lines", }, }, i = { [""] = { "wa", desc = "Save buffer, and return to normal mode" }, [""] = { function() vim.cmd "stopinsert" vim.cmd "wa" require("snacks").bufdelete() end, desc = "Save, close buffer, and return to normal mode", }, [""] = { function() vim.cmd "stopinsert" vim.cmd "wa" require("snacks").bufdelete() end, desc = "Save, close buffer, and return to normal mode", }, }, v = { [""] = { function() vim.cmd "w" require("snacks").bufdelete() end, desc = "Save and close buffer", }, [""] = { function() vim.cmd "w" require("snacks").bufdelete() end, desc = "Save and close buffer", }, ["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 = { [""] = { function() vim.cmd 'normal! "+p' end, desc = "Paste from system clipboard" }, ["f"] = { function() require("snacks").picker.buffers() end, desc = "Find buffers", }, [""] = { function() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes([[]], true, false, true), "n", false) vim.cmd "close" end, desc = "Exit terminal and close window", }, }, }, }, }