1

Regenerate nvim config

This commit is contained in:
2024-06-02 03:29:20 +02:00
parent 75eea0c030
commit ef2e28883d
5576 changed files with 604886 additions and 503 deletions

View File

@ -0,0 +1,149 @@
local Pattern = require("flash.search.pattern")
local Pos = require("flash.search.pos")
local Util = require("flash.util")
---@class Flash.State.Window
---@field win number
---@field buf number
---@field topline number
---@field botline number
---@field changedtick number
---@class Flash.Cache
---@field state Flash.State
---@field pattern Flash.Pattern
---@field wins Flash.State.Window[]
local M = {}
M.__index = M
---@type table<Flash.State.Window, {matches: Flash.Match[]}>
M.cache = setmetatable({}, { __mode = "k" })
---@param state Flash.State
function M.new(state)
local self = setmetatable({}, M)
self.state = state
self.pattern = Pattern.new("", state.opts.search.mode, state.opts.search.trigger)
self.wins = {}
return self
end
---@return boolean dirty Returns true when dirty
function M:update()
local dirty = false
if self.pattern ~= self.state.pattern then
self.pattern = self.state.pattern:clone()
dirty = true
M.cache = {}
end
local win = vim.api.nvim_get_current_win()
if self.state.win ~= win then
self.state.win = win
self.state.pos = Pos(win)
self.state.restore_windows = Util.save_layout()
M.cache = {}
dirty = true
end
self:_update_wins()
for _, w in ipairs(self.state.wins) do
if self:_dirty(w) then
dirty = true
end
end
return dirty
end
---@param win window
function M:get_state(win)
local window = self:get(win)
if not window then
return
end
if M.cache[window] then
return M.cache[window]
end
local from = Pos({ window.topline, 0 })
local to = Pos({ window.botline + 1, 0 })
if not self.state.opts.search.wrap and win == self.state.win then
if self.state.opts.search.forward then
from = self.state.pos
else
to = self.state.pos
end
end
local matcher = self.state:get_matcher(win)
if matcher.update then
matcher:update()
end
M.cache[window] = {
matches = matcher:get({ from = from, to = to }),
}
return M.cache[window]
end
---@param win window
---@return Flash.State.Window
function M:get(win)
return self.wins[win]
end
function M:_update_wins()
-- prioritize current window
self.state.wins = { self.state.win }
if self.state.opts.search.multi_window then
local keep_current = false
---@param w window
self.state.wins = vim.tbl_filter(function(w)
local buf = vim.api.nvim_win_get_buf(w)
local ft = vim.bo[buf].filetype
for _, exclude in ipairs(self.state.opts.search.exclude) do
if type(exclude) == "string" and exclude == ft then
return false
elseif type(exclude) == "function" and exclude(w) then
return false
end
end
if w == self.state.win then
keep_current = true
return false
end
return true
end, vim.api.nvim_tabpage_list_wins(0))
if keep_current then
table.insert(self.state.wins, 1, self.state.win)
end
end
end
---@param win window
function M:_dirty(win)
local info = vim.fn.getwininfo(win)[1]
local buf = vim.api.nvim_win_get_buf(win)
---@type Flash.State.Window
local state = {
win = win,
buf = buf,
cursor = vim.api.nvim_win_get_cursor(win),
topline = info.topline,
botline = info.botline,
changedtick = vim.b[buf].changedtick,
}
if not vim.deep_equal(state, self.wins[win]) then
self.wins[win] = state
return true
end
end
return M

View File

@ -0,0 +1,36 @@
local Repeat = require("flash.repeat")
---@class Flash.Commands
local M = {}
---@param opts? Flash.State.Config
function M.jump(opts)
local state = Repeat.get_state("jump", opts)
state:loop()
return state
end
---@param opts? Flash.State.Config
function M.treesitter(opts)
return require("flash.plugins.treesitter").jump(opts)
end
---@param opts? Flash.State.Config
function M.treesitter_search(opts)
return require("flash.plugins.treesitter").search(opts)
end
---@param opts? Flash.State.Config
function M.remote(opts)
local Config = require("flash.config")
opts = Config.get({ mode = "remote" }, opts)
return M.jump(opts)
end
---@param enabled? boolean
function M.toggle(enabled)
local Search = require("flash.plugins.search")
return Search.toggle(enabled)
end
return M

View File

@ -0,0 +1,345 @@
---@type Flash.Config
local M = {}
---@class Flash.Config
---@field mode? string
---@field enabled? boolean
---@field ns? string
---@field config? fun(opts:Flash.Config)
local defaults = {
-- 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,
},
}
---@type Flash.Config
local options
---@param opts? Flash.Config
function M.setup(opts)
opts = opts or {}
opts.mode = nil
options = {}
options = M.get(opts)
require("flash.plugins.search").setup()
if options.modes.char.enabled then
require("flash.plugins.char").setup()
end
end
---@param ... Flash.Config|Flash.State.Config|nil
---@return Flash.State.Config
function M.get(...)
if options == nil then
M.setup()
end
---@type Flash.Config[]
local all = { {}, defaults, options or {} }
---@type table<string, boolean>
local modes = {}
for i = 1, select("#", ...) do
---@type Flash.Config?
local opts = select(i, ...)
if type(opts) == "string" then
opts = options.modes[opts]
end
if opts then
table.insert(all, opts)
local idx = #all
while opts.mode and not modes[opts.mode] do
modes[opts.mode or ""] = true
opts = options.modes[opts.mode] or {}
table.insert(all, idx, opts)
end
end
end
-- backward compatibility
for _, o in ipairs(all) do
if o.highlight and o.highlight.label then
o.label = vim.tbl_deep_extend("force", o.label or {}, o.highlight.label)
---@diagnostic disable-next-line: no-unknown
o.highlight.label = nil
vim.notify_once(
"flash: `opts.highlight.label` is deprecated, use `opts.label` instead",
vim.log.levels.WARN
)
end
for _, field in ipairs({ "autohide", "jump_labels" }) do
if type(o[field]) == "function" then
local motion = require("flash.plugins.char").motion
---@diagnostic disable-next-line: no-unknown
o[field] = o[field](motion)
end
end
end
local ret = vim.tbl_deep_extend("force", unpack(all))
---@cast ret Flash.State.Config
if type(ret.config) == "function" then
ret.config(ret)
end
if vim.g.vscode then
ret.prompt.enabled = false
end
return ret
end
return setmetatable(M, {
__index = function(_, key)
if options == nil then
M.setup()
end
return options[key]
end,
})

View File

@ -0,0 +1,32 @@
local Docs = require("lazy.docs")
local M = {}
function M.update()
local config = Docs.extract("lua/flash/config.lua", "\nlocal defaults = ({.-\n})")
config = config:gsub("%s*debug = false.\n", "\n")
Docs.save({
config = config,
setup = Docs.extract("lua/flash/docs.lua", "function M%.suggested%(%)\n%s*return (.-)\nend"),
})
end
function M.suggested()
return {
"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" },
},
}
end
M.update()
return M

View File

@ -0,0 +1,68 @@
local Pos = require("flash.search.pos")
local M = {}
---@type ffi.namespace*
local C
local incsearch_state = {}
local function _ffi()
if not C then
local ffi = require("ffi")
ffi.cdef([[
int search_match_endcol;
int no_mapping;
unsigned int search_match_lines;
void setcursor_mayforce(bool force);
]])
C = ffi.C
end
return C
end
---@private
---@param from Pos
function M.get_end_pos(from)
_ffi()
local ret = Pos({
from[1] + C.search_match_lines,
math.max(0, C.search_match_endcol - 1),
})
local line = vim.api.nvim_buf_get_lines(0, ret[1] - 1, ret[1], false)[1]
local char_idx = vim.fn.charidx(line, ret[2])
ret[2] = vim.fn.byteidx(line, char_idx)
return ret
end
function M.save_incsearch_state()
_ffi()
incsearch_state = {
match_endcol = C.search_match_endcol,
match_lines = C.search_match_lines,
}
end
function M.mappings_enabled()
_ffi()
return C.no_mapping == 0
end
function M.setcursor(force)
if vim.api.nvim__redraw then
vim.api.nvim__redraw({ cursor = true })
else
if force == nil then
force = false
end
_ffi()
return C.setcursor_mayforce(force)
end
end
function M.restore_incsearch_state()
_ffi()
C.search_match_endcol = incsearch_state.match_endcol
C.search_match_lines = incsearch_state.match_lines
end
return M

View File

