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,Tmotions - 🌳 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), andfuzzysearch 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",
---@typeFlash.Configopts= {},
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() endas 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 windowsmulti_window=true,
-- search directionforward=true,
-- when `false`, find only matches in the given directionwrap=true,
---@typeFlash.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 windowsreturnnotvim.api.nvim_win_get_config(win).focusableend,
},
-- 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 doingtrigger="",
-- 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 searchmax_length=false, ---@typenumber|false
},
jump= {
-- save location in the jumplistjumplist=true,
-- jump positionpos="start", ---@type"start" | "end" | "range"-- add pattern to search historyhistory=false,
-- add pattern to search registerregister=false,
-- clear highlight after jumpnohlsearch=false,
-- automatically jump when there is only one matchautojump=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, ---@typeboolean?-- jump position offset. Not used for range jumps.-- 0: default-- 1: when pos == "end" and pos < current positionoffset=nil, ---@typenumber
},
label= {
-- allow uppercase labelsuppercase=true,
-- add any labels with the correct case here, that you want to excludeexclude="",
-- 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 matchafter=true, ---@typeboolean|number[]-- show the label before the matchbefore=false, ---@typeboolean|number[]-- position of the label extmarkstyle="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 firstdistance=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 9shade=5,
},
-- With `format`, you can change how the label is rendered.-- Should return a list of `[text, highlight]` tuples.---@classFlash.Format---@fieldstateFlash.State---@fieldmatchFlash.Match---@fieldhl_groupstring---@fieldafterboolean---@typefun(opts:Flash.Format): string[][]format=function(opts)
return { { opts.match.label, opts.hl_group } }
end,
},
highlight= {
-- show a backdrop with hl FlashBackdropbackdrop=true,
-- Highlight the search matchesmatches=true,
-- extmark prioritypriority=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.---@typefun(match:Flash.Match, state:Flash.State)|nilaction=nil,
-- initial pattern to use when opening flashpattern="",
-- When `true`, flash will try to continue the last searchcontinue=false,
-- Set config to a function to dynamically change the configconfig=nil, ---@typefun(opts:Flash.Config)|nil-- You can override the default options for a specific mode.-- Use it with `require("flash").jump({mode = "forward"})`---@typetable<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 `,` motionschar= {
enabled=true,
-- dynamic configuration for ftFT motionsconfig=function(opts)
-- autohide flash when in operator-pending modeopts.autohide=opts.autohideor (vim.fn.mode(true):find("no") andvim.v.operator=="y")
-- disable jump labels when not enabled, when using a count,-- or when recording/executing registersopts.jump_labels=opts.jump_labelsandvim.v.count==0andvim.fn.reg_executing() ==""andvim.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 labelsautohide=false,
-- show jump labelsjump_labels=false,
-- set to `false` to use the current line onlymulti_line=true,
-- When using jump labels, don't use these keys-- This allows using those keys directly after the motionlabel= { 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", ";", "," },
---@aliasFlash.CharActionstable<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,
-- when using jump labels, set to 'true' to automatically jump-- or execute a motion when there is only one matchautojump=false,
},
},
-- options used for treesitter selections-- `require("flash").treesitter()`treesitter= {
labels="abcdefghijklmnopqrstuvwxyz",
jump= { pos="range", autojump=true },
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 flashremote= {
remote_op= { restore=true, motion=true },
},
},
-- options for the floating window that shows the prompt,-- for regular jumps-- `require("flash").prompt()` is always available to get the prompt textprompt= {
enabled=true,
prefix= { { "⚡", "FlashPromptIcon" } },
win_config= {
relative="editor",
border="none",
width=1, -- when <=1 it's a percentage of the editor widthheight=1,
row=-1, -- when negative it's an offset from the bottomcol=0, -- when negative it's an offset from the rightzindex=1000,
},
},
-- options for remote operator pending moderemote_op= {
-- restore window views and cursor position-- after doing a remote operationrestore=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 windowmotion=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,Tmotions:- After typing
f{char}orF{char},you can repeat the motion withfor go to the previous match withFto undo a jump. - Similarly, after typing
t{char}orT{char},you can repeat the motion withtor 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 modeequivalent to:
require("flash").jump({ remote_op= { restore=true, motion=true, }, })
this is only useful in operator pending mode.
For example, press
yrto start yanking and open flash- select a label to set the cursor position
- perform any motion, like
iwor 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_opoptions by default, so thatys, behaves likeyrfor 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:
promptis disabled. You can userequire("flash").prompt()to get the prompt text and integrate it into the statusline.highlightsare 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
typeFlashMatcher=(win: number,state: FlashState)=>FlashMatch[];typeFlashLabeler=(matches: FlashMatch[],state: FlashState)=>void;interfaceFlashMatch{win: number;pos: [number,number];// (1,0)-indexedend_pos: [number,number];// (1,0)-indexedlabel?: string|false;// set to false to never show a label for this matchhighlight?: boolean;// override opts.highlight.matches for this match}// Check the code for the full definition// of Flash.State at `lua/flash/state.lua`typeFlashState={};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)
---@paramdiagDiagnosticreturnvim.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"\\<" ..strend,
},
})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 charsearch= {
mode=function(pattern)
-- remove leading dotifpattern:sub(1, 1) =="." thenpattern=pattern:sub(2)
end-- return word pattern and proper skip patternreturn ([[\<%s\w*\>]]):format(pattern), ([[\<%s]]):format(pattern)
end,
},
-- select the rangejump= { pos="range" },
})f, t, F, T with labels
Use the options below:
{
modes= {
char= {
jump_labels=true
}
}
}Treesitter incremental selection
The nvim-treesittermain rewrite no longer includes incremental selection. You can use flash.nvim instead.
vim.keymap.set({"n", "x", "o"}, "<c-space>", function()
require("flash").treesitter({
actions= {
["<c-space>"] ="next",
["<BS>"] ="prev"
}
})
end, { desc="Treesitter incremental selection" })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)
localfunctionflash(prompt_bufnr)
require("flash").jump({
pattern="^",
label= { after= { 0, 0 } },
search= {
mode="search",
exclude= {
function(win)
returnvim.bo[vim.api.nvim_win_get_buf(win)].filetype~="TelescopeResults"end,
},
},
action=function(match)
localpicker=require("telescope.actions.state").get_current_picker(prompt_bufnr)
picker:set_selection(match.pos[1] -1)
end,
})
endopts.defaults=vim.tbl_deep_extend("force", opts.defaultsor {}, {
mappings= {
n= { s=flash },
i= { ["<c-s>"] =flash },
},
})
end,
}Snacks Picker integration
This will allow you to use s in normal mode
and <a-s> in insert mode, to jump to a label in the picker results.
{
"folke/snacks.nvim",
opts= {
picker= {
win= {
input= {
keys= {
["<a-s>"] = { "flash", mode= { "n", "i" } },
["s"] = { "flash" },
},
},
},
actions= {
flash=function(picker)
require("flash").jump({
pattern="^",
label= { after= { 0, 0 } },
search= {
mode="search",
exclude= {
function(win)
returnvim.bo[vim.api.nvim_win_get_buf(win)].filetype~="snacks_picker_list"end,
},
},
action=function(match)
localidx=picker.list:row2idx(match.pos[1])
picker.list:_move(idx, true, true)
end,
})
end,
},
},
},
}Continue last search
require("flash").jump({continue=true})2-char jump, similar to mini.jump2d or HopWord (hop.nvim)
localFlash=require("flash")
---@paramoptsFlash.Formatlocalfunctionformat(opts)
-- always show first and second labelreturn {
{ opts.match.label1, "FlashMatch" },
{ opts.match.label2, "FlashLabel" },
}
endFlash.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 labelreturnvim.tbl_filter(function(m)
returnm.label==match.labelandm.win==winend, state.results)
end,
labeler=function(matches)
for_, minipairs(matches) dom.label=m.label2-- use the second labelendend,
})
end,
labeler=function(matches, state)
locallabels=state:labels()
form, matchinipairs(matches) domatch.label1=labels[math.floor((m-1) /#labels) +1]
match.label2=labels[(m-1) %#labels+1]
match.label=match.label1endend,
})| 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 |



