flash.nvim
lets you navigate your code with search labels, enhanced character motions, and Treesitter integration.
/
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.f
, t
, F
, T
motionsexact
, search
(regex), and fuzzy
search modesInstall 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 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
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, -- when using jump labels, set to 'true' to automatically jump -- or execute a motion when there is only one match autojump = 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 flash remote = { 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 text 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
;
and ,
to increase/decrease the selectionregular 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:
f{char}
or F{char},
you can repeat the motion with f
or go to the previous match with F
to undo a jump.t{char}
or T{char},
you can repeat the motion with t
or go to the previous match with T
.;
or previous match with ,
<esc>
.toggle Search: require("flash").toggle(boolean?)
Treesitter Search: require("flash").treesitter_search(opts?)
opens flash in Treesitter Search mode
yR
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
iw
or even start flash Treesitter with S
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
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 CodeThe 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 matchesYou can also add labels in the matcher
function and then set labeler
to an empty function labeler = function() end
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, }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) return vim.bo[vim.api.nvim_win_get_buf(win)].filetype ~= "snacks_picker_list" end, }, }, action = function(match) local idx = 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)
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
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4