
Theme: kanagawa
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.
vim.keymap.set("n", "<leader>o", "<cmd>Portal jumplist backward<cr>")
vim.keymap.set("n", "<leader>i", "<cmd>Portal jumplist forward<cr>")
Next steps
{
"cbochs/portal.nvim",
-- Optional dependencies
dependencies = {
"cbochs/grapple.nvim",
"ThePrimeagen/harpoon"
},
}
use {
"cbochs/portal.nvim",
-- Optional dependencies
requires = {
"cbochs/grapple.nvim",
"ThePrimeagen/harpoon"
},
}
Plug "cbochs/portal.nvim"
" Optional dependencies
Plug "cbochs/grapple.nvim"
Plug "ThePrimeagen/harpoon"
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,
},
})
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
changelistFilter, match, and iterate over Neovim's :h changelist.
Defaults
opts.start: current change indexopts.direction: "backward"opts.max_results: #settings.labelsContent
type: "changelist"buffer: 0cursor: the changelist lnum and colextra.direction: the search directionextra.distance: the absolute distance between the start and current changelist entry:select(): uses native g; and g, to preserve changelist ordering-- Open a default search for the changelist
require("portal.builtin").changelist.tunnel()
grappleFilter, match, and iterate over tagged files from grapple.
Defaults
opts.start: 1opts.direction: "forward"opts.max_results: #settings.labelsContent
type: "grapple"buffer: the file tags's bufnrcursor: the file tags's row and colextra.key: the file tags's key:select(): uses grapple#select-- Open a default search for grapples's tags
require("portal.builtin").grapple.tunnel()
harpoonFilter, match, and iterate over marked files from harpoon.
Defaults
opts.start: 1opts.direction: "forward"opts.max_results: #settings.labelsContent
type: "harpoon"buffer: the file mark's bufnrcursor: the file mark's row and colextra.index: the file mark's index:select(): uses harpoon.ui#nav_file-- Open a default search for harpoon's marks
require("portal.builtin").harpoon.tunnel()
jumplistFilter, match, and iterate over Neovim's :h jumplist.
Defaults
opts.start: current jump indexopts.direction: "backward"opts.max_results: #settings.labelsContent
type: "jumplist"buffer: the jumplist bufnrcursor: the jumplist lnum and colextra.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,
})
quickfixFilter, match, and iterate over Neovim's :h quickfix list.
Defaults
opts.start: 1opts.direction: "forward"opts.max_results: #settings.labelsContent
type: "quickfix"buffer: the quickfix bufnrcursor: the quickfix lnum and col:select(): uses nvim_win_set_cursor for selection-- Open portals for the quickfix list (from the top)
require("portal.builtin").quickfix.tunnel()
portal#tunnelSearch, 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 }),
})
portal#searchComplete 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()
portal#portalsCreate 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)
portal#openOpen 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#closeClose 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[]
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.
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.
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
})
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.
-- 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,
}
})
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: filters nil valuesCollect operations
Operations which return a collection (list or table) of values.
Iterator.collect(): T[]Iterator.collect_table(): tableIterator.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()
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.SearchOptionsOptions available for tuning a search query. See the builtins section for information regarding search option defaults.
Type: table
start: integerdirection: Portal.Directionmax_results: integerfilter: Portal.SearchPredicateslots: Portal.SearchPredicate[] | nilPortal.DirectionUsed for indicating whether a search should be performed forwards or backwards.
Type: enum
"forward""backward"Portal.SearchPredicateA predicate where the argument provided is an instance of Portal.Content.
Type: fun(c: Portal.Content): boolean
Portal.QueryNamed 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.Iteratorslots: Portal.SearchPredicate[] | nilPortal.ContentAn 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: stringbuffer: integercursor: { row: integer, col: integer }extra: table:select()Portal.WindowA wrapper object around some Portal.Content.
Type: object
:select():open():close()Portal.PredicateBasic function type used for filtering and matching values produced from an iterator.
Type: fun(v: any): boolean
Portal.QueryGeneratorGenerating function which transforms an input set of Portal.SearchOptions into a proper Portal.Query.
Type: fun(o: Portal.SearchOptions, s: Portal.Settings): Portal.Query