Skip to content

Repository files navigation

⚡flash.nvim

flash.nvim lets you navigate your code with search labels, enhanced character motions, and Treesitter integration.

Search IntegrationStandalone Jump
f, t, F, TTreesitter

✨ Features

  • 🔍 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), and fuzzy search modes
  • 🪟 Multi Window jumping
  • 🌐 Remote Actions: perform motions in remote locations
  • dot-repeatable jumps
  • 📡 highly extensible: check the examples

📋 Requirements

  • Neovim >= 0.8.0 (needs to be built with LuaJIT)

📦 Installation

Install the plugin with your preferred package manager:

lazy.nvim:

{
"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 like function() 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

⚙️ Configuration

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

🚀 Usage

  • Treesitter: require("flash").treesitter(opts?) opens flash in Treesitter mode

    • use a jump label, or use ; and , to increase/decrease the selection
  • regular search: search as you normally do, but enhanced with jump labels. You need to set opts.modes.search.enabled = true, or toggle it with require("flash").toggle()

  • f, t, F, T motions:

    • After typing f{char} or F{char}, you can repeat the motion with f or go to the previous match with F to undo a jump.
    • Similarly, after typing t{char} or T{char}, you can repeat the motion with t or go to the previous match with T.
    • You can also go to the next match with ; or previous match with ,
    • Any highlights clear automatically when moving, changing buffers, or pressing <esc>.
  • 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 with S
      • 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 that ys, behaves like yr for remote operations

      require("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 disabled. You can use require("flash").prompt() to get the prompt text and integrate it into the statusline.
    • highlights are set to different defaults that will actually work in VS Code

📡 API

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 window
  • labeler: 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={};

💡 Examples

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

🌈 Highlights

GroupDefaultDescription
FlashBackdropCommentbackdrop
FlashMatchSearchsearch matches
FlashCurrentIncSearchcurrent match
FlashLabelSubstitutejump label
FlashPromptMsgAreaprompt
FlashPromptIconSpecialprompt icon
FlashCursorCursorcursor

📦 Alternatives

About

Navigate your code with search labels, enhanced character motions and Treesitter integration

Topics

Resources

Stars

4.2k stars

Watchers

5 watching

Forks

Releases

Used by

Contributors

Languages