A micro and highly composable finder for Neovim with no dependencies.
On lazy.nvim
{
"danielefongo/microscope",
dependencies = {
"danielefongo/microscope-files",
"danielefongo/microscope-buffers",
},
config = function()
local microscope = require("microscope")
local actions = require("microscope.builtin.actions")
local layouts = require("microscope.builtin.layouts")
local files = require("microscope-files")
local buffers = require("microscope-buffers")
microscope.setup({
prompt = ":> " -- optional (default is "> ")
size = {
width = 80,
height = 30,
},
layout = layouts.default,
bindings = {
["<c-j>"] = actions.next,
["<c-k>"] = actions.previous,
["<a-j>"] = actions.scroll_down,
["<a-k>"] = actions.scroll_up,
["<cr>"] = actions.open,
["<esc>"] = actions.close,
["<tab>"] = actions.select,
["<c-f>"] = actions.toggle_full_screen,
["<a-s>"] = actions.refine,
},
spinner = { ... } -- optional
args = { ... } -- optional
})
microscope.register(files.finders)
microscope.register(buffers.finders)
-- Bind using microscope instance
vim.keymap.set("n", "<leader>of", microscope.finders.file:bind())
vim.keymap.set("n", "<leader>oo", microscope.finders.old_file:bind())
-- Bind using microscope commands
vim.keymap.set("n", "<leader>of", ":Microscope file<cr>")
vim.keymap.set("n", "<leader>oo", ":Microscope old_file<cr>")
end,
}
You can create your own finder.
Each finder can be defined using the following options:
local opts = {
lens = lens_spec, -- required
parsers = list_of_parsers, -- optional
open = open_fn, -- optional
preview = preview_fn, -- optional
layout = layout_fn, -- optional
full_screen = full_screen, -- optional
size = custom_size, -- optional (overrides/extends the microscope size option)
args = override_args, -- optional (overrides the lens args)
bindings = custom_bindings, -- optional (overrides/extends the microscope bindings option)
prompt = prompt, -- optional (overrides/extends the microscope prompt option)
spinner = spinner, -- optional (overrides/extends the microscope spinner option)
}
These properties are explained below.
A finder can be created in one of two ways:
local microscope = require("microscope")
-- First way
local finder = microscope.finder(opts)
-- Second way (registers the finder as microscope.finders.name)
local finder = microscope.register({
name = opts,
})
After creating the finder, you can use it by binding it to a shortcut. There are three ways to achieve this:
-- First way
vim.keymap.set("n", "<leader>of", finder:bind(override_opts))
-- Second way
vim.keymap.set("n", "<leader>of", microscope.finders.name:bind(override_opts))
-- Third way
vim.keymap.set("n", "<leader>of", ":Microscope name override_opts<cr>")
Note: override_opts
is optional and may be a partial of finder opts (see next section). These are evaluated at invocation time, so they are the last to be evaluated.
After using the hide
builtin action, it is possible to resume the finder. There are different ways to achieve this:
-- First way
vim.keymap.set("n", "<leader>h", microscope.resume)
-- Second way
vim.keymap.set("n", "<leader>h", ":Microscope resume")
Note: If the finder is resumed from a different buffer or window, the data corresponding to the request will be replaced, and no new search will be triggered until the input is changed. In such cases, the updated request will be used for the subsequent search.
After creating the finder, you have the option to override its opts using the override()
method. Here's an example:
Example:
finder:override({
size = {
width = 30,
},
bindings = {
["<cr>"] = actions.nothing,
},
})
This element represents a specification for a lens, which is used to retrieve or filter data (e.g. list of files or buffers), based on the request. It is a table that contains:
- a lens function
- an optional list of input lens specification
- an optional table or default args
local lens = {
fun = function(flow, request, args, context)
-- logic
end,
inputs = { ... }, -- optional list of other lens specs
args = { ... }, -- optional table of args
}
Example:
local lenses = {}
function lenses.rg(cwd)
return {
fun = function(flow)
flow.consume(flow.cmd.shell("rg", { "--files" }, cwd))
end,
}
end
function lenses.fzf(...)
return {
fun = function(flow, request)
flow.consume(flow.cmd.iter(flow.read_iter()):pipe("fzf", { "-f", request.text }))
end,
inputs = { ... },
}
end
local my_lens = lenses.fzf(lenses.rg())
Each lens may have default arguments, which either extend or replace the ones received from the input lenses. Therefore, the final arguments consist of the combination of all the lenses args.
This is a list of functions, called parser functions, used to parse the data retrieved from the lens.
This function accepts result data and the original request, and transforms the data by adding extra information. The first parser will receive a data containing only the text
field. It is important for each parser function to propagate this field, even if unmodified, as it will be used to render the result. The additional information can include things like file
or any other custom data you wish to add. The final data table will be passed to the open function and the preview function.
There is another special data field called highlights
, which should be propagated unless you intend to remove it. This field represents a list of highlights to be applied to the result. For more details, please refer to the highlight section.
Example:
local parser_fn = function(data, _)
local elements = vim.split(data.text, ":", {})
data.highlights = build_highlights(data)
data.buffer = tonumber(elements[1])
return data
end
The open function is called when the open action is triggered. It takes a single result data obtained from the parsers, the request, and optional metadata (sent by results:open
).
Example:
local open_fn = function(data, request, metadata)
vim.cmd("e " .. data.file)
end
The preview function is called when a result is focused. It takes a single result data obtained from the parsers and the preview
microscope instance.
Example:
local preview_fn = function(data, window)
window:write({ data.text })
end
For more details about the preview window API, refer to the preview window section.
The layout function is responsible for defining the structure of the finder using the display API. It receives a table with the following fields:
finder_size
: represents the size provided in the microscope settings.ui_size
: represents the size of the entire UI.preview
: indicates whether the preview function has been set or not.full_screen
: indicates whether the full-screen option has been set or not.
Example:
local layout_fn = function(opts)
return display
.vertical({
display.horizontal({
display.results("40%"),
display.preview(),
}),
display.input(1),
})
:build(opts.finder_size)
end
The full_screen
is represented as a boolean, defaults to false
.
The size
is represented as a table containing width
and height
fields.
The args
is a table used to override the default lens args. Attempting to set arguments with types different from those of the defaults will trigger a critical microscope error.
The bindings
table is used to define shortcut bindings for microscope actions.
Example:
local actions = require("microscope.builtin.actions")
local bindings = {
["<c-j>"] = actions.next,
["<c-k>"] = actions.previous,
["<a-j>"] = actions.scroll_down,
["<a-k>"] = actions.scroll_up,
["<cr>"] = actions.open,
["<esc>"] = actions.close,
["<tab>"] = actions.select,
["<c-f>"] = actions.toggle_full_screen,
["<c-h>"] = actions.hide,
}
This function accepts a microscope finder instance to interact with it.
Example:
local close_action = function(microscope)
microscope:close()
end
The prompt
is a string prefixed to the search query, defaults to "> ".
The spinner
table represents the specification for the loading spinner, which replaces the results title while retrieving the results. Its structure is as follows:
local spinner = {
interval = 500,
delay = 300,
position = "center",
symbols = {
". ",
" . ",
" . ",
" .",
" . ",
" . ",
},
}
Microscope exposes a list of lens specs in microscope.builtin.lenses
:
cache(...)
: caches resultsfzf(...)
: filters results using fzfhead(...)
: limits results. Default args are:{ limit = 5000 }
write(data)
: writes data directly into the flowshell(command)
: executes a command (raw string). The command can contain pipes (e.g.echo x | grep "x"
)fn(fun)
: executes a function
Microscope exposes a list of actions in microscope.builtin.actions
:
previous
: goes to the previous resultnext
: goes to the next resultscroll_down
: scrolls down previewscroll_up
: scrolls up previewtoggle_full_screen
: toggles full screenopen
: opens selected resultsselect
: selects resultset_layout(layout_fun)
: accepts a layout function and returns the corresponding actionset_args(arguments)
: accepts an args table and returns the corresponding actionalter(override_opts)
: accepts a table of options to override in the finder's instancerefine
: starts a new search on retrieved results using a fuzzy lensrefine_with(lens, parser, prompt)
: starts a new search on retrieved results using a specific lens, parser and optional prompthide
: hides the finderclose
: closes the finder
Microscope exposes a list of parsers in microscope.builtin.parsers
:
fuzzy
: highlights result
Microscope exposes a list of layouts in microscope.builtin.layouts
:
default
A lens is used to retrieve or filter data (e.g., a list of files or buffers) depending on the request, and it can be piped into other lenses. It accepts a lens spec.
Example:
local lens = require("microscope.api.lens")
local function rg(cwd)
return {
fun = function(flow)
flow.consume(flow.cmd.shell("rg", { "--files" }, cwd))
end,
}
end
local function fzf(...)
return {
fun = function(flow, request)
flow.consume(flow.cmd.shell("fzf", { "-f", request.text }))
end,
inputs = { ... },
}
end
local my_lens = lens.new(fzf(rg()))
The lens function has these parameters:
The flow is a bag of functions:
can_read
: returns true if there is at least one input lens.read
: returnsarray_string
ornil
if there is no more input data.array_string
is a string representing a list of lines separated by a newline and terminating with a newline (e.g., "hello\nworld\n").read_iter
: returns an iterator overread
.read_array
: returns an array of lines ornil
if there is no more input data.read_array_iter
: returns an iterator overread_array
.write
: accepts anarray_string
or a list of lines and propagates the data (e.g., to the next lens).array_string
is a string representing a list of lines separated by a newline and terminating with a newline (e.g., "hello\nworld\n").stop
: stops the flow.stopped
: returns true if the flow is stopped (e.g., you close the finder before reaching the end of the lens function).cmd
: accessor for commandcollect
: accepts a command and an optionalto_array
parameter, returning the output. If the second parameter is not provided, it will return anarray_string
.array_string
is a string representing a list of lines separated by a newline and terminating with a newline (e.g., "hello\nworld\n").consume
: accepts a command and writes its output into the flow in a "streaming" manner.
The request is what the user provides using the search
function of scope. In the context of a finder, it is represented by a table containing the following fields:
text
: the searched textbuf
: the original bufnrwin
: the original winnr
The arguments table represents the merged combination of default args and override_args.
The context is a table that is shared across multiple requests. It can be used to cache results or to perform logic based on the previous request.
The microscope.api.command
module provides a utility for running commands inside flows.
It can be lazy-instantiated with different constructors:
-
shell
: runs a shell command. Args and cwd are optional.local mycmd = cmd.shell("echo", { "-n", "hello\nworld" }, cwd)
-
iter
: consumes an iterator function.local elements = { "hello\n", "world\n" } local iterator = function() return table.remove(elements, 1) end local mycmd = cmd.iter(iterator)
-
const
: stores a constant.local mycmd = cmd.const("hello\nworld\n") -- array version local mycmd = cmd.const({ "hello", "world" })
-
fn
: executes a Vim function passing varargs.This is required because one cannot execute Vim functions like
vim.api.nvim_buf_get_name
inside coroutines.-- Synthetic way local mycmd = cmd.fn(vim.api.nvim_buf_get_name, request.buf) -- Verbose way local mycmd = cmd.fn(function() return vim.api.nvim_buf_get_name(request.buf) end)
-
await
: executes a function passing varargs, awaiting for the callback resolution.This is required because one cannot execute Vim functions like
vim.api.nvim_buf_get_name
inside coroutines.local mycmd = cmd.await(function(resolve, ...) an_async_function(function(data) resolve(data) end) end)
Once instantiated, it can be chained with other commands:
-
pipe
: pipe to another shell command. Args and cwd are optional.local mycmd = cmd .shell("echo", { "-n", "hello\nworld" }, cwd) :pipe("grep", { "hello" })
-
filter
: filters using a lua function. The input is a string.local mycmd = cmd .shell("echo", { "-n", "hello\nworld" }, cwd) :filter(function(lines) return string.gsub(data, "hello", "hallo") end)
The microscope.api.scope
module provides a utility for working with lenses.
This module can be particularly useful when working with the preview function. You can use it to directly write the obtained lines into the preview window, providing a convenient way to display and interact with the data retrieved by the lens.
The new
function accepts an object with two fields:
lens
: a lens specificationcallback
: an optional callback function that is called at the end
Example:
local scope = require("microscope.api.scope")
local cat_scope = scope.new({
lens = {
fun = function(flow, any_request)
flow.cmd.shell("cat", { any_request.text })
end,
},
callback = function(lines, any_request)
do_something(lines)
end,
})
The search
function accepts a request, which will be passed through the lens flow, and optionally, override_args. If these arguments have different types than the defaults, it will raise a critical microscope error.
local override_args = { ... }
cat_scope:search({
text = "my_file",
}, override_args)
-- To stop before completion
cat_scope:stop()
This module provides assistance in building layouts. The functions available for creating displays are as follows:
input(size)
: defines the input display. The default size is 1.results(size)
: defines the results display. The default size is nil.preview(size)
: defines the preview display. The default size is nil.space(size)
: defines a fake display to create spacing between and around displays. The default size is nil.vertical(displays, size)
: represents a vertical display. The first parameter is a list of displays, and the second one is the size.horizontal(displays, size)
: represents a horizontal display. The first parameter is a list of displays, and the second one is the size.
The size can be specified in the following ways:
- An integer: represents the number of rows or columns.
- A percentage string (e.g., "40%"): represents the percentage of rows or columns.
nil
: indicates that the component will expand to occupy as much space as possible.
To construct the layout, you can use the build
function of the display instance by passing a size (e.g., finder size).
Example:
display
.vertical({
display.horizontal({
display.results("40%"),
display.space(4),
display.preview(),
}),
display.space("10%"),
display.input(1),
})
:build(finder_size)
The Microscope Finder instance exposes three components:
In addition, the finder provides the following functions:
close()
: closes the finder.get_opts()
: retrieves the finder's instance options.set_opts(opts)
: overrides the finder's instance options.This differs from opts override since it is only for the instance.
alter(lambda)
: overrides the finder's instance options. It accepts a lambda function with one parameter, which is a copy of the finder's instance options. This function should return the new options (opts
) that will be set. This function effectively combines the functionality ofget_opts
andset_opts
.
The input window provides the following functions:
text()
: returns the text.set_text(text)
: sets the text.reset()
: resets the text.
The preview window provides the following functions:
set_buf_hl(color, line, from, to)
: highlights the buffer.set_win_opt(key, value)
: sets a window option.get_win_opt(key)
: gets a window option.set_buf_opt(key, value)
: sets a buffer option.get_buf_opt(key)
: gets a buffer option.get_win()
: returns the window number of the preview window.get_buf()
: returns the buffer number of the preview window.set_cursor(cursor)
: sets the cursor position safely.get_cursor(cursor)
: retrieves the current cursor position.clear()
: clears the text in the preview window.write(lines, from, to)
: writes lines of text to the buffer of the preview window. Thefrom
andto
parameters are optional.write_term(lines)
: writes ANSI lines to the buffer.read(from, to)
: reads lines from the buffer of the preview window.
The results window provides the following functions:
set_buf_hl(color, line, from, to)
: highlights the buffer.set_win_opt(key, value)
: sets a window option.set_buf_opt(key, value)
: sets a buffer option.get_win()
: returns the window number of the results window.get_buf()
: returns the buffer number of the results window.set_cursor(cursor)
: sets the cursor position safely.get_cursor(cursor)
: retrieves the current cursor position.clear()
: clears the text in the results window.write(lines, from, to)
: writes lines of text to the buffer of the results window. Thefrom
andto
parameters are optional.read(from, to)
: reads lines from the buffer of the results window.raw_results()
: returns the list of retrieved and unparsed results.select()
: adds the focused result to the selected results.selected()
: obtains the list of selected results.open(metadata)
: opens the selected results. Themetadata
parameter can be any data you want to pass to theopen
function.
The microscope.api.highlight
module allows you to create highlights for a line.
Example:
local highlight = require("microscope.api.highlight")
local data = {
text = "1: buffer one",
highlights = {},
}
local highlights = highlight
.new(data.highlights, data.text)
:hl_match(highlight.color.color1, "(%d+:)(.*)", 1) -- highlight the first group with color1
:hl(highlight.color.color2, 3, 10) -- highlight from column 3 to 10 with color2
:get_highlights()
Another utility function is microscope.utils.highlight
. This function accepts a path
and a bufnr
and automatically highlights the buffer based on the inferred filetype.
The microscope.api.error
module allows you to display an error. It provides two useful functions:
generic(message)
: displays a generic error message.critical(message)
: displays an error message and closes the finder.
Plugins can expose finders, lens specs, actions, parsers, previews, and open functions. This allows you to use pre-packaged finders or easily create custom finders using the provided building blocks. Here's an example:
local microscope = require("microscope")
local lenses = require("microscope.builtin.lenses")
local parsers = require("microscope.builtin.parsers")
local files = require("microscope-files")
local function ls()
return {
fun = function(flow)
flow.consume(flow.cmd.shell("ls"))
end,
}
end
microscope.setup({ ... })
microscope.register(files.finders)
microscope.register({
ls = {
lens = lenses.fzf(ls()),
preview = files.preview.cat,
parsers = { files.parsers.file, parsers.fuzzy },
},
})
You can also explore the already published plugins:
-
Clone plenary.nvim in root folder.
-
Install luacov
luarocks install luacov luarocks install luacov-console luarocks install luacov-html luarocks install luacheck cargo install stylua
-
Run tests
# without coverage make test [test=path-to-file] # with coverage make testcov # with coverage + html report make testcov-html # lint + stylua checks make stylua lint