🍿 A collection of QoL plugins for Neovim
Find a file
Copilot 1ed737e465
fix(dashboard): recent_files section not displaying files without cwd parameter (#2284)
## Problem

The `recent_files` section in the dashboard was not displaying any files
when called without a `cwd` parameter, which is the default usage in
most dashboard examples and documentation.

```lua
-- This would show an empty section after the bug was introduced
{ section = "recent_files", limit = 8 }
```

## Root Cause

When `opts.cwd` was not provided, the code set `root = ""` (empty
string) and passed it to the oldfiles filter:

```lua
local root = opts.cwd and svim.fs.normalize(...) or ""
for file in M.oldfiles({ filter = { [root] = true } }) do
```

The filter logic in `M.oldfiles` checks if a file path starts with the
filter path **AND** if the character immediately after that prefix is a
directory separator (`/` or `\`). With an empty string as the filter
path:

1. `file:sub(1, 0) == ""` is always `true`
2. But `file:sub(1, 1):find("[/\\]")` is `false` for most files (unless
they start with `/` or `\`)
3. This results in `matches = false`, causing all files to be filtered
out

## Solution

Changed the logic to use `nil` instead of an empty string when no `cwd`
is specified:

```lua
local root = opts.cwd and svim.fs.normalize(opts.cwd == true and vim.fn.getcwd() or opts.cwd) or nil
-- Only filter by directory when root is specified. If nil, M.oldfiles will use default filters only (excludes stdpath data/cache/state).
local oldfiles_opts = root and { filter = { [root] = true } } or nil
local ret = {} ---@type snacks.dashboard.Section
for file in M.oldfiles(oldfiles_opts) do
```

When no filter is passed, `M.oldfiles()` uses only its default filters
(excluding stdpath data/cache/state directories), which is the intended
behavior for showing all recent files.

## Testing

All usage scenarios now work correctly:

| Scenario | Code | Behavior |
|----------|------|----------|
| No cwd | `{ section = "recent_files" }` |  Shows all recent files
(except stdpath) |
| Current dir | `{ section = "recent_files", cwd = true }` |  Shows
files in cwd |
| Specific dir | `{ section = "recent_files", cwd = "/path" }` |  Shows
files in specified path |

## Impact

This is a minimal, surgical fix (4 lines changed) that restores the
expected behavior documented in all dashboard examples without affecting
any other functionality.

Fixes the issue where users reported empty recent_files sections after
updating to the latest version.

<!-- START COPILOT CODING AGENT SUFFIX -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>bug: dashboard does not display "recent_files"
section</issue_title>
> <issue_description>### Did you check docs and existing issues?
> 
> - [x] I have read all the snacks.nvim docs
> - [x] I have updated the plugin to the latest version before
submitting this issue
> - [x] I have searched the existing issues of snacks.nvim
> - [x] I have searched the existing issues of plugins related to this
issue
> 
> ### Neovim version (nvim -v)
> 
> 0.11 
> 
> ### Operating system/version
> 
> arch linux
> 
> ### Describe the bug
> 
> after updating, snacks.dashboard stopped displaying the "recent_files"
section.
> Looking through the commit history, I found that commit `5c4365e` is
relevant to that section. Upon rolling the plugin back to one commit
prior to it (commit `a4de830`), the section displayed as normal.
> 
> ### Steps To Reproduce
> 
> update to latest commit
> 
> ### Expected Behavior
> 
> section "recent files" displayed as per the documentations
> 
> ### Repro
> 
> ```lua
> vim.env.LAZY_STDPATH = ".repro"
> load(vim.fn.system("curl -s
https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))()
> 
> require("lazy.minit").repro({
>   spec = {
>     { "folke/snacks.nvim", opts = {} },
>     -- add any other plugins here
>   },
> })
> ```</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>

Fixes folke/snacks.nvim#2283

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 Share your feedback on Copilot coding agent for the chance to win a
$200 gift card! Click
[here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start
the survey.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: folke <292349+folke@users.noreply.github.com>
2025-10-19 22:17:03 +02:00
.github ci: update labeler 2025-10-19 11:43:47 +02:00
doc chore(build): auto-generate docs 2025-10-19 05:39:04 +00:00
docs chore(build): auto-generate docs 2025-10-19 10:05:32 +00:00
lua fix(dashboard): recent_files section not displaying files without cwd parameter (#2284) 2025-10-19 22:17:03 +02:00
plugin feat: ensure Snacks global is available when not using setup 2024-11-04 16:39:24 +01:00
queries fix(image.queries): add image type 2025-03-01 07:55:36 +01:00
scripts ci: update test scripts 2025-10-08 21:26:25 +02:00
tests ci: update minit.lua 2025-10-08 21:33:23 +02:00
.editorconfig ci: integrated with ci scripts 2024-11-03 09:04:33 +01:00
.gitignore test(image): better latex test document (#1392) 2025-02-27 08:46:07 +01:00
CHANGELOG.md chore(main): release 2.23.0 (#1434) 2025-09-15 08:36:55 +02:00
LICENSE ci: integrated with ci scripts 2024-11-03 09:04:33 +01:00
README.md docs(mini): update URLs for nvim-mini related plugins (#2189) 2025-09-17 15:36:48 +02:00
selene.toml feat: initial commit 2024-11-02 23:24:33 +01:00
stylua.toml feat: initial commit 2024-11-02 23:24:33 +01:00
vim.yml ci: update test scripts 2025-10-08 21:26:25 +02:00

🍿 snacks.nvim

A collection of small QoL plugins for Neovim.

Features

Snack Description Setup
animate Efficient animations including over 45 easing functions (library)
bigfile Deal with big files ‼️
bufdelete Delete buffers without disrupting window layout
dashboard Beautiful declarative dashboards ‼️
debug Pretty inspect & backtraces for debugging
dim Focus on the active scope by dimming the rest
explorer A file explorer (picker in disguise) ‼️
git Git utilities
gitbrowse Open the current file, branch, commit, or repo in a browser (e.g. GitHub, GitLab, Bitbucket)
image Image viewer using Kitty Graphics Protocol, supported by kitty, wezterm and ghostty ‼️
indent Indent guides and scopes
input Better vim.ui.input ‼️
layout Window layouts
lazygit Open LazyGit in a float, auto-configure colorscheme and integration with Neovim
notifier Pretty vim.notify ‼️
notify Utility functions to work with Neovim's vim.notify
picker Picker for selecting items ‼️
profiler Neovim lua profiler
quickfile When doing nvim somefile.txt, it will render the file as quickly as possible, before loading your plugins. ‼️
rename LSP-integrated file renaming with support for plugins like neo-tree.nvim and mini.files.
scope Scope detection, text objects and jumping based on treesitter or indent ‼️
scratch Scratch buffers with a persistent file
scroll Smooth scrolling ‼️
statuscolumn Pretty status column ‼️
terminal Create and toggle floating/split terminals
toggle Toggle keymaps integrated with which-key icons / colors
util Utility functions for Snacks (library)
win Create and manage floating windows or splits
words Auto-show LSP references and quickly navigate between them ‼️
zen Zen mode • distraction-free coding

Requirements

📦 Installation

Install the plugin with your package manager:

lazy.nvim

Important

A couple of plugins require snacks.nvim to be set-up early. Setup creates some autocmds and does not load any plugins. Check the code to see what it does.

Caution

You need to explicitly pass options for a plugin or set enabled = true to enable it.

Tip

It's a good idea to run :checkhealth snacks to see if everything is set up correctly.

{
  "folke/snacks.nvim",
  priority = 1000,
  lazy = false,
  ---@type snacks.Config
  opts = {
    -- your configuration comes here
    -- or leave it empty to use the default settings
    -- refer to the configuration section below
    bigfile = { enabled = true },
    dashboard = { enabled = true },
    explorer = { enabled = true },
    indent = { enabled = true },
    input = { enabled = true },
    picker = { enabled = true },
    notifier = { enabled = true },
    quickfile = { enabled = true },
    scope = { enabled = true },
    scroll = { enabled = true },
    statuscolumn = { enabled = true },
    words = { enabled = true },
  },
}

For an in-depth setup of snacks.nvim with lazy.nvim, check the example below.

⚙️ Configuration

Please refer to the readme of each plugin for their specific configuration.

Default Options
---@class snacks.Config
---@field animate? snacks.animate.Config
---@field bigfile? snacks.bigfile.Config
---@field dashboard? snacks.dashboard.Config
---@field dim? snacks.dim.Config
---@field explorer? snacks.explorer.Config
---@field gitbrowse? snacks.gitbrowse.Config
---@field image? snacks.image.Config
---@field indent? snacks.indent.Config
---@field input? snacks.input.Config
---@field layout? snacks.layout.Config
---@field lazygit? snacks.lazygit.Config
---@field notifier? snacks.notifier.Config
---@field picker? snacks.picker.Config
---@field profiler? snacks.profiler.Config
---@field quickfile? snacks.quickfile.Config
---@field scope? snacks.scope.Config
---@field scratch? snacks.scratch.Config
---@field scroll? snacks.scroll.Config
---@field statuscolumn? snacks.statuscolumn.Config
---@field terminal? snacks.terminal.Config
---@field toggle? snacks.toggle.Config
---@field win? snacks.win.Config
---@field words? snacks.words.Config
---@field zen? snacks.zen.Config
---@field styles? table<string, snacks.win.Config>
---@field image? snacks.image.Config|{}
{
  image = {
    -- define these here, so that we don't need to load the image module
    formats = {
      "png",
      "jpg",
      "jpeg",
      "gif",
      "bmp",
      "webp",
      "tiff",
      "heic",
      "avif",
      "mp4",
      "mov",
      "avi",
      "mkv",
      "webm",
      "pdf",
    },
  },
}

Some plugins have examples in their documentation. You can include them in your config like this:

{
  dashboard = { example = "github" }
}

If you want to customize options for a plugin after they have been resolved, you can use the config function:

{
  gitbrowse = {
    config = function(opts, defaults)
      table.insert(opts.remote_patterns, { "my", "custom pattern" })
    end
  },
}

🚀 Usage

See the example below for how to configure snacks.nvim.

{
  "folke/snacks.nvim",
  priority = 1000,
  lazy = false,
  ---@type snacks.Config
  opts = {
    bigfile = { enabled = true },
    dashboard = { enabled = true },
    explorer = { enabled = true },
    indent = { enabled = true },
    input = { enabled = true },
    notifier = {
      enabled = true,
      timeout = 3000,
    },
    picker = { enabled = true },
    quickfile = { enabled = true },
    scope = { enabled = true },
    scroll = { enabled = true },
    statuscolumn = { enabled = true },
    words = { enabled = true },
    styles = {
      notification = {
        -- wo = { wrap = true } -- Wrap notifications
      }
    }
  },
  keys = {
    -- Top Pickers & Explorer
    { "<leader><space>", function() Snacks.picker.smart() end, desc = "Smart Find Files" },
    { "<leader>,", function() Snacks.picker.buffers() end, desc = "Buffers" },
    { "<leader>/", function() Snacks.picker.grep() end, desc = "Grep" },
    { "<leader>:", function() Snacks.picker.command_history() end, desc = "Command History" },
    { "<leader>n", function() Snacks.picker.notifications() end, desc = "Notification History" },
    { "<leader>e", function() Snacks.explorer() end, desc = "File Explorer" },
    -- find
    { "<leader>fb", function() Snacks.picker.buffers() end, desc = "Buffers" },
    { "<leader>fc", function() Snacks.picker.files({ cwd = vim.fn.stdpath("config") }) end, desc = "Find Config File" },
    { "<leader>ff", function() Snacks.picker.files() end, desc = "Find Files" },
    { "<leader>fg", function() Snacks.picker.git_files() end, desc = "Find Git Files" },
    { "<leader>fp", function() Snacks.picker.projects() end, desc = "Projects" },
    { "<leader>fr", function() Snacks.picker.recent() end, desc = "Recent" },
    -- git
    { "<leader>gb", function() Snacks.picker.git_branches() end, desc = "Git Branches" },
    { "<leader>gl", function() Snacks.picker.git_log() end, desc = "Git Log" },
    { "<leader>gL", function() Snacks.picker.git_log_line() end, desc = "Git Log Line" },
    { "<leader>gs", function() Snacks.picker.git_status() end, desc = "Git Status" },
    { "<leader>gS", function() Snacks.picker.git_stash() end, desc = "Git Stash" },
    { "<leader>gd", function() Snacks.picker.git_diff() end, desc = "Git Diff (Hunks)" },
    { "<leader>gf", function() Snacks.picker.git_log_file() end, desc = "Git Log File" },
    -- Grep
    { "<leader>sb", function() Snacks.picker.lines() end, desc = "Buffer Lines" },
    { "<leader>sB", function() Snacks.picker.grep_buffers() end, desc = "Grep Open Buffers" },
    { "<leader>sg", function() Snacks.picker.grep() end, desc = "Grep" },
    { "<leader>sw", function() Snacks.picker.grep_word() end, desc = "Visual selection or word", mode = { "n", "x" } },
    -- search
    { '<leader>s"', function() Snacks.picker.registers() end, desc = "Registers" },
    { '<leader>s/', function() Snacks.picker.search_history() end, desc = "Search History" },
    { "<leader>sa", function() Snacks.picker.autocmds() end, desc = "Autocmds" },
    { "<leader>sb", function() Snacks.picker.lines() end, desc = "Buffer Lines" },
    { "<leader>sc", function() Snacks.picker.command_history() end, desc = "Command History" },
    { "<leader>sC", function() Snacks.picker.commands() end, desc = "Commands" },
    { "<leader>sd", function() Snacks.picker.diagnostics() end, desc = "Diagnostics" },
    { "<leader>sD", function() Snacks.picker.diagnostics_buffer() end, desc = "Buffer Diagnostics" },
    { "<leader>sh", function() Snacks.picker.help() end, desc = "Help Pages" },
    { "<leader>sH", function() Snacks.picker.highlights() end, desc = "Highlights" },
    { "<leader>si", function() Snacks.picker.icons() end, desc = "Icons" },
    { "<leader>sj", function() Snacks.picker.jumps() end, desc = "Jumps" },
    { "<leader>sk", function() Snacks.picker.keymaps() end, desc = "Keymaps" },
    { "<leader>sl", function() Snacks.picker.loclist() end, desc = "Location List" },
    { "<leader>sm", function() Snacks.picker.marks() end, desc = "Marks" },
    { "<leader>sM", function() Snacks.picker.man() end, desc = "Man Pages" },
    { "<leader>sp", function() Snacks.picker.lazy() end, desc = "Search for Plugin Spec" },
    { "<leader>sq", function() Snacks.picker.qflist() end, desc = "Quickfix List" },
    { "<leader>sR", function() Snacks.picker.resume() end, desc = "Resume" },
    { "<leader>su", function() Snacks.picker.undo() end, desc = "Undo History" },
    { "<leader>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" },
    { "gr", 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" },
    { "<leader>ss", function() Snacks.picker.lsp_symbols() end, desc = "LSP Symbols" },
    { "<leader>sS", function() Snacks.picker.lsp_workspace_symbols() end, desc = "LSP Workspace Symbols" },
    -- Other
    { "<leader>z",  function() Snacks.zen() end, desc = "Toggle Zen Mode" },
    { "<leader>Z",  function() Snacks.zen.zoom() end, desc = "Toggle Zoom" },
    { "<leader>.",  function() Snacks.scratch() end, desc = "Toggle Scratch Buffer" },
    { "<leader>S",  function() Snacks.scratch.select() end, desc = "Select Scratch Buffer" },
    { "<leader>n",  function() Snacks.notifier.show_history() end, desc = "Notification History" },
    { "<leader>bd", function() Snacks.bufdelete() end, desc = "Delete Buffer" },
    { "<leader>cR", function() Snacks.rename.rename_file() end, desc = "Rename File" },
    { "<leader>gB", function() Snacks.gitbrowse() end, desc = "Git Browse", mode = { "n", "v" } },
    { "<leader>gg", function() Snacks.lazygit() end, desc = "Lazygit" },
    { "<leader>un", function() Snacks.notifier.hide() end, desc = "Dismiss All Notifications" },
    { "<c-/>",      function() Snacks.terminal() end, desc = "Toggle Terminal" },
    { "<c-_>",      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" } },
    {
      "<leader>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,
    }
  },
  init = function()
    vim.api.nvim_create_autocmd("User", {
      pattern = "VeryLazy",
      callback = function()
        -- Setup some globals for debugging (lazy-loaded)
        _G.dd = function(...)
          Snacks.debug.inspect(...)
        end
        _G.bt = function()
          Snacks.debug.backtrace()
        end

        -- Override print to use snacks for `:=` command
        if vim.fn.has("nvim-0.11") == 1 then
          vim._print = function(_, ...)
            dd(...)
          end
        else
          vim.print = _G.dd 
        end

        -- Create some toggle mappings
        Snacks.toggle.option("spell", { name = "Spelling" }):map("<leader>us")
        Snacks.toggle.option("wrap", { name = "Wrap" }):map("<leader>uw")
        Snacks.toggle.option("relativenumber", { name = "Relative Number" }):map("<leader>uL")
        Snacks.toggle.diagnostics():map("<leader>ud")
        Snacks.toggle.line_number():map("<leader>ul")
        Snacks.toggle.option("conceallevel", { off = 0, on = vim.o.conceallevel > 0 and vim.o.conceallevel or 2 }):map("<leader>uc")
        Snacks.toggle.treesitter():map("<leader>uT")
        Snacks.toggle.option("background", { off = "light", on = "dark", name = "Dark Background" }):map("<leader>ub")
        Snacks.toggle.inlay_hints():map("<leader>uh")
        Snacks.toggle.indent():map("<leader>ug")
        Snacks.toggle.dim():map("<leader>uD")
      end,
    })
  end,
}

🌈 Highlight Groups

Snacks defines a lot of highlight groups and it's impossible to document them all.

Instead, you can use the picker to see all the highlight groups.

Snacks.picker.highlights({pattern = "hl_group:^Snacks"})