@ -0,0 +1,216 @@
local M = {}
function M.clear(ns)
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
vim.api.nvim_buf_clear_namespace(buf, ns, 0, -1)
end
end
function M.setup()
if vim.g.vscode then
local hls = {
FlashBackdrop = { fg = "#545c7e" },
FlashCurrent = { bg = "#ff966c", fg = "#1b1d2b" },
FlashLabel = { bg = "#ff007c", bold = true, fg = "#c8d3f5" },
FlashMatch = { bg = "#3e68d7", fg = "#c8d3f5" },
FlashCursor = { reverse = true },
}
for hl_group, hl in pairs(hls) do
hl.default = true
vim.api.nvim_set_hl(0, hl_group, hl)
end
else
local links = {
FlashBackdrop = "Comment",
FlashMatch = "Search",
FlashCurrent = "IncSearch",
FlashLabel = "Substitute",
FlashPrompt = "MsgArea",
FlashPromptIcon = "Special",
FlashCursor = "Cursor",
}
for hl_group, link in pairs(links) do
vim.api.nvim_set_hl(0, hl_group, { link = link, default = true })
end
end
end
M.setup()
---@param state Flash.State
function M.backdrop(state)
for _, win in ipairs(state.wins) do
local info = vim.fn.getwininfo(win)[1]
local buf = vim.api.nvim_win_get_buf(win)
local from = { info.topline, 0 }
local to = { info.botline + 1, 0 }
if state.win == win and not state.opts.search.wrap then
if state.opts.search.forward then
from = { state.pos[1], state.pos[2] + 1 }
else
to = state.pos
end
end
-- we need to create a backdrop for each line because of the way
-- extmarks priority rendering works
for line = from[1], to[1] do
vim.api.nvim_buf_set_extmark(buf, state.ns, line - 1, line == from[1] and from[2] or 0, {
hl_group = state.opts.highlight.groups.backdrop,
end_row = line == to[1] and line - 1 or line,
hl_eol = line ~= to[1],
end_col = line == to[1] and to[2] or from[2],
priority = state.opts.highlight.priority,
strict = false,
})
end
end
end
---@param state Flash.State
function M.cursor(state)
for _, win in ipairs(state.wins) do
local cursor = vim.api.nvim_win_get_cursor(win)
local buf = vim.api.nvim_win_get_buf(win)
vim.api.nvim_buf_set_extmark(buf, state.ns, cursor[1] - 1, cursor[2], {
hl_group = "FlashCursor",
end_col = cursor[2] + 1,
priority = state.opts.highlight.priority + 3,
strict = false,
})
end
end
---@param state Flash.State
function M.update(state)
M.clear(state.ns)
if state.opts.highlight.backdrop then
M.backdrop(state)
end
local style = state.opts.label.style
if style == "inline" and vim.fn.has("nvim-0.10.0") == 0 then
style = "overlay"
end
local after = state.opts.label.after
after = after == true and { 0, 1 } or after
---@cast after number[]
local before = state.opts.label.before
before = before == true and { 0, -1 } or before
---@cast before number[]
if style == "inline" and before then
before[2] = before[2] + 1
end
local target = state.target
---@type table<string, {buf: number, row: number, col: number, text:string[][]}>
local extmarks = {}
---@param match Flash.Match
---@param pos number[]
---@param offset number[]
---@param is_after boolean
local function label(match, pos, offset, is_after)
local buf = vim.api.nvim_win_get_buf(match.win)
local cursor = vim.api.nvim_win_get_cursor(match.win)
local pos2 = require("flash.util").offset_pos(buf, pos, offset)
local row, col = pos2[1] - 1, pos2[2]
-- dont show the label if the cursor is on the same position
-- in the same window
-- and the label is not a range
if
cursor[1] == row + 1
and cursor[2] == col
and match.win == state.win
and state.opts.jump.pos ~= "range"
then
return
end
if match.fold then
-- set the row to the fold start
row = match.fold - 1
col = 0
end
local hl_group = state.opts.highlight.groups.label
if state.rainbow then
hl_group = state.rainbow:get(match)
elseif
-- set hl_group to current if the match is the current target
-- and the target is a single character
target
and target.pos[1] == row + 1
and target.pos[2] == col
and target.pos == target.end_pos
then
hl_group = state.opts.highlight.groups.current
end
if match.label == "" then
-- when empty label, highlight the position
vim.api.nvim_buf_set_extmark(buf, state.ns, row, col, {
hl_group = hl_group,
end_row = row,
end_col = col + 1,
strict = false,
priority = state.opts.highlight.priority + 2,
})
else
-- else highlight the label
local key = buf .. ":" .. row .. ":" .. col
extmarks[key] = extmarks[key] or { buf = buf, row = row, col = col, text = {} }
local text = state.opts.label.format({
state = state,
match = match,
hl_group = hl_group,
after = is_after,
})
for i = #text, 1, -1 do
table.insert(extmarks[key].text, 1, text[i])
end
end
end
for _, match in ipairs(state.results) do
local buf = vim.api.nvim_win_get_buf(match.win)
local highlight = state.opts.highlight.matches
if match.highlight ~= nil then
highlight = match.highlight
end
if highlight then
vim.api.nvim_buf_set_extmark(buf, state.ns, match.pos[1] - 1, match.pos[2], {
end_row = match.end_pos[1] - 1,
end_col = match.end_pos[2] + 1,
hl_group = target and match.pos == target.pos and state.opts.highlight.groups.current
or state.opts.highlight.groups.match,
strict = false,
priority = state.opts.highlight.priority + 1,
})
end
end
for _, match in ipairs(state.results) do
if match.label and after then
label(match, match.end_pos, after, true)
end
if match.label and before then
label(match, match.pos, before, false)
end
end
for _, extmark in pairs(extmarks) do
vim.api.nvim_buf_set_extmark(extmark.buf, state.ns, extmark.row, extmark.col, {
virt_text = extmark.text,
virt_text_pos = style,
strict = false,
priority = state.opts.highlight.priority + 2,
})
end
M.cursor(state)
end
return M

View File

@ -0,0 +1,13 @@
---@type Flash.Commands
local M = {}
---@param opts? Flash.Config
function M.setup(opts)
require("flash.config").setup(opts)
end
return setmetatable(M, {
__index = function(_, k)
return require("flash.commands")[k]
end,
})

View File

@ -0,0 +1,253 @@
local Hacks = require("flash.hacks")
local Pos = require("flash.search.pos")
local Util = require("flash.util")
local M = {}
---@param match Flash.Match
---@param state Flash.State
---@return Flash.Match?
function M.jump(match, state)
local register = vim.v.register
-- add to jump list
if state.opts.jump.jumplist then
vim.cmd("normal! m'")
end
local mode = vim.fn.mode(true)
local is_op = mode:sub(1, 2) == "no"
local is_visual = mode:sub(1, 1) == "v"
if is_op and (state.opts.remote_op.motion or match.win ~= vim.api.nvim_get_current_win()) then
-- use our special logic for remote operator pending mode
return M.remote_op(match, state, register)
end
-- change window if needed
if match.win ~= vim.api.nvim_get_current_win() then
if is_visual then
-- cancel visual mode in the current window,
-- to avoid issues with the remote window
vim.cmd("normal! v")
end
vim.api.nvim_set_current_win(match.win)
if is_visual then
-- enable visual mode in the remote window,
-- from its current cursor position
vim.cmd("normal! v")
end
end
M._jump(match, state, { op = is_op })
end
function M.fix_selection()
local selection = vim.go.selection
vim.go.selection = "inclusive"
vim.schedule(function()
vim.go.selection = selection
end)
end
-- Remote operator pending mode.Cancel the operator and
-- re-trigger the operator in the remote window.
---@param match Flash.Match
---@param state Flash.State
---@param register string
---@return Flash.Match?
function M.remote_op(match, state, register)
Util.exit()
-- schedule this so that the active operator is properly cancelled
vim.schedule(function()
local motion = state.opts.remote_op.motion
if motion == nil then
motion = match.win ~= vim.api.nvim_get_current_win()
end
vim.api.nvim_set_current_win(match.win)
-- use a new motion to select the text-object to act on,
-- unless we're jumping to a range
if motion then
if vim.fn.mode() == "v" then
vim.cmd("normal! v")
end
if state.opts.jump.pos == "range" then
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(match.win, match.end_pos)
else
vim.api.nvim_win_set_cursor(
match.win,
state.opts.jump.pos == "start" and match.pos or match.end_pos
)
end
-- otherwise, use the remote window's cursor position
else
local from = vim.api.nvim_win_get_cursor(match.win)
M._jump(match, state, { op = true })
local to = vim.api.nvim_win_get_cursor(match.win)
-- if a range was selected, use that instead
if vim.fn.mode() == "v" then
vim.cmd("normal! v") -- end the selection
from = vim.api.nvim_buf_get_mark(0, "<")
to = vim.api.nvim_buf_get_mark(0, ">")
end
-- vim.api.nvim_buf_set_mark(0, "[", from[1], from[2], {})
-- vim.api.nvim_buf_set_mark(0, "]", to[1], to[2], {})
-- select the range for the operator
vim.api.nvim_win_set_cursor(0, from)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(0, to)
end
---@diagnostic disable-next-line: param-type-mismatch
local opmap = vim.fn.maparg(vim.v.operator, "", false, true) --[[@as any]]
if not vim.tbl_isempty(opmap) then
vim.keymap.del("", vim.v.operator)
end
-- re-trigger the operator
vim.api.nvim_input('"' .. register .. vim.v.operator)
if state.opts.remote_op.restore then
vim.schedule(function()
if not vim.tbl_isempty(opmap) then
vim.fn.mapset(opmap.mode, false, opmap)
end
M.restore_remote(state)
end)
end
end)
end
-- Restore window views after the remote operation ends
---@param state Flash.State
function M.restore_remote(state)
local restore = vim.schedule_wrap(function()
state:restore()
end)
-- wait till getting user input clears
if not Hacks.mappings_enabled() then
return Util.on_done(function()
return Hacks.mappings_enabled()
end, function()
M.restore_remote(state)
end)
-- wait till operator pending mode ends
elseif vim.fn.mode(true):sub(1, 2) == "no" then
return Util.on_done(function()
return vim.fn.mode(true):sub(1, 2) ~= "no"
end, function()
M.restore_remote(state)
end)
-- restore after making edits
elseif vim.fn.mode() == "i" and vim.v.operator == "c" then
vim.api.nvim_create_autocmd("InsertLeave", {
once = true,
callback = restore,
})
else
restore()
end
end
-- Performs the actual jump in the current window,
-- taking operator-pending mode into account.
---@param match Flash.Match
---@param state Flash.State
---@param opts? {op:boolean}
---@return Flash.Match?
function M._jump(match, state, opts)
opts = opts or {}
M.fix_selection()
M.open_folds(match)
-- select range
if state.opts.jump.pos == "range" then
if vim.fn.mode() == "v" then
vim.cmd("normal! v")
end
vim.api.nvim_win_set_cursor(match.win, match.pos)
vim.cmd("normal! v")
vim.api.nvim_win_set_cursor(match.win, match.end_pos)
else
local pos = state.opts.jump.pos == "start" and match.pos or match.end_pos
if opts.op then
-- fix inclusive/exclusive
-- default is exclusive
if state.opts.jump.inclusive ~= false then
vim.cmd("normal! v")
end
end
local current = Pos(match.win)
local offset = state.opts.jump.offset
if not offset and state.opts.jump.pos == "end" and pos < current then
offset = 1
end
pos = Pos(
require("flash.util").offset_pos(vim.api.nvim_win_get_buf(match.win), pos, { 0, offset or 0 })
)
pos[2] = math.max(0, pos[2])
vim.api.nvim_win_set_cursor(match.win, pos)
end
end
---@param match Flash.Match
function M.open_folds(match)
local cursor = vim.api.nvim_win_get_cursor(match.win)
local from = match.pos[1]
local to = match.end_pos[1]
local is_visual = vim.fn.mode(true):find("v")
local opened = false
for line = from, to do
if vim.fn.foldclosed(line) ~= -1 then
vim.api.nvim_win_set_cursor(match.win, { line, 0 })
vim.cmd("normal! zO")
opened = true
end
end
if opened then
vim.api.nvim_win_set_cursor(match.win, cursor)
if is_visual then
vim.cmd("normal! v")
end
end
end
---@param state Flash.State
function M.on_jump(state)
-- fix or restore the search register
local sf = vim.v.searchforward
if state.opts.jump.register then
vim.fn.setreg("/", state.pattern.search)
end
vim.v.searchforward = sf
-- add the real search pattern to the history
if state.opts.jump.history then
vim.fn.histadd("search", state.pattern.search)
end
-- clear the highlight
if state.opts.jump.nohlsearch then
vim.cmd.nohlsearch()
elseif state.opts.jump.register then
-- this will show the search matches again
vim.cmd("set hlsearch")
end
end
return M

