Intro

Recently, I hit a very specific issue. I wanted to edit a file that is reasonably put into .gitignore like .env. neovim’s great file explorer, Telescope, respects ignore files such as .gitignore and .ignore, when searching for files, which makes sense most of the time. I do not need to find a file in .git, a Python virtual environment folder like venv, or in node_modules, or at least not often enough that I could not go about it some other way. But files like .env or the .ignore file itself that I am ignoring from my git repository I absolutely must be seeing in my file picker.

So, I checked the Telescope documentation and code to find a solution. This is the journey and the result!

Hidden and ignored files

By default, require('telescope.builtin').find_files() finds all files that are not hidden (i.e. starting with a dot) or are in one of the usual ignore files (.gitignore and .ignore, maybe even more). The reason for this behavior is that Telescope uses ripgrep (and friends) internally, which are operating just like that. There are two optional parameters that can be used to control, which files are found:

  • hidden: obviously, it either shows (hidden=true) or hides (hidden=false) hidden files. This should by true by default, at least I think so, but it’s no big deal, simply call the file picker like this:
    1
    
    require('telescope.builtin').find_files(hidden=true)
    
  • no-ignore: this one either respects or disrespects ignore files, i.e., no-ignore=true shows me all files, because it tells the search tool to not care about ignore files.

Note: The parameter values are a bit couter-intutive, at least to me, but that is the issue with boolean flags in general. hidden=true means hidden files are shown, no_ignore=true means the ignore files are not used.

And this is where my problems begin. When I am developing on my own, I simply create an .ignore file, run the filepicker with hidden=true, no_ignore=false and can edit the files that I need, while dumping all other files into .ignore. However, when I am in a git repository with a well behaved .gitignore file, my file picker will not show the files listed in .gitignore, even though they are not listed in .ignore. Most of the time, this is not an issue, but sometimes it is and then it becomes tedious to type out :e .env instead of just running the file picker, which is deeply ingrained in my muscle memory.

The root cause is simply that Telescope has only one setting for ignore files. Based on this setting, the underlying search tool like ripgrep or find is then configured, when the file picker is started.

The default behavior

As is often the case, it helps to simply take a look at the source code. Here is the implementation of the Telescope file picker from nvim-telescope/telescope.nvim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
files.find_files = function(opts)
  -- some code before...
  local hidden = opts.hidden
  local no_ignore = opts.no_ignore

  -- some code in between...

  if command == "fd" or command == "fdfind" or command == "rg" then
    if hidden then
      find_command[#find_command + 1] = "--hidden"
    end
    if no_ignore then
      find_command[#find_command + 1] = "--no-ignore"
    end
    -- more code...
  end

  -- more code...
end

I picked out the parts that are dealing with the actual ignoring of files, the rest of the code is pretty straightforward, the length is mostly a result from handling the different search tools that can be used behind the scenes. For the parameter no_ignore, the flag --no-ignore is passed to the find tool. In the case of ripgrep there are actually several options to handle ignore files. From its man page:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
       --no-ignore
           Don’t respect ignore files (.gitignore, .ignore, etc.). This implies --no-ignore-dot, --no-ignore-exclude, --no-ignore-global,
           no-ignore-parent and --no-ignore-vcs.

           This does not imply --no-ignore-files, since --ignore-file is specified explicitly as a command line argument.

           When given only once, the -u flag is identical in behavior to --no-ignore and can be considered an alias. However, subsequent
           -u flags have additional effects; see --unrestricted.

           This flag can be disabled with the --ignore flag.

       --no-ignore-dot
           Don’t respect .ignore files.

           This does not affect whether ripgrep will ignore files and directories whose names begin with a dot. For that, see the
           -./--hidden flag.

           This flag can be disabled with the --ignore-dot flag.

       --no-ignore-exclude
           Don’t respect ignore files that are manually configured for the repository such as git’s .git/info/exclude.

           This flag can be disabled with the --ignore-exclude flag.

       --no-ignore-files
           When set, any --ignore-file flags, even ones that come after this flag, are ignored.

           This flag can be disabled with the --ignore-files flag.

       --no-ignore-global
           Don’t respect ignore files that come from "global" sources such as git’s core.excludesFile configuration option (which
           defaults to $HOME/.config/git/ignore).

           This flag can be disabled with the --ignore-global flag.

       --no-ignore-messages
           Suppresses all error messages related to parsing ignore files such as .ignore or .gitignore.

           This flag can be disabled with the --ignore-messages flag.

       --no-ignore-parent
           Don’t respect ignore files (.gitignore, .ignore, etc.) in parent directories.

           This flag can be disabled with the --ignore-parent flag.

       --no-ignore-vcs
           Don’t respect version control ignore files (.gitignore, etc.). This implies --no-ignore-parent for VCS files. Note that
           .ignore files will continue to be respected.

           This flag can be disabled with the --ignore-vcs flag.

There it is, we actually need --no-ignore-vcs but NOT --no-ignore-dot, but the global setting --no-ignore overrides this. So the solution is simple, pass in --no-ignore-vcs instead of --no-ignore. However, I do not want to make changes to the Telescope code directly, but luckily we can easily extend it with our own code.

The solution

I am defining my own file picker function that essentially just primes the find_command argument that the find_files method will use. From the find_files code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  local find_command = (function()
    if opts.find_command then
      if type(opts.find_command) == "function" then
        return opts.find_command(opts)
      end
      return opts.find_command
    elseif 1 == vim.fn.executable "rg" then
      return { "rg", "--files", "--color", "never" }
    elseif 1 == vim.fn.executable "fd" then
      return { "fd", "--type", "f", "--color", "never" }
    elseif 1 == vim.fn.executable "fdfind" then
      return { "fdfind", "--type", "f", "--color", "never" }
    elseif 1 == vim.fn.executable "find" and vim.fn.has "win32" == 0 then
      return { "find", ".", "-type", "f" }
    elseif 1 == vim.fn.executable "where" then
      return { "where", "/r", ".", "*" }
    end
  end)()

  if not find_command then
    utils.notify("builtin.find_files", {
      msg = "You need to install either find, fd, or rg",
      level = "ERROR",
    })
    return
  end

So, by passing in a function to the find_command parameter, we can easily set our own custom settings for the build tool that we want to use:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
local builtin = require('telescope.builtin')

local custom_find_files = function()
    local opts = {
        find_command = function()
            return {
                'rg',
                '--files',
                '--color=never',
                '--no-heading',
                '--with-filename',
                '--line-number',
                '--column',
                '--smart-case',
                '--no-ignore-vcs',
            }
        end,
        hidden = true,
    }
    builtin.find_files(opts)
end

In my case, I am using ripgrep and passing the newly discovered --no-ignore-vcs flag. The default behavior will honor the .ignore file, which is almost always the behavior that I want, so that is all there is to do. Finally, I am calling my new custom picker:

1
2
3
4
5
  -- before
  vim.keymap.set('n', '<leader>ff', '<cmd>Telescope find_files hidden=true<cr>')

  -- after
  vim.keymap.set('n', '<leader>ff', custom_find_files)

Conclusion

While the solution is very easy, I let myself be bothered by this for way too long. Actually solving it was done in a few minutes and now I am happy every time I bring up my file picker and see .env and .ignore in the list.

Hope this is useful to anyone else!