Awesome
Portal.nvim
Theme: kanagawa
Introduction
Look at you, sailing through [neovim] majestically, like an eagle... piloting a blimp.
Portal is a plugin that aims to build upon and enhance existing location lists (e.g. jumplist, changelist, quickfix list, etc.) and their associated motions (e.g. <c-o>
and <c-i>
) by presenting jump locations to the user in the form of portals.
See the quickstart section to get started.
Features
- Labelled portals for immediate movement to a portal location
- Customizable filters and slots for well-known lists
- Composable multiple location lists can be used in a single search
- Extensible able to search any list with custom queries
Requirements
- Neovim >= 0.8
- Neovim >= 0.9 - optional, for floating window title
Quickstart
- Install Portal.nvim using your preferred package manager
- Add keybinds for opening portals, both forwards and backwards
vim.keymap.set("n", "<leader>o", "<cmd>Portal jumplist backward<cr>")
vim.keymap.set("n", "<leader>i", "<cmd>Portal jumplist forward<cr>")
Next steps
- Check out the default settings
- Explore the available builtin queries
- Tune your search results with a custom filter or slot list
- Try combining multiple queries using the Portal API
Installation
<details> <summary>lazy.nvim</summary>{
"cbochs/portal.nvim",
-- Optional dependencies
dependencies = {
"cbochs/grapple.nvim",
"ThePrimeagen/harpoon"
},
}
</details>
<details>
<summary>packer</summary>
use {
"cbochs/portal.nvim",
-- Optional dependencies
requires = {
"cbochs/grapple.nvim",
"ThePrimeagen/harpoon"
},
}
</details>
<details>
<summary>vim-plug</summary>
Plug "cbochs/portal.nvim"
" Optional dependencies
Plug "cbochs/grapple.nvim"
Plug "ThePrimeagen/harpoon"
</details>
Settings
The following are the default settings for Portal. Setup is not required, but settings may be overridden by passing them as table arguments to the portal#setup
function.
require("portal").setup({
---@type "debug" | "info" | "warn" | "error"
log_level = "warn",
---The base filter applied to every search.
---@type Portal.SearchPredicate | nil
filter = nil,
---The maximum number of results for any search.
---@type integer | nil
max_results = nil,
---The maximum number of items that can be searched.
---@type integer
lookback = 100,
---An ordered list of keys for labelling portals.
---Labels will be applied in order, or to match slotted results.
---@type string[]
labels = { "j", "k", "h", "l" },
---Select the first portal when there is only one result.
select_first = false,
---Keys used for exiting portal selection. Disable with [{key}] = false
---to `false`.
---@type table<string, boolean>
escape = {
["<esc>"] = true,
},
---The raw window options used for the portal window
window_options = {
relative = "cursor",
width = 80,
height = 3,
col = 2,
focusable = false,
border = "single",
noautocmd = true,
},
})
</details>
Usage
Builtin Queries
<details> <summary>Builtin Queries and Examples</summary>Builin queries have a standardized interface. Each builtin can be accessed via the Portal
command or lua API.
Overview: the tunnel
method provides the default entry point for using Portal for a location list; the tunnel_forward
and tunnel_backward
are convenience methods for easy keybinds; the search
method returns the results of a query; and the query
method builds a query for use in portal#tunnel
or portal#search
.
Command: :Portal {builtin} [direction]
API: require("portal.builtin").{builtin}
{builtin}.query(opts)
{builtin}.search(opts)
{builtin}.tunnel(opts)
{builtin}.tunnel_backward(opts)
{builtin}.tunnel_forward(opts)
opts?
: Portal.SearchOptions
changelist
Filter, match, and iterate over Neovim's :h changelist
.
Defaults
opts.start
: current change indexopts.direction
:"backward"
opts.max_results
:#settings.labels
Content
type
:"changelist"
buffer
:0
cursor
: the changelistlnum
andcol
extra.direction
: the search directionextra.distance
: the absolute distance between the start and current changelist entry:select()
: uses nativeg;
andg,
to preserve changelist ordering
-- Open a default search for the changelist
require("portal.builtin").changelist.tunnel()
</details>
grapple
Filter, match, and iterate over tagged files from grapple.
Defaults
opts.start
:1
opts.direction
:"forward"
opts.max_results
:#settings.labels
Content
type
:"grapple"
buffer
: the file tags'sbufnr
cursor
: the file tags'srow
andcol
extra.key
: the file tags's key:select()
: usesgrapple#select
-- Open a default search for grapples's tags
require("portal.builtin").grapple.tunnel()
</details>
harpoon
Filter, match, and iterate over marked files from harpoon.
Defaults
opts.start
:1
opts.direction
:"forward"
opts.max_results
:#settings.labels
Content
type
:"harpoon"
buffer
: the file mark'sbufnr
cursor
: the file mark'srow
andcol
extra.index
: the file mark's index:select()
: usesharpoon.ui#nav_file
-- Open a default search for harpoon's marks
require("portal.builtin").harpoon.tunnel()
</details>
jumplist
Filter, match, and iterate over Neovim's :h jumplist
.
Defaults
opts.start
: current jump indexopts.direction
:"backward"
opts.max_results
:#settings.labels
Content
type
:"jumplist"
buffer
: the jumplistbufnr
cursor
: the jumplistlnum
andcol
extra.direction
: the search directionextra.distance
: the absolute distance between the start and current jumplist entry:select()
: uses native<c-o>
and<c-i>
to preserve jumplist ordering
-- Open a default search for the jumplist
require("portal.builtin").jumplist.tunnel()
-- Open a queried search for the jumplist going backwards (<c-o>)
-- Query for two jumps:
-- 1. A jump that is in the same buffer as the current buffer
-- 2. A jump that is in a buffer that has been modified
require("portal.builtin").jumplist.tunnel_backward({
slots = {
function(value) return value.buffer == vim.fn.bufnr() end,
function(value) return vim.api.nvim_buf_get_option(value.buffer, "modified") end,
}
})
-- Open a filtered search for the jumplist going forwards (<c-i>)
-- Filters the results based on whether the buffer has been tagged
-- by grapple.nvim or not. Return a maximum of two results.
require("portal.builtin").jumplist.tunnel_forward({
max_results = 2,
filter = function(value)
return require("grapple").exists({ buffer = value.buffer })
end,
})
</details>
quickfix
Filter, match, and iterate over Neovim's :h quickfix
list.
Defaults
opts.start
:1
opts.direction
:"forward"
opts.max_results
:#settings.labels
Content
type
:"quickfix"
buffer
: the quickfixbufnr
cursor
: the quickfixlnum
andcol
:select()
: usesnvim_win_set_cursor
for selection
-- Open portals for the quickfix list (from the top)
require("portal.builtin").quickfix.tunnel()
</details>
</details>
Portal API
<details> <summary>Portal API and Examples</summary>portal#tunnel
Search, open, and select a portal from a given query.
API: require("portal").tunnel(queries, overrides)
queries
: Portal.Query[]
overrides
: Portal.Settings
-- Run a simple filtered search over the jumplist
local query = require("portal.builtin").jumplist.query()
require("portal").tunnel(query)
-- Search both the jumplist and quickfix list
require("portal").tunnel({
require("portal.builtin").jumplist.query({ max_results = 1 })
require("portal.builtin").quickfix.query({ max_results = 1 }),
})
</details>
portal#search
Complete a search for a given query and return the results
API: require("portal").search(queries)
queries
: Portal.Query[]
returns
: portal.content[]
-- Return the results of a query over the jumplist and quickfix list
local results = require("portal").search({
require("portal.builtin").jumplist.query()
require("portal.builtin").quickfix.query(),
})
-- Select the first location from the list of results
results[1]:select()
</details>
portal#portals
Create portals (windows) for a given set of search results. By default portals will not be open.
API: require("portal").portals(queries, overrides)
results
: Portal.Content[]
overrides
: Portal.Settings
returns
: portal.window[]
-- Return the results of a query over the jumplist and quickfix list
local query = require("portal.builtin").jumplist.query()
local results = require("portal").search(query)
local windows = require("portal").portals(results)
-- Open the portal windows
require("portal").open(windows)
-- Select the first location from the list of portal windows
windows[1]:select()
-- Close the portal windows
require("portal").close(windows)
</details>
portal#open
Open a given list of portal (windows). Preferred over a for-loop as it forces a UI redraw.
API: require("portal").open(windows)
results
: Portal.Window[]
portal#close
Close a given list of portal (windows). Preferred over a for-loop as it forces a UI redraw.
API: require("portal").close(windows)
results
: Portal.Window[]
Portals
A portal is a labelled floating window showing a snippet of some buffer. The label indicates a key that can be used to navigate directly to the buffer location. A portal may also contain additional information, such as the buffer's name or the result's index.
<img width="1043" alt="portal_screenshot" src="https://user-images.githubusercontent.com/2467016/222313082-8ae51576-5497-40e8-88d9-466ca504e22d.png">Search
To begin a search, a query (or list of queries) must be provided to portal. Each query will contain a filtered location list iterator and (optionally) one or more slots to match against.
Filters
During a search, a filter may be applied to remove any unwanted results from being displayed. More specifically, a filter is a predicate function which accepts some value and returns true
or false
, indicating whether that value should be kept or discarded.
-- Filter for results that are in the same buffer
require("portal.builtin").jumplist({
filter = function(v) return v.buffer == vim.fn.bufnr() end
})
-- Filter for results that are in a modified buffer
require("portal.builtin").quickfix({
filter = function(v) return vim.api.nvim_buf_get_option(v.buffer, "modified") end
})
-- Filter for buffers that have been tagged by grapple.nvim
require("portal.builtin").quickfix({
filter = function(v) return require("grapple").exists({ buffer = v.buffer }) end
})
-- Filter for results that are in some "root" directory
require("portal.builtin").jumplist({
filter = function(v)
local root_files = vim.fs.find({ ".git" }, { upward = true })
if #root_files > 0 then
local root_dir = vim.fs.dirname(root_files[1])
local file_path = vim.api.nvim_buf_get_name(v.buffer)
return string.find(file_path, root_dir, 1, true) ~= nil
end
return true
end
})
</details>
Slots
To search for an exact set of results, one or more slots may be provided to a query. Each slot will attempt to be matched with its exact order (and index) preserved.
<details> <summary><b>Examples</b></summary>-- Try to match one result where the buffer is different than the
-- current buffer
require("portal.builtin").jumplist({
slots = function(v) return v.buffer ~= vim.fn.bufnr() end
})
-- Try to match two results where the buffer is different than the
-- current buffer
require("portal.builtin").jumplist({
slots = {
function(v) return v.buffer ~= vim.fn.bufnr() end,
function(v) return v.buffer ~= vim.fn.bufnr() end,
}
})
</details>
Iterators
All searches are performed over an input location list. Portal uses declarative iterators to prepare (map
), refine (filter
), match (reduce
), and collect
list search results. Iterators can be used to create custom queries.
Iterable operations
Operations which return a lua-style iterator.
Iterator.next(index?: number)
Iterator.iter()
Chainable operations
Operations which return an iterator.
Iterator.start_at(n: integer)
Iterator.reverse()
Iterator.rrepeat(value: any)
Iterator.wrap()
Iterator.skip(n: integer)
Iterator.step_by(n: integer)
Iterator.take(n: integer)
Iterator.filter(f: fun(v: any): boolean)
Iterator.map(f: fun(v: any, i: any): any | nil
: filtersnil
values
Collect operations
Operations which return a collection (list or table) of values.
Iterator.collect(): T[]
Iterator.collect_table(): table
Iterator.reduce(reducer: fun(acc, val, i): any, initial_state: any)
Iterator.flatten()
local Iterator = require("portal.iterator")
-- Print all values in a list
local iter = Iterator:new({ 1, 2, 3})
for i, v in iter:iter() do
print(v)
end
-- Create the list { 7, 8, 9 }
Iterator:new({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })
:start_at(7)
:take(3)
:collect()
-- Create the list { 2, 4, 6, 8, 10 }
Iterator:new({ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 })
:filter(function(v) return v % 2 == 0 end)
:collect()
-- Create the table { a = 1, b = 2 }
Iterator:new({ "a", "b" })
:map(function(v, i) return { v, i } end)
:collect_table()
-- Create a filtered and mapped table { 4, 6 }
Iterator:new({ 1, 2, 3})
:filter(function(v) return v > 1 end)
:map(function(v) return v * 2 end)
:collect()
-- Create the same filtered and mapped table { 4, 6 }
Iterator:new({ 1, 2, 3 })
:map(function(v) if v > 1 then return v * 2 end end)
:collect()
-- Create the repeated list { 1, 1, 1 }
Iterator:rrepeat(1)
:take(3)
:collect()
</details>
Highlight Groups
A few highlight groups are available for customizing the look of Portal.
Group | Description | Default |
---|---|---|
PortalLabel | Portal label (extmark) | Search |
PoralTitle | Floating window title | FloatTitle |
PortalBorder | Floating window border | FloatBorder |
PortalNormal | Floating window background | NormalFloat |
Portal Types
<details open> <summary>Type Definitions</summary>Portal.SearchOptions
Options available for tuning a search query. See the builtins section for information regarding search option defaults.
Type: table
start
:integer
direction
:Portal.Direction
max_results
:integer
filter
:Portal.SearchPredicate
slots
:Portal.SearchPredicate[]
|nil
Portal.Direction
Used for indicating whether a search should be performed forwards or backwards.
Type: enum
"forward"
"backward"
Portal.SearchPredicate
A predicate where the argument provided is an instance of Portal.Content
.
Type: fun(c: Portal.Content): boolean
Portal.Query
Named tuple of (source, slots)
. Used as the input to portal#tunnel
. When no slots
are present, the source
iterator will be simply be collected and presented as the search results.
Type: table
source
:Portal.Iterator
slots
:Portal.SearchPredicate[]
|nil
Portal.Content
An object with the fields (type, buffer, cursor)
and a :select()
method used for opening and selecting a portal location. Extra data is available in the extra
field and can be used to aide in filtering, querying, and selecting a portal. See the builtins section for information on which additional fields are present.
Type: object
type
:string
buffer
:integer
cursor
:{ row: integer, col: integer }
extra
:table
:select()
Portal.Window
A wrapper object around some Portal.Content
.
Type: object
:select()
:open()
:close()
Portal.Predicate
Basic function type used for filtering and matching values produced from an iterator.
Type: fun(v: any): boolean
Portal.QueryGenerator
Generating function which transforms an input set of Portal.SearchOptions
into a proper Portal.Query
.
Type: fun(o: Portal.SearchOptions, s: Portal.Settings): Portal.Query