A simple finder plugin for neovim.
Screen.Recording.2022-12-10.at.2.05.05.PM.mov
- Fuzzy filtering that's scoped to fields defined by a regex
- e.g.
^([^:]-):%d+:(.*)$
enables querying against the filename and the line seperately in the output of a grep command.
- e.g.
- fzf style query syntax
- e.g.
.c$ !^foo 'bar baz
- e.g.
- ANSI escape code parsing
- preserve color, bold, etc. of command output
- Easy to debug
- ~1k sloc, no coroutines, no dependencies
- Rich feature set
- no file previews, no icons
- Lots of built-in sources
- currently only
buffers
andoldfiles
are provided
- currently only
- Lots of configuration options
open()
opens a finder window populated based on the source
parameter, which can be an array of
strings (e.g. {'~/foo', '~/bar'}
). You can also pass a string command as source
to run a command
asynchronously (e.g. 'rg --vimgrep foo'
). The output of the command will be used to populate the
finder. In rare cases when you want more control over the argument breakdown of the command, you can
instead pass a function that returns the command name and an array of arguments (e.g. function() return 'rg', {'--vimgrep', 'foo'} end
).
Results in the finder window can be filtered using the same syntax as fzf. (Note: the OR operator is not yet supported).
Typing a query in an open_live()
window causes the command to be re-run. open_live()
has a
nearly identical API as open()
, except that source
cannot be an array of strings (it must be a
command). In the case of a string command, the current query will be implicitly added as the last
argument (e.g. 'rg --vimgrep -- '
). If source
is a function, the query will be passed in as a
parameter (e.g. function(query) return 'rg', {'--vimgrep', '--', query} end
).
Both open()
and open_live()
accept an optional config
argument. They are identical except that
scopes
is not supported by open_live()
. The passed in config
will be deep-merged with the
default config:
local default_config = {
-- Called when selecting an item to open. `action` is the name of the table
-- key corresponding to the key that was pressed. (eg. 'edit')
---@type fun(action: string, results: string[])
on_complete = function(action, results)
for i, result in ipairs(results) do
if i == #results then -- execute action
vim.cmd(action .. ' ' .. vim.fn.fnameescape(result))
else -- create a buffer
local buf = vim.fn.bufnr(result, true)
vim.bo[buf].buflisted = true
end
end
end,
-- Returns custom highlight ranges to highlight the result line.
---@type fun(result: string): {col_start: number, col_end: number, hl_group: string}[]?
get_highlights = nil,
-- Function to override the default matcher with. If `query` is contained in `str`, it
-- should return a list of positions and a score. Otherwise, it returns nil. The higher the
-- score, the better the match.
---@type fun(str: string, query: string): (number[],number)|nil
matcher = require('ufind.matcher.default'),
-- Lua pattern with capture groups that defines scopes that will be queried individually.
scopes = '^(.*)$',
-- Whether to parse ansi escape codes.
ansi = false,
-- Initial query to use when first opened.
initial_query = '',
-- Initial scope to use when first opened.
initial_scope = 1,
layout = {
---@type 'none'|'single'|'double'|'rounded'|'solid'|string[]
border = 'none',
height = 0.8,
width = 0.7,
input_on_top = true,
},
keymaps = {
quit = '<Esc>',
-- The name of an action can be anything, but it should be handled in `on_complete`.
actions = {
edit = '<CR>',
split = '<C-s>',
vsplit = '<C-v>',
tabedit = '<C-t>',
},
up = {'<C-k>', '<Up>'},
down = {'<C-j>', '<Down>'},
page_up = {'<C-b>', '<PageUp>'},
page_down = {'<C-f>', '<PageDown>'},
home = '<Home>',
['end'] = '<End>',
scroll_left = '<S-Left>',
scroll_right = '<S-Right>',
wheel_up = '<ScrollWheelUp>',
wheel_down = '<ScrollWheelDown>',
prev_scope = '<C-p>',
next_scope = '<C-n>',
toggle_select = '<Tab>',
toggle_select_all = '<C-a>',
},
}
The default config table is exposed for convenience. (useful to fall back to the default
on_complete
function)
For convenience, ufind also provides a few built-in sources.
A function that returns an array of buffer names. The buffers only include named, listed, existing buffers, and excludes the current buffer. (source code)
A function that returns an array of oldfiles. The list excludes oldfiles that are directories or are wildignored. (source code)
The matching algorithm used when filtering results can be overridden. There are two built-in matchers available:
This is the default matcher. It uses a fuzzy filtering algorithm that's tailored to results that are a file path. It ranks character matches in the path's basename higher.
This is a non-fuzzy matcher, useful for matching non-path results such as grep results. It isn't
quite an exact matcher, as it uses "smart" case-sensitivity (much like vim's 'smartcase'
).
local ufind = require'ufind'
-- Helper containing common configuration
local function cfg(t)
return vim.tbl_deep_extend('keep', t, {
layout = { border = 'single' },
keymaps = {
actions = {
vsplit = '<C-l>',
}
},
})
end
-- Example of finding vim buffers
vim.keymap.set('n', '<space>b', function()
ufind.open(require'ufind.source.buffers'(), cfg{})
end)
-- Example of finding vim oldfiles
vim.keymap.set('n', '<space>o', function()
ufind.open(require'ufind.source.oldfiles'(), cfg{})
end)
-- Example of finding a file within the current directory
vim.keymap.set('n', '<space>f', function()
ufind.open_live('fd --color=always --type=file --', cfg{ansi = true})
end)
-- Example of a `:Grep` command
vim.api.nvim_create_user_command('Grep', function(o)
ufind.open('rg --vimgrep --fixed-strings --color=ansi -- ' .. o.args, cfg{
ansi = true,
scopes = '^([^:]-):%d+:%d+:(.*)$',
matcher = require'ufind.matcher.exact',
on_complete = function(action, results)
local pat = '^([^:]-):(%d+):(%d+):(.*)$'
if #results == 1 then -- open a single result
local fname, linenr, colnr = results[1]:match(pat)
if fname then
-- edit the file at the appropriate line/column number
vim.cmd(('%s +%s %s | norm! %s|'):format(
action, linenr, vim.fn.fnameescape(fname), colnr))
end
else -- put selected results into a quickfix list
vim.fn.setqflist({}, ' ', {
items = vim.tbl_map(function(result)
local fname, linenr, colnr, line = result:match(pat)
return fname and {
filename = fname,
text = line,
lnum = linenr,
col = colnr,
} or {}
end, results),
})
vim.cmd(action .. '| copen | cc!')
end
end,
})
end, {nargs = '+'})
-- Example of a custom action
vim.keymap.set('n', '<space>b', function()
ufind.open(require'ufind.source.buffers'(), cfg{
keymaps = {
actions = {
delete_buffer = '<C-d>',
},
},
on_complete = function(action, results)
if action == 'delete_buffer' then -- `:bdelete` selected results
for _, result in ipairs(results) do
vim.cmd('bd ' .. vim.fn.fnameescape(result))
end
else -- fallback to default `on_complete`
require'ufind'.default_config.on_complete(action, results)
end
end,
})
end)
-- TODO: Example of customizing the display of results
MIT License