Skip to content

A micro and highly composable finder for Neovim with no dependencies.

License

Notifications You must be signed in to change notification settings

danielefongo/microscope

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Microscope

ci code-coverage

A micro and highly composable finder for Neovim with no dependencies.

Base setup

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.

Finder

Finder opts

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.

Creation

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,
})

Binding

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.

Resuming

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.

Opts override

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,
  },
})

Lens spec

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:

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())

Default args

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.

Parsers

This is a list of functions, called parser functions, used to parse the data retrieved from the lens.

Parser function

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

Open function

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

Preview function

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.

Layout function

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

Full screen

The full_screen is represented as a boolean, defaults to false.

Size

The size is represented as a table containing width and height fields.

Override args

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.

Bindings

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,
}

Action

This function accepts a microscope finder instance to interact with it.

Example:

local close_action = function(microscope)
  microscope:close()
end

Prompt

The prompt is a string prefixed to the search query, defaults to "> ".

Spinner

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 = {
    ".   ",
    " .  ",
    "  . ",
    "   .",
    "  . ",
    " .  ",
  },
}

Builtins

Lenses

Microscope exposes a list of lens specs in microscope.builtin.lenses:

  • cache(...): caches results
  • fzf(...): filters results using fzf
  • head(...): limits results. Default args are: { limit = 5000 }
  • write(data): writes data directly into the flow
  • shell(command): executes a command (raw string). The command can contain pipes (e.g. echo x | grep "x")
  • fn(fun): executes a function

Actions

Microscope exposes a list of actions in microscope.builtin.actions:

  • previous: goes to the previous result
  • next: goes to the next result
  • scroll_down: scrolls down preview
  • scroll_up: scrolls up preview
  • toggle_full_screen: toggles full screen
  • open: opens selected results
  • select: selects result
  • set_layout(layout_fun): accepts a layout function and returns the corresponding action
  • set_args(arguments): accepts an args table and returns the corresponding action
  • alter(override_opts): accepts a table of options to override in the finder's instance
  • refine: starts a new search on retrieved results using a fuzzy lens
  • refine_with(lens, parser, prompt): starts a new search on retrieved results using a specific lens, parser and optional prompt
  • hide: hides the finder
  • close: closes the finder

Parsers

Microscope exposes a list of parsers in microscope.builtin.parsers:

  • fuzzy: highlights result

Layouts

Microscope exposes a list of layouts in microscope.builtin.layouts:

  • default

API

Lens

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()))

Lens function

The lens function has these parameters:

Flow

The flow is a bag of functions:

  • can_read: returns true if there is at least one input lens.
  • read: returns array_string or nil 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 over read.
  • read_array: returns an array of lines or nil if there is no more input data.
  • read_array_iter: returns an iterator over read_array.
  • write: accepts an array_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 command
  • collect: accepts a command and an optional to_array parameter, returning the output. If the second parameter is not provided, it will return an array_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.
Request

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 text
  • buf: the original bufnr
  • win: the original winnr
Arguments

The arguments table represents the merged combination of default args and override_args.

Context

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.

Command

The microscope.api.command module provides a utility for running commands inside flows.

Instantiation

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)
Chaining

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)

Scope

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 specification
  • callback: 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()

Display

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)

Microscope Finder

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 of get_opts and set_opts.

Input Window

The input window provides the following functions:

  • text(): returns the text.
  • set_text(text): sets the text.
  • reset(): resets the text.

Preview Window

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. The from and to parameters are optional.
  • write_term(lines): writes ANSI lines to the buffer.
  • read(from, to): reads lines from the buffer of the preview window.

Results 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. The from and to 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. The metadata parameter can be any data you want to pass to the open function.

Highlight

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.

Error

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

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:

Testing

  • 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

About

A micro and highly composable finder for Neovim with no dependencies.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages