flash.nvim
lets you navigate your code with search labels,
enhanced character motions, and Treesitter integration.
Search Integration | Standalone Jump |
---|---|
f , t , F , T |
Treesitter |
- 🔍 Search Integration: integrate flash.nvim with your regular
search using
/
or?
. Labels appear next to the matches, allowing you to quickly jump to any location. Labels are guaranteed not to exist as a continuation of the search pattern. - ⌨️ type as many characters as you want before using a jump label.
- ⚡ Enhanced
f
,t
,F
,T
motions - 🌳 Treesitter Integration: all parents of the Treesitter node under your cursor are highlighted with a label for quick selection of a specific Treesitter node.
- 🎯 Jump Mode: a standalone jumping mode similar to search
- 🔎 Search Modes:
exact
,search
(regex), andfuzzy
search modes - 🪟 Multi Window jumping
- 🌐 Remote Actions: perform motions in remote locations
- ⚫ dot-repeatable jumps
- 📡 highly extensible: check the examples
- Neovim >= 0.8.0 (needs to be built with LuaJIT)
Install the plugin with your preferred package manager:
{
"folke/flash.nvim",
event = "VeryLazy",
---@type Flash.Config
opts = {},
-- stylua: ignore
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash Treesitter" },
{ "r", mode = "o", function() require("flash").remote() end, desc = "Remote Flash" },
{ "R", mode = { "o", "x" }, function() require("flash").treesitter_search() end, desc = "Treesitter Search" },
{ "<c-s>", mode = { "c" }, function() require("flash").toggle() end, desc = "Toggle Flash Search" },
},
}
⚠️ When creating the keymaps manually either use a lua function likefunction() require("flash").jump() end
as the rhs, or a string like<cmd>lua require("flash").jump()<cr>
. DO NOT use:lua
, since that will break dot-repeat
flash.nvim is highly configurable. Please refer to the default settings below.
Default Settings
{
-- labels = "abcdefghijklmnopqrstuvwxyz",
labels = "asdfghjklqwertyuiopzxcvbnm",
search = {
-- search/jump in all windows
multi_window = true,
-- search direction
forward = true,
-- when `false`, find only matches in the given direction
wrap = true,
---@type Flash.Pattern.Mode
-- Each mode will take ignorecase and smartcase into account.
-- * exact: exact match
-- * search: regular search
-- * fuzzy: fuzzy search
-- * fun(str): custom function that returns a pattern
-- For example, to only match at the beginning of a word:
-- mode = function(str)
-- return "\\<" .. str
-- end,
mode = "exact",
-- behave like `incsearch`
incremental = false,
-- Excluded filetypes and custom window filters
---@type (string|fun(win:window))[]
exclude = {
"notify",
"cmp_menu",
"noice",
"flash_prompt",
function(win)
-- exclude non-focusable windows
return not vim.api.nvim_win_get_config(win).focusable
end,
},
-- Optional trigger character that needs to be typed before
-- a jump label can be used. It's NOT recommended to set this,
-- unless you know what you're doing
trigger = "",
-- max pattern length. If the pattern length is equal to this
-- labels will no longer be skipped. When it exceeds this length
-- it will either end in a jump or terminate the search
max_length = false, ---@type number|false
},
jump = {
-- save location in the jumplist
jumplist = true,
-- jump position
pos = "start", ---@type "start" | "end" | "range"
-- add pattern to search history
history = false,
-- add pattern to search register
register = false,
-- clear highlight after jump
nohlsearch = false,
-- automatically jump when there is only one match
autojump = false,
-- You can force inclusive/exclusive jumps by setting the
-- `inclusive` option. By default it will be automatically
-- set based on the mode.
inclusive = nil, ---@type boolean?
-- jump position offset. Not used for range jumps.
-- 0: default
-- 1: when pos == "end" and pos < current position
offset = nil, ---@type number
},
label = {
-- allow uppercase labels
uppercase = true,
-- add any labels with the correct case here, that you want to exclude
exclude = "",
-- add a label for the first match in the current window.
-- you can always jump to the first match with `<CR>`
current = true,
-- show the label after the match
after = true, ---@type boolean|number[]
-- show the label before the match
before = false, ---@type boolean|number[]
-- position of the label extmark
style = "overlay", ---@type "eol" | "overlay" | "right_align" | "inline"
-- flash tries to re-use labels that were already assigned to a position,
-- when typing more characters. By default only lower-case labels are re-used.
reuse = "lowercase", ---@type "lowercase" | "all" | "none"
-- for the current window, label targets closer to the cursor first
distance = true,
-- minimum pattern length to show labels
-- Ignored for custom labelers.
min_pattern_length = 0,
-- Enable this to use rainbow colors to highlight labels
-- Can be useful for visualizing Treesitter ranges.
rainbow = {
enabled = false,
-- number between 1 and 9
shade = 5,
},
-- With `format`, you can change how the label is rendered.
-- Should return a list of `[text, highlight]` tuples.
---@class Flash.Format
---@field state Flash.State
---@field match Flash.Match
---@field hl_group string
---@field after boolean
---@type fun(opts:Flash.Format): string[][]
format = function(opts)
return { { opts.match.label, opts.hl_group } }
end,
},
highlight = {
-- show a backdrop with hl FlashBackdrop
backdrop = true,
-- Highlight the search matches
matches = true,
-- extmark priority
priority = 5000,
groups = {
match = "FlashMatch",
current = "FlashCurrent",
backdrop = "FlashBackdrop",
label = "FlashLabel",
},
},
-- action to perform when picking a label.
-- defaults to the jumping logic depending on the mode.
---@type fun(match:Flash.Match, state:Flash.State)|nil
action = nil,
-- initial pattern to use when opening flash
pattern = "",
-- When `true`, flash will try to continue the last search
continue = false,
-- Set config to a function to dynamically change the config
config = nil, ---@type fun(opts:Flash.Config)|nil
-- You can override the default options for a specific mode.
-- Use it with `require("flash").jump({mode = "forward"})`
---@type table<string, Flash.Config>
modes = {
-- options used when flash is activated through
-- a regular search with `/` or `?`
search = {
-- when `true`, flash will be activated during regular search by default.
-- You can always toggle when searching with `require("flash").toggle()`
enabled = false,
highlight = { backdrop = false },
jump = { history = true, register = true, nohlsearch = true },
search = {
-- `forward` will be automatically set to the search direction
-- `mode` is always set to `search`
-- `incremental` is set to `true` when `incsearch` is enabled
},
},
-- options used when flash is activated through
-- `f`, `F`, `t`, `T`, `;` and `,` motions
char = {
enabled = true,
-- dynamic configuration for ftFT motions
config = function(opts)
-- autohide flash when in operator-pending mode
opts.autohide = opts.autohide or (vim.fn.mode(true):find("no") and vim.v.operator == "y")
-- disable jump labels when not enabled, when using a count,
-- or when recording/executing registers
opts.jump_labels = opts.jump_labels
and vim.v.count == 0
and vim.fn.reg_executing() == ""
and vim.fn.reg_recording() == ""
-- Show jump labels only in operator-pending mode
-- opts.jump_labels = vim.v.count == 0 and vim.fn.mode(true):find("o")
end,
-- hide after jump when not using jump labels
autohide = false,
-- show jump labels
jump_labels = false,
-- set to `false` to use the current line only
multi_line = true,
-- When using jump labels, don't use these keys
-- This allows using those keys directly after the motion
label = { exclude = "hjkliardc" },
-- by default all keymaps are enabled, but you can disable some of them,
-- by removing them from the list.
-- If you rather use another key, you can map them
-- to something else, e.g., { [";"] = "L", [","] = H }
keys = { "f", "F", "t", "T", ";", "," },
---@alias Flash.CharActions table<string, "next" | "prev" | "right" | "left">
-- The direction for `prev` and `next` is determined by the motion.
-- `left` and `right` are always left and right.
char_actions = function(motion)
return {
[";"] = "next", -- set to `right` to always go right
[","] = "prev", -- set to `left` to always go left
-- clever-f style
[motion:lower()] = "next",
[motion:upper()] = "prev",
-- jump2d style: same case goes next, opposite case goes prev
-- [motion] = "next",
-- [motion:match("%l") and motion:upper() or motion:lower()] = "prev",
}
end,
search = { wrap = false },
highlight = { backdrop = true },
jump = { register = false },
},
-- options used for treesitter selections
-- `require("flash").treesitter()`
treesitter = {
labels = "abcdefghijklmnopqrstuvwxyz",
jump = { pos = "range" },
search = { incremental = false },
label = { before = true, after = true, style = "inline" },
highlight = {
backdrop = false,
matches = false,
},
},
treesitter_search = {
jump = { pos = "range" },
search = { multi_window = true, wrap = true, incremental = false },
remote_op = { restore = true },
label = { before = true, after = true, style = "inline" },
},
-- options used for remote flash
remote = {
remote_op = { restore = true, motion = true },
},
},
-- options for the floating window that shows the prompt,
-- for regular jumps
prompt = {
enabled = true,
prefix = { { "⚡", "FlashPromptIcon" } },
win_config = {
relative = "editor",
width = 1, -- when <=1 it's a percentage of the editor width
height = 1,
row = -1, -- when negative it's an offset from the bottom
col = 0, -- when negative it's an offset from the right
zindex = 1000,
},
},
-- options for remote operator pending mode
remote_op = {
-- restore window views and cursor position
-- after doing a remote operation
restore = false,
-- For `jump.pos = "range"`, this setting is ignored.
-- `true`: always enter a new motion when doing a remote operation
-- `false`: use the window's cursor position and jump target
-- `nil`: act as `true` for remote windows, `false` for the current window
motion = false,
},
}
-
Treesitter:
require("flash").treesitter(opts?)
opens flash in Treesitter mode- use a jump label, or use
;
and,
to increase/decrease the selection
- use a jump label, or use
-
regular search: search as you normally do, but enhanced with jump labels. You need to set
opts.modes.search.enabled = true
, or toggle it withrequire("flash").toggle()
-
f
,t
,F
,T
motions:- After typing
f{char}
orF{char},
you can repeat the motion withf
or go to the previous match withF
to undo a jump. - Similarly, after typing
t{char}
orT{char},
you can repeat the motion witht
or go to the previous match withT
. - You can also go to the next match with
;
or previous match with,
- Any highlights clear automatically when moving, changing buffers,
or pressing
<esc>
.
- After typing
-
toggle Search:
require("flash").toggle(boolean?)
- toggles flash on or off while using regular search
-
Treesitter Search:
require("flash").treesitter_search(opts?)
opens flash in Treesitter Search mode- combination of Treesitter and Search modes
- do something like
yR
- you can now start typing a search pattern.
- arround your matches, all the surrounding Treesitter nodes will be labeled.
- select a label to perform the operator on the new selection
-
remote:
require("flash").remote(opts?)
opens flash in remote mode-
equivalent to:
require("flash").jump({ remote_op = { restore = true, motion = true, }, })
-
this is only useful in operator pending mode.
-
For example, press
yr
to start yanking and open flash- select a label to set the cursor position
- perform any motion, like
iw
or even start flash Treesitter withS
- the yank will be performed on the new selection
- you'll be back in the original window / position
-
You can also configure the
remote_op
options by default, so thatys
, behaves likeyr
for remote operationsrequire("flash").jump({ remote_op = { restore = true, motion = nil, }, })
-
-
jump:
require("flash").jump(opts?)
opens flash with the given options- type any number of characters before typing a jump label
-
VS Code: some functionality is changed/disabled when running flash in VS Code:
prompt
is disabledhighlights
are set to different defaults that will actually work in VS Code
The options for require("flash").jump(opts?)
, are the same as
those in the config section, but can additionally have the following fields:
matcher
: a custom function that generates matches for a given windowlabeler
: a custom function to label matches
You can also add labels in the matcher
function and then set labeler
to an empty function labeler = function() end
Type Definitions
type FlashMatcher = (win: number, state: FlashState) => FlashMatch[];
type FlashLabeler = (matches: FlashMatch[], state: FlashState) => void;
interface FlashMatch {
win: number;
pos: [number, number]; // (1,0)-indexed
end_pos: [number, number]; // (1,0)-indexed
label?: string | false; // set to false to never show a label for this match
highlight?: boolean; // override opts.highlight.matches for this match
}
// Check the code for the full definition
// of Flash.State at `lua/flash/state.lua`
type FlashState = {};
Forward search only
require("flash").jump({
search = { forward = true, wrap = false, multi_window = false },
})
Backward search only
require("flash").jump({
search = { forward = false, wrap = false, multi_window = false },
})
Show diagnostics at target, without changing cursor position
require("flash").jump({
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
-- More advanced example that also highlights diagnostics:
require("flash").jump({
matcher = function(win)
---@param diag Diagnostic
return vim.tbl_map(function(diag)
return {
pos = { diag.lnum + 1, diag.col },
end_pos = { diag.end_lnum + 1, diag.end_col - 1 },
}
end, vim.diagnostic.get(vim.api.nvim_win_get_buf(win)))
end,
action = function(match, state)
vim.api.nvim_win_call(match.win, function()
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.diagnostic.open_float()
end)
state:restore()
end,
})
Match beginning of words only
require("flash").jump({
search = {
mode = function(str)
return "\\<" .. str
end,
},
})
Initialize flash with the word under the cursor
require("flash").jump({
pattern = vim.fn.expand("<cword>"),
})
Jump to a line
require("flash").jump({
search = { mode = "search", max_length = 0 },
label = { after = { 0, 0 } },
pattern = "^"
})
Select any word
require("flash").jump({
pattern = ".", -- initialize pattern with any char
search = {
mode = function(pattern)
-- remove leading dot
if pattern:sub(1, 1) == "." then
pattern = pattern:sub(2)
end
-- return word pattern and proper skip pattern
return ([[\<%s\w*\>]]):format(pattern), ([[\<%s]]):format(pattern)
end,
},
-- select the range
jump = { pos = "range" },
})
f
, t
, F
, T
with labels
Use the options below:
{
modes = {
char = {
jump_labels = true
}
}
}
Telescope integration
This will allow you to use s
in normal mode
and <c-s>
in insert mode, to jump to a label in Telescope results.
{
"nvim-telescope/telescope.nvim",
optional = true,
opts = function(_, opts)
local function flash(prompt_bufnr)
require("flash").jump({
pattern = "^",
label = { after = { 0, 0 } },
search = {
mode = "search",
exclude = {
function(win)
return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= "TelescopeResults"
end,
},
},
action = function(match)
local picker = require("telescope.actions.state").get_current_picker(prompt_bufnr)
picker:set_selection(match.pos[1] - 1)
end,
})
end
opts.defaults = vim.tbl_deep_extend("force", opts.defaults or {}, {
mappings = {
n = { s = flash },
i = { ["<c-s>"] = flash },
},
})
end,
}
Continue last search
require("flash").jump({continue = true})
2-char jump, similar to mini.jump2d or HopWord (hop.nvim)
local Flash = require("flash")
---@param opts Flash.Format
local function format(opts)
-- always show first and second label
return {
{ opts.match.label1, "FlashMatch" },
{ opts.match.label2, "FlashLabel" },
}
end
Flash.jump({
search = { mode = "search" },
label = { after = false, before = { 0, 0 }, uppercase = false, format = format },
pattern = [[\<]],
action = function(match, state)
state:hide()
Flash.jump({
search = { max_length = 0 },
highlight = { matches = false },
label = { format = format },
matcher = function(win)
-- limit matches to the current label
return vim.tbl_filter(function(m)
return m.label == match.label and m.win == win
end, state.results)
end,
labeler = function(matches)
for _, m in ipairs(matches) do
m.label = m.label2 -- use the second label
end
end,
})
end,
labeler = function(matches, state)
local labels = state:labels()
for m, match in ipairs(matches) do
match.label1 = labels[math.floor((m - 1) / #labels) + 1]
match.label2 = labels[(m - 1) % #labels + 1]
match.label = match.label1
end
end,
})
Group | Default | Description |
---|---|---|
FlashBackdrop |
Comment |
backdrop |
FlashMatch |
Search |
search matches |
FlashCurrent |
IncSearch |
current match |
FlashLabel |
Substitute |
jump label |
FlashPrompt |
MsgArea |
prompt |
FlashPromptIcon |
Special |
prompt icon |
FlashCursor |
Cursor |
cursor |