View File

@ -0,0 +1,228 @@
---@class Flash.Labeler
---@field state Flash.State
---@field used table<string, string>
---@field labels string[]
local M = {}
M.__index = M
function M.new(state)
local self
self = setmetatable({}, M)
self.state = state
self.used = {}
self:reset()
return self
end
function M:labeler()
return function()
return self:update()
end
end
function M:update()
self:reset()
if #self.state.pattern() < self.state.opts.label.min_pattern_length then
return
end
local matches = self:filter()
for _, match in ipairs(matches) do
self:label(match, true)
end
for _, match in ipairs(matches) do
if not self:label(match) then
break
end
end
end
function M:reset()
local skip = {} ---@type table<string, boolean>
self.labels = {}
for _, l in ipairs(self.state:labels()) do
if not skip[l] then
self.labels[#self.labels + 1] = l
skip[l] = true
end
end
if
not self.state.opts.search.max_length
or #self.state.pattern() < self.state.opts.search.max_length
then
for _, win in pairs(self.state.wins) do
self.labels = self:skip(win, self.labels)
end
end
for _, m in ipairs(self.state.results) do
if m.label ~= false then
m.label = nil
end
end
end
function M:valid(label)
return vim.tbl_contains(self.labels, label)
end
function M:use(label)
self.labels = vim.tbl_filter(function(c)
return c ~= label
end, self.labels)
end
---@param m Flash.Match
---@param used boolean?
function M:label(m, used)
if m.label ~= nil then
return true
end
local pos = m.pos:id(m.win)
local label ---@type string?
if used then
label = self.used[pos]
else
label = self.labels[1]
end
if label and self:valid(label) then
self:use(label)
local reuse = self.state.opts.label.reuse == "all"
or (self.state.opts.label.reuse == "lowercase" and label:lower() == label)
if reuse then
self.used[pos] = label
end
m.label = label
end
return #self.labels > 0
end
function M:filter()
---@type Flash.Match[]
local ret = {}
local target = self.state.target
local from = vim.api.nvim_win_get_cursor(self.state.win)
---@type table<number, boolean>
local folds = {}
-- only label visible matches
for _, match in ipairs(self.state.results) do
-- and don't label the first match in the current window
local skip = (target and match.pos == target.pos)
and not self.state.opts.label.current
and match.win == self.state.win
-- Only label the first match in each fold
if not skip and match.fold then
if folds[match.fold] then
skip = true
else
folds[match.fold] = true
end
end
if not skip then
table.insert(ret, match)
end
end
-- sort by current win, other win, then by distance
table.sort(ret, function(a, b)
local use_distance = self.state.opts.label.distance and a.win == self.state.win
if a.win ~= b.win then
local aw = a.win == self.state.win and 0 or a.win
local bw = b.win == self.state.win and 0 or b.win
return aw < bw
end
if use_distance then
local dfrom = from[1] * vim.go.columns + from[2]
local da = a.pos[1] * vim.go.columns + a.pos[2]
local db = b.pos[1] * vim.go.columns + b.pos[2]
return math.abs(dfrom - da) < math.abs(dfrom - db)
end
if a.pos[1] ~= b.pos[1] then
return a.pos[1] < b.pos[1]
end
return a.pos[2] < b.pos[2]
end)
return ret
end
-- Returns valid labels for the current search pattern
-- in this window.
---@param labels string[]
---@return string[] returns labels to skip or `nil` when all labels should be skipped
function M:skip(win, labels)
local pattern = self.state.pattern.skip
-- skip all labels if the pattern is empty
if pattern == "" then
return {}
end
-- skip all labels if the pattern is invalid
local ok = pcall(vim.regex, pattern)
if not ok then
return {}
end
-- skip all labels if the pattern ends with a backslash
-- except if it's escaped
if pattern:find("\\$") and not pattern:find("\\\\$") then
return {}
end
vim.api.nvim_win_call(win, function()
while #labels > 0 do
-- this is needed, since an uppercase label would trigger smartcase
local label_group = table.concat(labels, "")
if vim.go.ignorecase then
label_group = label_group:lower()
end
local p = "\\%(" .. pattern .. "\\)\\m\\zs[" .. label_group .. "]"
local pos
ok, pos = pcall(vim.fn.searchpos, p, "cnw")
if not ok then
labels = {}
break
end
-- not found, we're done
if pos[1] == 0 then
return
end
local line = vim.api.nvim_buf_get_lines(0, pos[1] - 1, pos[1], false)[1]
local char = vim.fn.strpart(line, pos[2] - 1, 1, true)
local label_count = #labels
labels = vim.tbl_filter(function(c)
-- when ignorecase is set, we need to skip
-- both the upper and lower case labels
if vim.go.ignorecase then
return c:lower() ~= char:lower()
end
return c ~= char
end, labels)
-- HACK: this will fail if the pattern is an incomplete regex
-- In that case, we skip all labels
if label_count == #labels then
labels = {}
break
end
end
end)
return labels
end
return M

View File

@ -0,0 +1,299 @@
local require = require("flash.require")
local Config = require("flash.config")
local Labeler = require("flash.labeler")
local Repeat = require("flash.repeat")
local Util = require("flash.util")
local M = {}
---@alias Flash.Char.Motion "'f'" | "'F'" | "'t'" | "'T'"
M.motion = "f" ---@type Flash.Char.Motion
M.char = nil ---@type string?
M.jumping = false
M.state = nil ---@type Flash.State?
M.jump_labels = false
---@type table<Flash.Char.Motion, Flash.State.Config>
M.motions = {
f = { label = { after = { 0, 0 }, before = false } },
t = {},
F = {
jump = { inclusive = false },
search = { forward = false },
label = { after = { 0, 0 }, before = false },
},
T = {
jump = { inclusive = false },
search = { forward = false },
label = { before = true, after = false },
},
}
function M.new()
local State = require("flash.state")
local opts = Config.get({
mode = "char",
labeler = M.labeler,
search = {
multi_window = false,
mode = M.mode(M.motion),
max_length = 1,
},
prompt = {
enabled = false,
},
}, M.motions[M.motion])
-- never show the current match label
opts.highlight.groups.current = M.motion:lower() == "f" and opts.highlight.groups.label
or opts.highlight.groups.match
-- exclude the motion labels so we can use them for next/prev
opts.labels = opts.labels:gsub(M.motion:lower(), "")
opts.labels = opts.labels:gsub(M.motion:upper(), "")
return State.new(opts)
end
function M.labeler(matches, state)
if M.jump_labels then
if not state._labeler then
state._labeler = Labeler.new(state)
end
state._labeler:update()
else
-- set to empty label, so that the character will just be highlighted
for _, m in ipairs(matches) do
m.label = ""
end
end
end
---@param motion Flash.Char.Motion
function M.mode(motion)
---@param c string
return function(c)
c = c:gsub("\\", "\\\\")
local pattern ---@type string
if motion == "t" then
pattern = "\\m.\\ze\\V" .. c
elseif motion == "T" then
pattern = "\\V" .. c .. "\\zs\\m."
else
pattern = "\\V" .. c
end
if not Config.get("char").multi_line then
local pos = vim.api.nvim_win_get_cursor(0)
pattern = ("\\%%%dl"):format(pos[1]) .. pattern
end
return pattern
end
end
function M.visible()
return M.state and M.state.visible
end
function M.setup()
Repeat.setup()
local keys = {}
for k, v in pairs(Config.modes.char.keys) do
if vim.g.mapleader ~= v and vim.g.maplocalleader ~= v then
keys[type(k) == "number" and v or k] = v
end
end
-- don't override ;, mappings if they exist
for _, key in ipairs({ ";", "," }) do
local mapping = vim.fn.maparg(key, "n", false, false)
if keys[key] == key and mapping ~= "" then
keys[key] = nil
end
end
for _, key in ipairs({ "f", "F", "t", "T", ";", "," }) do
if keys[key] then
vim.keymap.set({ "n", "x", "o" }, keys[key], function()
M.jumping = true
local autohide = Config.get("char").autohide
if Repeat.is_repeat then
M.jump_labels = false -- never show jump labels when repeating
M.state:jump({ count = vim.v.count1 })
M.state:show()
else
M.jump(key)
end
vim.schedule(function()
M.jumping = false
if M.state and autohide then
M.state:hide()
end
end)
end, {
silent = true,
})
end
end
vim.api.nvim_create_autocmd({ "BufLeave", "CursorMoved", "InsertEnter" }, {
group = vim.api.nvim_create_augroup("flash_char", { clear = true }),
callback = function(event)
local hide = event.event == "InsertEnter" or not M.jumping
if hide and M.state then
M.state:hide()
end
end,
})
vim.on_key(function(key)
if M.state and key == Util.ESC and (vim.fn.mode() == "n" or vim.fn.mode() == "v") then
M.state:hide()
end
end)
end
function M.parse(key)
---@class Flash.Char.Parse
local ret = {
jump = M.next,
actions = {}, ---@type table<string, fun()>
getchar = false,
}
-- repeat last search when hitting the same key
-- don't repeat when executing a macro
if M.visible() and vim.fn.reg_executing() == "" and M.motion:lower() == key:lower() then
ret.actions = M.actions(M.motion)
if ret.actions[key] then
ret.jump = ret.actions[key]
return ret
else
-- no action defined, so clear the state
M.motion = ""
end
end
-- different motion, clear the state
if M.motions[key] and M.motion ~= key then
if M.state then
M.state:hide()
end
M.motion = key
end
ret.actions = M.actions(M.motion)
if M.motions[key] then
ret.getchar = true
else -- ;,
ret.jump = ret.actions[key] or M.next
end
return ret
end
---@param motion Flash.Char.Motion
---@return table<string, fun()>
function M.actions(motion)
local ret = Config.get("char").char_actions(motion)
for key, value in pairs(ret) do
ret[key] = M[value]
end
return ret
end
function M.jump(key)
local parsed = M.parse(key)
if not M.motion then
return
end
local is_op = vim.fn.mode(true):sub(1, 2) == "no"
-- always re-calculate when not visible
M.state = M.visible() and M.state or M.new()
-- get a new target
if parsed.getchar or not M.char then
local char = M.state:get_char()
if char then
M.char = char
else
return M.state:hide()
end
end
-- HACK: When the motion is t or T, we need to set the current position as a valid target
-- but only when we are not repeating
M.current = M.motion:lower() == "t" and parsed.getchar
-- update the state when needed
if M.state.pattern:empty() then
M.state:update({ pattern = M.char })
end
local jump = parsed.jump
M.jump_labels = Config.get("char").jump_labels
jump()
M.state:update({ force = true })
if M.jump_labels then
parsed.actions[Util.CR] = function()
return false
end
M.state:loop({
restore = is_op,
abort = function()
Util.exit()
end,
jump_on_max_length = false,
actions = parsed.actions,
})
end
return M.state
end
M.current = false
function M.right()
return M.state.opts.search.forward and M.next() or M.prev()
end
function M.left()
return M.state.opts.search.forward and M.prev() or M.next()
end
function M.next()
M.state:jump({
count = vim.v.count1,
forward = M.state.opts.search.forward,
current = M.current,
})
M.current = false
return true
end
function M.prev()
M.state:jump({
count = vim.v.count1,
forward = not M.state.opts.search.forward,
current = M.current,
})
M.current = false
-- check if we should enable wrapping.
if not M.state.opts.search.wrap then
local before = M.state:find({ count = 1, forward = false })
if before and (before.pos < M.state.pos) == M.state.opts.search.forward then
M.state.opts.search.wrap = true
M.state._labeler = nil
M.state:update({ force = true })
end
end
return true
end
return M

View File

@ -0,0 +1,163 @@
local require = require("flash.require")
local Config = require("flash.config")
local Jump = require("flash.jump")
local State = require("flash.state")
local Util = require("flash.util")
local M = {}
---@type Flash.State?
M.state = nil
M.op = false
M.enabled = true
---@param enabled? boolean
function M.toggle(enabled)
if enabled == nil then
enabled = not M.enabled
end
if M.enabled == enabled then
return M.enabled
end
M.enabled = enabled
if State.is_search() then
if M.enabled then
M.start()
M.update(false)
elseif M.state then
M.state:hide()
M.state = nil
end
-- redraw to show the change
vim.cmd("redraw")
-- trigger incsearch to update the matches
vim.api.nvim_feedkeys(" " .. Util.BS, "n", true)
end
return M.enabled
end
---@param check_jump? boolean
function M.update(check_jump)
if not M.state then
return
end
local pattern = vim.fn.getcmdline()
-- when doing // or ??, get the pattern from the search register
-- See :h search-commands
if pattern:sub(1, 1) == vim.fn.getcmdtype() then
pattern = vim.fn.getreg("/") .. pattern:sub(2)
end
M.state:update({ pattern = pattern, check_jump = check_jump })
end
function M.start()
M.state = State.new({
mode = "search",
action = M.jump,
search = {
forward = vim.fn.getcmdtype() == "/",
mode = "search",
incremental = vim.go.incsearch,
},
})
if M.op then
M.state.opts.search.multi_window = false
end
end
function M.setup()
local group = vim.api.nvim_create_augroup("flash", { clear = true })
M.enabled = Config.modes.search.enabled or false
local function wrap(fn)
return function(...)
if M.state then
return fn(...)
end
end
end
vim.api.nvim_create_autocmd("CmdlineChanged", {
group = group,
callback = wrap(function()
M.update()
end),
})
vim.api.nvim_create_autocmd("CmdlineLeave", {
group = group,
callback = wrap(function()
M.state:hide()
M.state = nil
end),
})
vim.api.nvim_create_autocmd("CmdlineEnter", {
group = group,
callback = function()
if State.is_search() and M.enabled then
M.start()
M.set_op(vim.fn.mode() == "v")
end
end,
})
vim.api.nvim_create_autocmd("ModeChanged", {
pattern = "*:c",
group = group,
callback = function()
M.set_op(vim.v.event.old_mode:sub(1, 2) == "no" or vim.fn.mode() == "v")
end,
})
end
function M.set_op(op)
M.op = op
if M.op and M.state then
M.state.opts.search.multi_window = false
end
end
---@param self Flash.State
---@param match Flash.Match
function M.jump(match, self)
local pos = match.pos
local search_reg = vim.fn.getreg("/")
-- For operator pending mode, set the search pattern to the
-- first character on the match position
if M.op then
local pos_pattern = ("\\%%%dl\\%%%dc."):format(pos[1], pos[2] + 1)
vim.fn.setcmdline(pos_pattern)
end
-- schedule a <cr> input to trigger the search
vim.schedule(function()
vim.api.nvim_input(M.op and "<cr>" or "<esc>")
end)
-- restore the real search pattern after the search
-- and perform the jump when not in operator pending mode
vim.api.nvim_create_autocmd("CmdlineLeave", {
once = true,
callback = vim.schedule_wrap(function()
-- delete the search pattern.
-- The correct one will be added in `on_jump`
vim.fn.histdel("search", -1)
if M.op then
-- restore original search pattern
vim.fn.setreg("/", search_reg)
else
Jump.jump(match, self)
end
Jump.on_jump(self)
end),
})
end
return M

View File

@ -0,0 +1,181 @@
local Config = require("flash.config")
local Pos = require("flash.search.pos")
local Repeat = require("flash.repeat")
local Util = require("flash.util")
local M = {}
---@class Flash.Match.TS: Flash.Match
---@field node TSNode
---@field depth? number
---@param win window
---@param pos? Pos
function M.get_nodes(win, pos)
local buf = vim.api.nvim_win_get_buf(win)
local line_count = vim.api.nvim_buf_line_count(buf)
pos = pos or Pos()
local nodes = {} ---@type TSNode[]
local ok, tree = pcall(vim.treesitter.get_parser, buf)
if not ok then
vim.notify(
"No treesitter parser for this buffer with filetype=" .. vim.bo[buf].filetype,
vim.log.levels.WARN,
{ title = "flash.nvim" }
)
vim.api.nvim_input("<esc>")
end
if not (ok and tree) then
return {}
end
do
-- get all ranges of the current node and its parents
local node = tree:named_node_for_range({ pos[1] - 1, pos[2], pos[1] - 1, pos[2] }, {
ignore_injections = false,
})
while node do
nodes[#nodes + 1] = node
node = node:parent() ---@type TSNode
end
end
-- convert ranges to matches
---@type Flash.Match.TS[]
local ret = {}
local first = true
---@type table<string,boolean>
local done = {}
for _, node in ipairs(nodes) do
local range = { node:range() }
---@type Flash.Match.TS
local match = {
node = node,
pos = { range[1] + 1, range[2] },
end_pos = { range[3] + 1, range[4] - 1 },
first = first,
}
first = false
-- If the match is at the end of the buffer,
-- then move it to the last character of the last line.
if match.end_pos[1] > line_count then
match.end_pos[1] = line_count
match.end_pos[2] =
#vim.api.nvim_buf_get_lines(buf, match.end_pos[1] - 1, match.end_pos[1], false)[1]
elseif match.end_pos[2] == -1 then
-- If the end points to the start of the next line, move it to the
-- end of the previous line.
-- Otherwise operations include the first character of the next line
local line =
vim.api.nvim_buf_get_lines(buf, match.end_pos[1] - 2, match.end_pos[1] - 1, false)[1]
match.end_pos[1] = match.end_pos[1] - 1
match.end_pos[2] = #line
end
local id = table.concat(vim.tbl_flatten({ match.pos, match.end_pos }), ".")
if not done[id] then
done[id] = true
ret[#ret + 1] = match
end
end
for m, match in ipairs(ret) do
match.pos = Pos(match.pos)
match.end_pos = Pos(match.end_pos)
match.win = win
match.depth = #ret - m
end
return ret
end
---@param win window
---@param state Flash.State
function M.matcher(win, state)
local labels = state:labels()
local ret = M.get_nodes(win, state.pos)
for i = 1, #ret do
ret[i].label = table.remove(labels, 1)
end
return ret
end
---@param opts? Flash.Config
function M.jump(opts)
local state = Repeat.get_state(
"treesitter",
Config.get({ mode = "treesitter" }, opts, {
matcher = M.matcher,
labeler = function() end,
search = { multi_window = false, wrap = true, incremental = false, max_length = 0 },
})
)
---@type Flash.Match?
local current
for _, m in ipairs(state.results) do
---@cast m Flash.Match.TS
if not current or m.depth > current.depth then
current = m
end
end
current = state:jump(current)
state:loop({
abort = function()
vim.cmd([[normal! v]])
end,
actions = {
[";"] = function()
current = state:jump({ match = current, forward = false })
end,
[","] = function()
current = state:jump({ forward = true, match = current })
end,
[Util.CR] = function()
state:jump(current and current.label or nil)
return false
end,
},
jump_on_max_length = false,
})
return state
end
---@param opts? Flash.Config
function M.search(opts)
opts = Config.get({ mode = "treesitter_search" }, opts, {
matcher = function(win, _state, _opts)
local Search = require("flash.search")
local search = Search.new(win, _state)
local matches = {} ---@type Flash.Match[]
for _, m in ipairs(search:get(_opts)) do
-- don't add labels to the search results
m.label = false
table.insert(matches, m)
for _, n in ipairs(M.get_nodes(win, m.pos)) do
-- don't highlight treesitter nodes. Use labels only
n.highlight = false
table.insert(matches, n)
end
end
return matches
end,
jump = { pos = "range" },
})
opts.search.exclude = vim.deepcopy(opts.search.exclude)
table.insert(opts.search.exclude, function(win)
local buf = vim.api.nvim_win_get_buf(win)
return not pcall(vim.treesitter.get_parser, buf)
end)
local state = Repeat.get_state("treesitter-search", opts)
state:loop()
return state
end
return M

View File

@ -0,0 +1,85 @@
local Config = require("flash.config")
---@class Flash.Prompt
---@field win window
---@field buf buffer
local M = {}
local ns = vim.api.nvim_create_namespace("flash_prompt")
function M.visible()
return M.win and vim.api.nvim_win_is_valid(M.win) and M.buf and vim.api.nvim_buf_is_valid(M.buf)
end
function M.show()
if M.visible() then
return
end
require("flash.highlight")
M.buf = vim.api.nvim_create_buf(false, true)
vim.bo[M.buf].buftype = "nofile"
vim.bo[M.buf].bufhidden = "wipe"
vim.bo[M.buf].filetype = "flash_prompt"
local config = vim.deepcopy(Config.prompt.win_config)
if config.width <= 1 then
config.width = config.width * vim.go.columns
end
if config.row < 0 then
config.row = vim.go.lines + config.row
end
if config.col < 0 then
config.col = vim.go.columns + config.col
end
config = vim.tbl_extend("force", config, {
style = "minimal",
focusable = false,
noautocmd = true,
})
M.win = vim.api.nvim_open_win(M.buf, false, config)
vim.wo[M.win].winhighlight = "Normal:FlashPrompt"
end
function M.hide()
if M.win and vim.api.nvim_win_is_valid(M.win) then
vim.api.nvim_win_close(M.win, true)
M.win = nil
end
if M.buf and vim.api.nvim_buf_is_valid(M.buf) then
vim.api.nvim_buf_delete(M.buf, { force = true })
M.buf = nil
end
end
---@param pattern string
function M.set(pattern)
M.show()
local text = vim.deepcopy(Config.prompt.prefix)
text[#text + 1] = { pattern }
local str = ""
for _, item in ipairs(text) do
str = str .. item[1]
end
vim.api.nvim_buf_set_lines(M.buf, 0, -1, false, { str })
vim.api.nvim_buf_clear_namespace(M.buf, ns, 0, -1)
local col = 0
for _, item in ipairs(text) do
local width = vim.fn.strlen(item[1])
if item[2] then
vim.api.nvim_buf_set_extmark(M.buf, ns, 0, col, {
hl_group = item[2],
end_col = col + width,
})
end
col = col + width
end
end
return M

View File

@ -0,0 +1,401 @@
---@class Flash.Rainbow
---@field cache table<string, string>
---@field count number
---@field shade number
local M = {}
---@type table<string,true>
M.hl = {}
function M.setup()
if M.did_setup then
return
end
M.did_setup = true
vim.api.nvim_create_autocmd("ColorScheme", {
callback = function()
M.hl = {}
end,
})
end
---@param state Flash.State
function M.new(state)
local self = setmetatable({}, { __index = M })
self.cache = {}
self.count = 0
self.shade = state.opts.label.rainbow.shade
return self
end
---@param match Flash.Match
function M:get(match)
local buf = vim.api.nvim_win_get_buf(match.win)
local id = match.pos:id(buf)
if match.depth then
id = id .. ":" .. tostring(match.depth)
end
if not self.cache[id] then
self.count = self.count + 1
self.cache[id] = M.get_color(self.count, self.shade)
end
return self.cache[id]
end
---@param idx number
---@param shade number
function M.get_color(idx, shade)
M.setup()
idx = (idx - 1) % #M.rainbow + 1
local color = M.rainbow[idx]
shade = (shade or 5) * 100
local bg = vim.tbl_get(M.colors, color, shade)
if bg then
local hl = "FlashColor" .. color .. shade
if not M.hl[hl] then
M.hl[hl] = true
local bg_shade = shade == 500 and 950 or shade < 500 and 900 or 50
local fg = vim.tbl_get(M.colors, color, bg_shade)
vim.api.nvim_set_hl(0, hl, { bg = "#" .. bg, fg = "#" .. fg, bold = true })
end
return hl
end
end
M.rainbow = {
-- "slate",
-- "gray",
-- "zinc",
-- "neutral",
-- "stone",
"red",
-- "orange",
"amber",
-- "yellow",
"lime",
"green",
-- "emerald",
"teal",
"cyan",
-- "sky",
"blue",
-- "indigo",
"violet",
-- "purple",
"fuchsia",
-- "pink",
"rose",
}
M.colors = {
slate = {
[50] = "f8fafc",
[100] = "f1f5f9",
[200] = "e2e8f0",
[300] = "cbd5e1",
[400] = "94a3b8",
[500] = "64748b",
[600] = "475569",
[700] = "334155",
[800] = "1e293b",
[900] = "0f172a",
[950] = "020617",
},
gray = {
[50] = "f9fafb",
[100] = "f3f4f6",
[200] = "e5e7eb",
[300] = "d1d5db",
[400] = "9ca3af",
[500] = "6b7280",
[600] = "4b5563",
[700] = "374151",
[800] = "1f2937",
[900] = "111827",
[950] = "030712",
},
zinc = {
[50] = "fafafa",
[100] = "f4f4f5",
[200] = "e4e4e7",
[300] = "d4d4d8",
[400] = "a1a1aa",
[500] = "71717a",
[600] = "52525b",
[700] = "3f3f46",
[800] = "27272a",
[900] = "18181b",
[950] = "09090B",
},
neutral = {
[50] = "fafafa",
[100] = "f5f5f5",
[200] = "e5e5e5",
[300] = "d4d4d4",
[400] = "a3a3a3",
[500] = "737373",
[600] = "525252",
[700] = "404040",
[800] = "262626",
[900] = "171717",
[950] = "0a0a0a",
},
stone = {
[50] = "fafaf9",
[100] = "f5f5f4",
[200] = "e7e5e4",
[300] = "d6d3d1",
[400] = "a8a29e",
[500] = "78716c",
[600] = "57534e",
[700] = "44403c",
[800] = "292524",
[900] = "1c1917",
[950] = "0a0a0a",
},
red = {
[50] = "fef2f2",
[100] = "fee2e2",
[200] = "fecaca",
[300] = "fca5a5",
[400] = "f87171",
[500] = "ef4444",
[600] = "dc2626",
[700] = "b91c1c",
[800] = "991b1b",
[900] = "7f1d1d",
[950] = "450a0a",
},
orange = {
[50] = "fff7ed",
[100] = "ffedd5",
[200] = "fed7aa",
[300] = "fdba74",
[400] = "fb923c",
[500] = "f97316",
[600] = "ea580c",
[700] = "c2410c",
[800] = "9a3412",
[900] = "7c2d12",
[950] = "431407",
},
amber = {
[50] = "fffbeb",
[100] = "fef3c7",
[200] = "fde68a",
[300] = "fcd34d",
[400] = "fbbf24",
[500] = "f59e0b",
[600] = "d97706",
[700] = "b45309",
[800] = "92400e",
[900] = "78350f",
[950] = "451a03",
},
yellow = {
[50] = "fefce8",
[100] = "fef9c3",
[200] = "fef08a",
[300] = "fde047",
[400] = "facc15",
[500] = "eab308",
[600] = "ca8a04",
[700] = "a16207",
[800] = "854d0e",
[900] = "713f12",
[950] = "422006",
},
lime = {
[50] = "f7fee7",
[100] = "ecfccb",
[200] = "d9f99d",
[300] = "bef264",
[400] = "a3e635",
[500] = "84cc16",
[600] = "65a30d",
[700] = "4d7c0f",
[800] = "3f6212",
[900] = "365314",
[950] = "1a2e05",
},
green = {
[50] = "f0fdf4",
[100] = "dcfce7",
[200] = "bbf7d0",
[300] = "86efac",
[400] = "4ade80",
[500] = "22c55e",
[600] = "16a34a",
[700] = "15803d",
[800] = "166534",
[900] = "14532d",
[950] = "052e16",
},
emerald = {
[50] = "ecfdf5",
[100] = "d1fae5",
[200] = "a7f3d0",
[300] = "6ee7b7",
[400] = "34d399",
[500] = "10b981",
[600] = "059669",
[700] = "047857",
[800] = "065f46",
[900] = "064e3b",
[950] = "022c22",
},
teal = {
[50] = "f0fdfa",
[100] = "ccfbf1",
[200] = "99f6e4",
[300] = "5eead4",
[400] = "2dd4bf",
[500] = "14b8a6",
[600] = "0d9488",
[700] = "0f766e",
[800] = "115e59",
[900] = "134e4a",
[950] = "042f2e",
},
cyan = {
[50] = "ecfeff",
[100] = "cffafe",
[200] = "a5f3fc",
[300] = "67e8f9",
[400] = "22d3ee",
[500] = "06b6d4",
[600] = "0891b2",
[700] = "0e7490",
[800] = "155e75",
[900] = "164e63",
[950] = "083344",
},
sky = {
[50] = "f0f9ff",
[100] = "e0f2fe",
[200] = "bae6fd",
[300] = "7dd3fc",
[400] = "38bdf8",
[500] = "0ea5e9",
[600] = "0284c7",
[700] = "0369a1",
[800] = "075985",
[900] = "0c4a6e",
[950] = "082f49",
},
blue = {
[50] = "eff6ff",
[100] = "dbeafe",
[200] = "bfdbfe",
[300] = "93c5fd",
[400] = "60a5fa",
[500] = "3b82f6",
[600] = "2563eb",
[700] = "1d4ed8",
[800] = "1e40af",
[900] = "1e3a8a",
[950] = "172554",
},
indigo = {
[50] = "eef2ff",
[100] = "e0e7ff",
[200] = "c7d2fe",
[300] = "a5b4fc",
[400] = "818cf8",
[500] = "6366f1",
[600] = "4f46e5",
[700] = "4338ca",
[800] = "3730a3",
[900] = "312e81",
[950] = "1e1b4b",
},
violet = {
[50] = "f5f3ff",
[100] = "ede9fe",
[200] = "ddd6fe",
[300] = "c4b5fd",
[400] = "a78bfa",
[500] = "8b5cf6",
[600] = "7c3aed",
[700] = "6d28d9",
[800] = "5b21b6",
[900] = "4c1d95",
[950] = "2e1065",
},
purple = {
[50] = "faf5ff",
[100] = "f3e8ff",
[200] = "e9d5ff",
[300] = "d8b4fe",
[400] = "c084fc",
[500] = "a855f7",
[600] = "9333ea",
[700] = "7e22ce",
[800] = "6b21a8",
[900] = "581c87",
[950] = "3b0764",
},
fuchsia = {
[50] = "fdf4ff",
[100] = "fae8ff",
[200] = "f5d0fe",
[300] = "f0abfc",
[400] = "e879f9",
[500] = "d946ef",
[600] = "c026d3",
[700] = "a21caf",
[800] = "86198f",
[900] = "701a75",
[950] = "4a044e",
},
pink = {
[50] = "fdf2f8",
[100] = "fce7f3",
[200] = "fbcfe8",
[300] = "f9a8d4",
[400] = "f472b6",
[500] = "ec4899",
[600] = "db2777",
[700] = "be185d",
[800] = "9d174d",
[900] = "831843",
[950] = "500724",
},
rose = {
[50] = "fff1f2",
[100] = "ffe4e6",
[200] = "fecdd3",
[300] = "fda4af",
[400] = "fb7185",
[500] = "f43f5e",
[600] = "e11d48",
[700] = "be123c",
[800] = "9f1239",
[900] = "881337",
[950] = "4c0519",
},
}
return M

View File

@ -0,0 +1,55 @@
local require = require("flash.require")
local State = require("flash.state")
local M = {}
---@type {is_repeat:boolean, fn:fun()}[]
M._funcs = {}
M._repeat = nil
-- Sets the current operatorfunc to the given function.
function M.set(fn)
vim.go.operatorfunc = [[{x -> x}]]
local visual = vim.fn.mode() == "v"
vim.cmd("normal! g@l")
if visual then
vim.cmd("normal! gv")
end
M._repeat = fn
vim.go.operatorfunc = [[v:lua.require'flash.repeat'._repeat]]
end
M.is_repeat = false
function M.setup()
if M._did_setup then
return
end
M._did_setup = true
vim.on_key(function(key)
if key == "." and vim.fn.reg_executing() == "" and vim.fn.reg_recording() == "" then
M.is_repeat = true
vim.schedule(function()
M.is_repeat = false
end)
end
end)
end
---@type table<string, Flash.State>
M._states = {}
---@param mode string
---@param opts? Flash.State.Config
function M.get_state(mode, opts)
M.setup()
local last = M._states[mode]
if (M.is_repeat or (opts and opts.continue)) and last then
last:show()
return last
end
M._states[mode] = State.new(opts)
return M._states[mode]
end
return M

View File

@ -0,0 +1,25 @@
return function(module)
local mod = nil
local function load()
if not mod then
mod = require(module)
package.loaded[module] = mod
end
return mod
end
-- if already loaded, return the module
-- otherwise return a lazy module
return type(package.loaded[module]) == "table" and package.loaded[module]
or setmetatable({}, {
__index = function(_, key)
return load()[key]
end,
__newindex = function(_, key, value)
load()[key] = value
end,
__call = function(_, ...)
return load()(...)
end,
})
end

View File

@ -0,0 +1,117 @@
local require = require("flash.require")
local Hacks = require("flash.hacks")
local Matcher = require("flash.search.matcher")
local Pos = require("flash.search.pos")
---@class Flash.Search: Flash.Matcher
---@field state Flash.State
---@field win window
local M = {}
M.__index = M
---@param win number
---@param state Flash.State
function M.new(win, state)
local self = setmetatable({}, M)
self.state = state
self.win = win
return self
end
---@param flags? string
---@return Flash.Match?
function M:_next(flags)
flags = flags or ""
local ok, pos = pcall(vim.fn.searchpos, self.state.pattern.search, flags or "")
-- incomplete or invalid pattern
if not ok then
return
end
if pos[1] == 0 then
return
end
pos = Pos({ pos[1], pos[2] - 1 })
return { win = self.win, pos = pos, end_pos = Hacks.get_end_pos(pos) }
end
---@param pos Pos
---@param fn function
function M:_call(pos, fn)
pos = Pos(pos)
local view = vim.api.nvim_win_call(self.win, vim.fn.winsaveview)
local buf = vim.api.nvim_win_get_buf(self.win)
local line_count = vim.api.nvim_buf_line_count(buf)
if pos[1] > line_count then
pos[1] = line_count
local line = vim.api.nvim_buf_get_lines(buf, pos[1] - 1, pos[1], false)[1]
pos[2] = #line - 1
end
vim.api.nvim_win_set_cursor(self.win, pos)
---@type boolean, any?
local ok, err
vim.api.nvim_win_call(self.win, function()
ok, err = pcall(fn)
vim.fn.winrestview(view)
end)
return not ok and error(err) or err
end
---@param opts? {from?:Pos, to?:Pos}
function M:get(opts)
if self.state.pattern:empty() then
return {}
end
opts = opts or {}
opts.from = opts.from and Pos(opts.from) or nil
opts.to = opts.to and Pos(opts.to) or nil
---@type Flash.Match[]
local ret = {}
self:_call(opts.from or { 1, 0 }, function()
local next = self:_next("cW")
while next and (not opts.to or next.pos <= opts.to) do
table.insert(ret, next)
next = self:_next("W")
end
end)
return ret
end
-- Moves the results cursor by `amount` (default 1) and wraps around.
-- When forward is `nil` it uses the current search direction.
-- Otherwise it uses the given direction.
---@param opts? Flash.Match.Find
function M:find(opts)
if self.state.pattern:empty() then
return
end
opts = Matcher.defaults(opts)
local flags = (opts.forward and "" or "b")
.. (opts.wrap and "w" or "W")
.. ((opts.count == 0 or opts.current) and "c" or "")
if opts.match then
opts.pos = opts.match.pos
end
---@type Flash.Match?
local ret
self:_call(opts.pos, function()
for _ = 1, math.max(opts.count, 1) do
ret = self:_next(flags)
flags = flags:gsub("c", "")
end
end)
if not ret or (opts.count == 0 and ret.pos ~= opts.pos) then
return
end
return ret
end
return M

View File

@ -0,0 +1,179 @@
local Pos = require("flash.search.pos")
---@class Flash.Match
---@field win window
---@field pos Pos -- (1,0) indexed
---@field end_pos Pos -- (1,0) indexed
---@field label? string|false -- set to false to disable label
---@field highlight? boolean
---@field fold? number
---@alias Flash.Match.Find {forward?:boolean, wrap?:boolean, count?:number, pos?: Pos, match?:Flash.Match, current?:boolean}
---@class Flash.Matcher
---@field win window
---@field get fun(self, opts?: {from?:Pos, to?:Pos}): Flash.Match[]
---@field find fun(self, opts?: Flash.Match.Find): Flash.Match
---@field labels fun(self, labels: string[]): string[]
---@field update? fun(self)
---@class Flash.Matcher.Custom: Flash.Matcher
---@field matches Flash.Match[]
local M = {}
M.__index = M
function M.new(win)
local self = setmetatable({}, M)
self.matches = {}
self.win = win
return self
end
---@param fn fun(win: window, state:Flash.State, opts: {from:Pos, to:Pos}): Flash.Match[]
function M.from(fn)
---@param win window
---@param state Flash.State
return function(win, state)
local ret = M.new(win)
ret.get = function(self, opts)
local matches = fn(win, state, opts)
if state.opts.filter then
matches = state.opts.filter(matches, state) or matches
end
self:set(matches)
return M.get(self, opts)
end
return ret
end
end
---@param ...? Flash.Match.Find
---@return Flash.Match.Find
function M.defaults(...)
local other = vim.tbl_filter(function(k)
return k ~= nil
end, { ... })
local opts = vim.tbl_extend("force", {
pos = vim.api.nvim_win_get_cursor(0),
forward = true,
wrap = true,
count = 1,
}, {}, unpack(other))
opts.pos = Pos(opts.pos)
return opts
end
---@param opts? Flash.Match.Find
function M:find(opts)
opts = M.defaults(opts)
if opts.count == 0 then
for _, match in ipairs(self.matches) do
if match.pos == opts.pos then
return match
end
end
return
end
---@type number?
local idx
if opts.match then
for m, match in ipairs(self.matches) do
if match.pos == opts.match.pos and match.end_pos == opts.match.end_pos then
idx = m + (opts.forward and 1 or -1)
break
end
end
elseif opts.forward then
for i = 1, #self.matches, 1 do
if self.matches[i].pos > opts.pos then
idx = i
break
end
end
else
for i = #self.matches, 1, -1 do
if self.matches[i].pos < opts.pos then
idx = i
break
end
end
end
if not idx then
if not opts.wrap then
return
end
idx = opts.forward and 1 or #self.matches
end
if opts.forward then
idx = idx + opts.count - 1
else
idx = idx - opts.count + 1
end
if opts.wrap then
idx = (idx - 1) % #self.matches + 1
end
return self.matches[idx]
end
---@param labels string[]
function M:labels(labels)
return labels
end
---@param opts? {from?:Pos, to?:Pos}
function M:get(opts)
return M.filter(self.matches, opts)
end
---@param matches Flash.Match[]
---@param opts? {from?:Pos, to?:Pos}
function M.filter(matches, opts)
opts = opts or {}
opts.from = opts.from and Pos(opts.from)
opts.to = opts.to and Pos(opts.to)
---@param match Flash.Match
return vim.tbl_filter(function(match)
if opts.from and match.end_pos < opts.from then
return false
end
if opts.to and match.pos > opts.to then
return false
end
return true
end, matches)
end
---@param matches Flash.Match[]
function M:set(matches)
for _, match in ipairs(matches) do
match.pos = Pos(match.pos)
match.end_pos = Pos(match.end_pos)
match.win = match.win or self.win
end
table.sort(matches, function(a, b)
if a.win ~= b.win then
return a.win < b.win
end
if a.pos ~= b.pos then
return a.pos < b.pos
end
local da = a.depth or 0
local db = b.depth or 0
if da ~= db then
return da < db
end
return a.end_pos < b.end_pos
end)
self.matches = matches
end
return M

View File

@ -0,0 +1,108 @@
local Util = require("flash.util")
---@class Flash.Pattern
---@field pattern string
---@field search string
---@field skip string
---@field trigger string
---@field mode Flash.Pattern.Mode
---@operator call:string Returns the input pattern
local M = {}
M.__index = M
---@alias Flash.Pattern.Mode "exact" | "fuzzy" | "search" | (fun(input:string):string,string?)
---@param pattern string
---@param mode Flash.Pattern.Mode
---@param trigger string
function M.new(pattern, mode, trigger)
local self = setmetatable({}, M)
self.mode = mode
self.trigger = trigger or ""
self:set(pattern or "")
return self
end
function M:__eq(other)
return other and other.pattern == self.pattern and other.mode == self.mode
end
function M:clone()
return M.new(self.pattern, self.mode, self.trigger)
end
function M:empty()
return self.pattern == ""
end
---@param pattern string
---@return boolean updated
function M:set(pattern)
if pattern ~= self.pattern then
self.pattern = pattern
if pattern == "" then
self.search = ""
self.skip = ""
else
if self.trigger ~= "" and pattern:sub(-1) == self.trigger then
pattern = pattern:sub(1, -2)
end
self.search, self.skip = M._get(pattern, self.mode)
end
return false
end
return true
end
---@param char string
function M:extend(char)
if char == Util.BS then
return self.pattern:sub(1, -2)
end
return self.pattern .. char
end
---@return string the input pattern
function M:__call()
return self.pattern
end
---@param pattern string
---@param mode Flash.Pattern.Mode
---@private
function M._get(pattern, mode)
local skip ---@type string?
if type(mode) == "function" then
pattern, skip = mode(pattern)
elseif mode == "exact" then
pattern, skip = M._exact(pattern)
elseif mode == "fuzzy" then
pattern, skip = M._fuzzy(pattern)
end
return pattern, skip or pattern
end
---@param pattern string
function M._exact(pattern)
return "\\V" .. pattern:gsub("\\", "\\\\")
end
---@param opts? {ignorecase: boolean, whitespace:boolean}
function M._fuzzy(pattern, opts)
opts = vim.tbl_deep_extend("force", {
ignorecase = vim.go.ignorecase,
whitespace = false,
}, opts or {})
local sep = opts.whitespace and ".\\{-}" or "\\[^\\ ]\\{-}"
---@param c string
local chars = vim.tbl_map(function(c)
return c == "\\" and "\\\\" or c
end, vim.fn.split(pattern, "\\zs"))
local ret = "\\V" .. table.concat(chars, sep) .. (opts.ignorecase and "\\c" or "\\C")
return ret, ret .. sep
end
return M

View File

@ -0,0 +1,85 @@
---@class Pos
---@field row number
---@field col number
---@field [1] number
---@field [2] number
---@overload fun(pos?: number[] | { row: number, col: number } | number): Pos
local P = {}
---@param pos? number[] | { row: number, col: number } | number
function P.new(pos)
if pos == nil then
pos = vim.api.nvim_win_get_cursor(0)
elseif type(pos) == "number" then
pos = vim.api.nvim_win_get_cursor(pos)
end
if getmetatable(pos) == P then
return pos
end
local self = setmetatable({}, P)
self[1] = pos[1] or pos.row
self[2] = pos[2] or pos.col
return self
end
function P:__index(key)
if key == "row" then
return rawget(self, 1)
elseif key == "col" then
return rawget(self, 2)
end
return P[key]
end
function P:__newindex(key, value)
if key == "row" then
rawset(self, 1, value)
elseif key == "col" then
rawset(self, 2, value)
else
rawset(self, key, value)
end
end
function P:__eq(other)
return self[1] == other[1] and self[2] == other[2]
end
function P:__tostring()
return ("[%d, %d]"):format(self[1], self[2])
end
function P:id(buf)
return table.concat({ buf, self[1], self[2] }, ":")
end
function P:dist(other)
return math.abs(self[1] - other[1]) + math.abs(self[2] - other[2])
end
function P:__add(other)
other = P(other)
return P.new({ self[1] + other[1], self[2] + other[2] })
end
function P:__sub(other)
other = P(other)
return P.new({ self[1] - other[1], self[2] - other[2] })
end
function P:__lt(other)
other = P(other)
return self[1] < other[1] or (self[1] == other[1] and self[2] < other[2])
end
function P:__le(other)
other = P(other)
return self < other or self == other
end
return setmetatable(P, {
__call = function(_, pos)
return P.new(pos)
end,
})

View File

@ -0,0 +1,420 @@
local require = require("flash.require")
local Cache = require("flash.cache")
local Config = require("flash.config")
local Hacks = require("flash.hacks")
local Highlight = require("flash.highlight")
local Jump = require("flash.jump")
local Matcher = require("flash.search.matcher")
local Pattern = require("flash.search.pattern")
local Prompt = require("flash.prompt")
local Rainbow = require("flash.rainbow")
local Search = require("flash.search")
local Util = require("flash.util")
---@class Flash.State.Config: Flash.Config
---@field matcher? fun(win: window, state:Flash.State, pos: {from:Pos, to:Pos}): Flash.Match[]
---@field filter? fun(matches:Flash.Match[], state:Flash.State): Flash.Match[]
---@field pattern? string
---@field labeler? fun(matches:Flash.Match[], state:Flash.State)
---@field actions? table<string, fun(state:Flash.State, char:string):boolean?>
---@class Flash.State
---@field win window
---@field wins window[]
---@field cache Flash.Cache
---@field pos Pos
---@field view any
---@field results Flash.Match[]
---@field target? Flash.Match
---@field pattern Flash.Pattern
---@field opts Flash.State.Config
---@field labeler fun(matches:Flash.Match[], state:Flash.State)
---@field visible boolean
---@field matcher fun(win: window, state:Flash.State): Flash.Matcher
---@field matchers Flash.Matcher[]
---@field restore_windows? fun()
---@field rainbow? Flash.Rainbow
---@field ns number
---@field langmap table<string, string>
local M = {}
M.__index = M
---@type table<Flash.State, boolean>
M._states = setmetatable({}, { __mode = "k" })
function M.setup()
if M._did_setup then
return
end
M._did_setup = true
local ns = vim.api.nvim_create_namespace("flash")
vim.api.nvim_set_decoration_provider(ns, {
on_start = function()
for state in pairs(M._states) do
if state.visible then
local ok, err = pcall(state.update, state)
if not ok then
vim.schedule(function()
vim.notify(
"Flash error during redraw:\n" .. err,
vim.log.levels.ERROR,
{ title = "flash.nvim" }
)
end)
end
end
end
end,
})
end
---@param char string
function M:lmap(char)
return vim.bo.iminsert == 1 and self.langmap[char] or char
end
function M:get_char()
local ret = Util.get_char()
return ret and self:lmap(ret) or nil
end
function M:labels()
local labels = self.opts.labels
if self.opts.label.uppercase then
labels = labels .. self.opts.labels:upper()
end
local list = vim.fn.split(labels, "\\zs")
local ret = {} ---@type string[]
local added = {} ---@type table<string, boolean>
for _, l in ipairs(vim.fn.split(self.opts.label.exclude, "\\zs")) do
added[l] = true
end
for _, l in ipairs(list) do
if not added[l] then
added[l] = true
ret[#ret + 1] = self:lmap(l)
end
end
return ret
end
function M.is_search()
local t = vim.fn.getcmdtype()
return t == "/" or t == "?"
end
---@param opts? Flash.State.Config
function M.new(opts)
M.setup()
local self = setmetatable({}, M)
self.opts = Config.get(opts)
self.langmap = {}
if vim.bo.iminsert == 1 then
local lmap = vim.api.nvim_buf_get_keymap(0, "l")
for _, m in ipairs(lmap) do
if m.lhs ~= "" then
self.langmap[m.lhs] = m.rhs
end
end
end
self.results = {}
self.matchers = {}
self.wins = {}
self.matcher = self.opts.matcher
if type(self.matcher) == "function" then
self.matcher = Matcher.from(self.opts.matcher)
elseif self.matcher == nil then
self.matcher = Search.new
end
self.pattern = Pattern.new(self.opts.pattern, self.opts.search.mode, self.opts.search.trigger)
self.visible = true
self.cache = Cache.new(self)
self.labeler = self.opts.labeler or require("flash.labeler").new(self):labeler()
self.ns = vim.api.nvim_create_namespace(self.opts.ns or "flash")
M._states[self] = true
if self.opts.label.rainbow.enabled then
self.rainbow = Rainbow.new(self)
end
self:update()
return self
end
---@param target? string|Flash.Match.Find|Flash.Match
---@return Flash.Match?
function M:jump(target)
local match ---@type Flash.Match?
if type(target) == "string" then
match = self:find({ label = target })
elseif target and target.end_pos then
match = target
elseif target then
match = self:find(target)
else
match = self.target
end
if match then
if self.opts.action then
self.opts.action(match, self)
else
Jump.jump(match, self)
Jump.on_jump(self)
end
return match
end
end
-- Will restore all window views
function M:restore()
if self.restore_windows then
self.restore_windows()
end
end
function M:get_matcher(win)
self.matchers[win] = self.matchers[win] or self.matcher(win, self)
return self.matchers[win]
end
---@param opts? Flash.Match.Find | {label?:string, pos?: Pos}
function M:find(opts)
if opts and opts.label then
for _, m in ipairs(self.results) do
if m.label == opts.label then
return m
end
end
return
end
opts = Matcher.defaults({
forward = self.opts.search.forward,
wrap = self.opts.search.wrap,
}, opts)
local matcher = self:get_matcher(self.win)
local ret = matcher:find(opts)
if ret then
for _, m in ipairs(self.results) do
if m.pos == ret.pos and m.end_pos == ret.end_pos then
return m
end
end
end
return ret
end
-- Checks if the given pattern is a jump label and jumps to it.
---@param pattern string
function M:check_jump(pattern)
if not self.visible then
return
end
if self.opts.search.trigger ~= "" and self.pattern():sub(-1) ~= self.opts.search.trigger then
return
end
local chars = vim.fn.strchars(pattern)
if
pattern:find(self.pattern(), 1, true) == 1 and chars == vim.fn.strchars(self.pattern()) + 1
then
local label = vim.fn.strcharpart(pattern, chars - 1, 1)
if self:jump(label) then
return true
end
end
end
---@param opts? {pattern:string, force:boolean, check_jump:boolean}
---@return boolean? abort `true` if the search was aborted
function M:update(opts)
opts = opts or {}
if opts.pattern then
-- abort if pattern is a jump label
if opts.check_jump ~= false and self:check_jump(opts.pattern) then
return true
end
self.pattern:set(opts.pattern)
end
if not self.visible then
return
end
if self.cache:update() or opts.force then
self:_update()
end
end
function M:hide()
if self.visible then
self.visible = false
Highlight.clear(self.ns)
end
end
function M:show()
if not self.visible then
self.visible = true
-- force cache to update win and position
self.win = nil
self:update({ force = true })
end
end
function M:_update()
-- This is needed because we trigger searches during redraw.
-- We need to save the state of the incsearch so that current match
-- will still be displayed correctly.
if M.is_search() then
Hacks.save_incsearch_state()
end
self.results = {}
local done = {} ---@type table<string, boolean>
---@type Flash.Matcher[]
local matchers = {}
for _, win in ipairs(self.wins) do
local buf = vim.api.nvim_win_get_buf(win)
matchers[win] = self:get_matcher(win)
local state = self.cache:get_state(win)
for _, m in ipairs(state and state.matches or {}) do
local id = m.pos:id(buf) .. m.end_pos:id(buf)
if not done[id] then
done[id] = true
table.insert(self.results, m)
end
end
end
self.matchers = matchers
for _, match in ipairs(self.results) do
vim.api.nvim_win_call(match.win, function()
local fold = vim.fn.foldclosed(match.pos[1])
match.fold = fold ~= -1 and fold or nil
end)
end
self:update_target()
self.labeler(self.results, self)
if M.is_search() then
Hacks.restore_incsearch_state()
end
Highlight.update(self)
end
function M:update_target()
-- set target to next match.
-- When not using incremental search,
-- we need to set the target to the previous match
self.target = self:find({
pos = self.pos,
count = vim.v.count1,
})
local info = vim.fn.getwininfo(self.win)[1]
local function is_visible()
return self.target and self.target.pos[1] >= info.topline and self.target.pos[1] <= info.botline
end
if self.opts.search.incremental then
-- only update cursor if the target is not visible
-- and we are not activated
if self.target and not self.is_search() then
vim.api.nvim_win_set_cursor(self.win, self.target.pos)
end
elseif not is_visible() then
self.target = self:find({
pos = self.pos,
count = vim.v.count1,
forward = not self.opts.search.forward,
})
if not is_visible() then
self.target = nil
end
end
end
---@class Flash.Step.Options
---@field actions? table<string, fun(state:Flash.State, char:string):boolean?>
---@field restore? boolean
---@field abort? fun()
---@field jump_on_max_length? boolean
---@param opts? Flash.Step.Options
function M:step(opts)
opts = opts or {}
if self.opts.prompt.enabled and not M.is_search() then
Prompt.set(self.pattern())
end
local actions = opts.actions or self.opts.actions or {}
local c = self:get_char()
if c == nil then
vim.api.nvim_input("<esc>")
if opts.restore ~= false then
self:restore()
end
if opts.abort then
opts.abort()
end
return
elseif actions[c] then
local ret = actions[c](self, c)
if ret == nil then
return true
end
return ret
-- jump to first
elseif c == Util.CR then
self:jump()
return
end
local orig = self.pattern()
-- break if we jumped
if self:update({ pattern = self.pattern:extend(c) }) then
return
end
-- when we exceed max length, either jump to the label,
-- or input the last key and break
if self.opts.search.max_length and #self.pattern() > self.opts.search.max_length then
self:update({ pattern = orig })
if opts.jump_on_max_length ~= false then
self:jump()
end
vim.api.nvim_input(c)
return
end
-- exit if no results and not in regular search mode
if #self.results == 0 and not self.pattern:empty() and self.pattern.mode ~= 'search' then
if self.opts.search.incremental then
vim.api.nvim_input(c)
end
return
end
-- autojump if only one result
if #self.results == 1 and self.opts.jump.autojump then
self:jump()
return
end
return true
end
---@param opts? Flash.Step.Options
function M:loop(opts)
while self:step(opts) do
end
self:hide()
Prompt.hide()
end
return M

View File

@ -0,0 +1,105 @@
local Hacks = require("flash.hacks")
local require = require("flash.require")
local M = {}
function M.t(str)
return vim.api.nvim_replace_termcodes(str, true, true, true)
end
M.CR = M.t("<cr>")
M.ESC = M.t("<esc>")
M.BS = M.t("<bs>")
M.EXIT = M.t("<C-\\><C-n>")
M.LUA_CALLBACK = "\x80\253g"
M.CMD = "\x80\253h"
function M.exit()
vim.api.nvim_feedkeys(M.EXIT, "nx", false)
vim.api.nvim_feedkeys(M.ESC, "n", false)
end
---@param buf number
---@param pos number[] (1,0)-indexed position
---@param offset number[]
---@return number[] (1,0)-indexed position
function M.offset_pos(buf, pos, offset)
local row = pos[1] + offset[1]
local ok, lines = pcall(vim.api.nvim_buf_get_lines, buf, row - 1, row, true)
if not ok or lines == nil then
-- fallback to old behavior if anything wrong happens
return { row, math.max(pos[2] + offset[2], 0) }
end
local line = lines[1]
local charidx = vim.fn.charidx(line, pos[2])
local col = vim.fn.byteidx(line, charidx + offset[2])
return { row, math.max(col, 0) }
end
function M.get_char()
Hacks.setcursor()
vim.cmd("redraw")
local ok, ret = pcall(vim.fn.getcharstr)
return ok and ret ~= M.ESC and ret or nil
end
function M.layout_wins()
local queue = { vim.fn.winlayout() }
---@type table<window, window>
local wins = {}
while #queue > 0 do
local node = table.remove(queue)
if node[1] == "leaf" then
wins[node[2]] = node[2]
else
vim.list_extend(queue, node[2])
end
end
return wins
end
function M.save_layout()
local current_win = vim.api.nvim_get_current_win()
local wins = M.layout_wins()
---@type table<window, table>
local state = {}
for _, win in pairs(wins) do
state[win] = vim.api.nvim_win_call(win, vim.fn.winsaveview)
end
return function()
for win, s in pairs(state) do
if vim.api.nvim_win_is_valid(win) then
local buf = vim.api.nvim_win_get_buf(win)
-- never restore terminal buffers to prevent flickering
if vim.bo[buf].buftype ~= "terminal" then
pcall(vim.api.nvim_win_call, win, function()
vim.fn.winrestview(s)
end)
end
end
end
vim.api.nvim_set_current_win(current_win)
state = {}
end
end
---@param done fun():boolean
---@param on_done fun()
function M.on_done(done, on_done)
local check = assert(vim.loop.new_check())
local fn = function()
if check:is_closing() then
return
end
if done() then
check:stop()
check:close()
on_done()
end
end
check:start(vim.schedule_wrap(fn))
end
return M