mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 02:38:46 +00:00
fix(picker): show regex matches in list when needed. Fixes #878
This commit is contained in:
parent
2528fcb02c
commit
1d99bac9bc
2 changed files with 118 additions and 78 deletions
|
@ -14,6 +14,7 @@
|
|||
---@field selected snacks.picker.Item[]
|
||||
---@field selected_map table<string, snacks.picker.Item>
|
||||
---@field matcher snacks.picker.Matcher matcher for formatting list items
|
||||
---@field matcher_regex snacks.picker.Matcher matcher for formatting list items
|
||||
---@field target? {cursor: number, top?: number}
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
@ -63,6 +64,7 @@ function M.new(picker)
|
|||
self.selected = {}
|
||||
self.selected_map = {}
|
||||
self.matcher = require("snacks.picker.core.matcher").new(picker.opts.matcher)
|
||||
self.matcher_regex = require("snacks.picker.core.matcher").new({ regex = true })
|
||||
local win_opts = Snacks.win.resolve(picker.opts.win.list, {
|
||||
show = false,
|
||||
enter = false,
|
||||
|
@ -488,8 +490,10 @@ function M:format(item)
|
|||
end
|
||||
|
||||
-- Highlight match positions for text
|
||||
local positions = self.matcher:positions({ text = text:gsub("%s*$", ""), idx = 1, score = 0, file = item.file })
|
||||
for _, pos in ipairs(positions.text or {}) do
|
||||
local it = { text = text:gsub("%s*$", ""), idx = 1, score = 0, file = item.file }
|
||||
local positions = self.matcher:positions(it).text or {}
|
||||
vim.list_extend(positions, self.matcher_regex:positions(it).text or {})
|
||||
for _, pos in ipairs(positions) do
|
||||
table.insert(extmarks, {
|
||||
col = pos - 1,
|
||||
end_col = pos,
|
||||
|
@ -562,10 +566,14 @@ function M:render()
|
|||
vim.api.nvim_buf_set_lines(self.win.buf, 0, -1, false, lines)
|
||||
|
||||
-- matcher for highlighting should include the search filter
|
||||
local pattern = vim.trim(self.picker.input.filter.pattern .. " " .. self.picker.input.filter.search)
|
||||
local pattern = vim.trim(self.picker.input.filter.pattern)
|
||||
if self.matcher.pattern ~= pattern then
|
||||
self.matcher:init(pattern)
|
||||
end
|
||||
local search = vim.trim(self.picker.input.filter.search)
|
||||
if self.matcher_regex.pattern ~= search then
|
||||
self.matcher_regex:init(search)
|
||||
end
|
||||
|
||||
-- render items
|
||||
for i = self.top, math.min(self:count(), self.top + height - 1) do
|
||||
|
|
|
@ -4,6 +4,9 @@ local Async = require("snacks.picker.util.async")
|
|||
---@field match_tick? number
|
||||
---@field match_topk? number
|
||||
|
||||
---@class snacks.picker.matcher.Config
|
||||
---@field regex? boolean used internally for positions of sources that use regex
|
||||
|
||||
---@class snacks.picker.Matcher
|
||||
---@field opts snacks.picker.matcher.Config
|
||||
---@field mods snacks.picker.matcher.Mods[][]
|
||||
|
@ -34,12 +37,13 @@ local YIELD_MATCH = 1 -- ms
|
|||
---@field field? string
|
||||
---@field ignorecase? boolean
|
||||
---@field fuzzy? boolean
|
||||
---@field regex? boolean
|
||||
---@field word? boolean
|
||||
---@field exact_suffix? boolean
|
||||
---@field exact_prefix? boolean
|
||||
---@field inverse? boolean
|
||||
|
||||
---@param opts? snacks.picker.matcher.Config
|
||||
---@param opts? snacks.picker.matcher.Config|{}
|
||||
function M.new(opts)
|
||||
local self = setmetatable({}, M)
|
||||
self.opts = vim.tbl_deep_extend("force", {
|
||||
|
@ -172,20 +176,24 @@ function M:init(pattern)
|
|||
if pattern == "" then
|
||||
return true
|
||||
end
|
||||
local is_or = false
|
||||
for _, p in ipairs(vim.split(pattern, " +")) do
|
||||
if p == "|" then
|
||||
is_or = true
|
||||
else
|
||||
local mods = self:_prepare(p)
|
||||
if mods.pattern ~= "" then
|
||||
if is_or and #self.mods > 0 then
|
||||
table.insert(self.mods[#self.mods], mods)
|
||||
else
|
||||
table.insert(self.mods, { mods })
|
||||
if self.opts.regex then
|
||||
self.mods = { { self:_prepare(pattern) } }
|
||||
else
|
||||
local is_or = false
|
||||
for _, p in ipairs(vim.split(pattern, " +")) do
|
||||
if p == "|" then
|
||||
is_or = true
|
||||
else
|
||||
local mods = self:_prepare(p)
|
||||
if mods.pattern ~= "" then
|
||||
if is_or and #self.mods > 0 then
|
||||
table.insert(self.mods[#self.mods], mods)
|
||||
else
|
||||
table.insert(self.mods, { mods })
|
||||
end
|
||||
end
|
||||
is_or = false
|
||||
end
|
||||
is_or = false
|
||||
end
|
||||
end
|
||||
for _, ors in ipairs(self.mods) do
|
||||
|
@ -210,74 +218,79 @@ function M:_prepare(pattern)
|
|||
---@type snacks.picker.matcher.Mods
|
||||
local mods = { pattern = pattern, entropy = 0, chars = {} }
|
||||
|
||||
local file_patterns = {
|
||||
"^(.*[/\\].*):(%d*):(%d*)$",
|
||||
"^(.*[/\\].*):(%d*)$",
|
||||
"^(.+%.[a-z_]+):(%d*):(%d*)$",
|
||||
"^(.+%.[a-z_]+):(%d*)$",
|
||||
}
|
||||
if self.opts.regex then
|
||||
mods.regex = true
|
||||
else
|
||||
local file_patterns = {
|
||||
"^(.*[/\\].*):(%d*):(%d*)$",
|
||||
"^(.*[/\\].*):(%d*)$",
|
||||
"^(.+%.[a-z_]+):(%d*):(%d*)$",
|
||||
"^(.+%.[a-z_]+):(%d*)$",
|
||||
}
|
||||
|
||||
for _, p in ipairs(file_patterns) do
|
||||
local file, line, col = pattern:match(p)
|
||||
if file then
|
||||
mods.field = "file"
|
||||
mods.pattern = file .. "$"
|
||||
self.file = {
|
||||
path = file,
|
||||
pos = { tonumber(line) or 1, tonumber(col) or 0 },
|
||||
}
|
||||
break
|
||||
for _, p in ipairs(file_patterns) do
|
||||
local file, line, col = pattern:match(p)
|
||||
if file then
|
||||
mods.field = "file"
|
||||
mods.pattern = file .. "$"
|
||||
self.file = {
|
||||
path = file,
|
||||
pos = { tonumber(line) or 1, tonumber(col) or 0 },
|
||||
}
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- minimum two chars for field pattern
|
||||
local field, p = pattern:match("^([%w_][%w_]+):(.*)$")
|
||||
if field then
|
||||
mods.field = field
|
||||
mods.pattern = p
|
||||
end
|
||||
mods.ignorecase = self.opts.ignorecase
|
||||
local is_lower = mods.pattern:lower() == mods.pattern
|
||||
if self.opts.smartcase then
|
||||
mods.ignorecase = is_lower
|
||||
end
|
||||
mods.fuzzy = self.opts.fuzzy
|
||||
if not mods.fuzzy then
|
||||
mods.entropy = mods.entropy + 10
|
||||
end
|
||||
if mods.pattern:sub(1, 1) == "!" then
|
||||
mods.fuzzy, mods.inverse = false, true
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy - 1
|
||||
end
|
||||
if mods.pattern:sub(1, 1) == "'" then
|
||||
mods.fuzzy = false
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy + 10
|
||||
if mods.pattern:sub(-1, -1) == "'" then
|
||||
mods.word = true
|
||||
mods.pattern = mods.pattern:sub(1, -2)
|
||||
-- minimum two chars for field pattern
|
||||
local field, p = pattern:match("^([%w_][%w_]+):(.*)$")
|
||||
if field then
|
||||
mods.field = field
|
||||
mods.pattern = p
|
||||
end
|
||||
mods.ignorecase = self.opts.ignorecase
|
||||
local is_lower = mods.pattern:lower() == mods.pattern
|
||||
if self.opts.smartcase then
|
||||
mods.ignorecase = is_lower
|
||||
end
|
||||
mods.fuzzy = self.opts.fuzzy
|
||||
if not mods.fuzzy then
|
||||
mods.entropy = mods.entropy + 10
|
||||
end
|
||||
elseif mods.pattern:sub(1, 1) == "^" then
|
||||
mods.fuzzy, mods.exact_prefix = false, true
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy + 20
|
||||
end
|
||||
if mods.pattern:sub(-1, -1) == "$" then
|
||||
mods.fuzzy = false
|
||||
mods.exact_suffix = true
|
||||
mods.pattern = mods.pattern:sub(1, -2)
|
||||
mods.entropy = mods.entropy + 20
|
||||
end
|
||||
local rare_chars = #mods.pattern:gsub("[%w%s]", "")
|
||||
mods.entropy = mods.entropy + math.min(#mods.pattern, 20) + rare_chars * 2
|
||||
if not mods.ignorecase and not is_lower then
|
||||
mods.entropy = mods.entropy * 2
|
||||
end
|
||||
if mods.ignorecase then
|
||||
mods.pattern = mods.pattern:lower()
|
||||
if mods.pattern:sub(1, 1) == "!" then
|
||||
mods.fuzzy, mods.inverse = false, true
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy - 1
|
||||
end
|
||||
if mods.pattern:sub(1, 1) == "'" then
|
||||
mods.fuzzy = false
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy + 10
|
||||
if mods.pattern:sub(-1, -1) == "'" then
|
||||
mods.word = true
|
||||
mods.pattern = mods.pattern:sub(1, -2)
|
||||
mods.entropy = mods.entropy + 10
|
||||
end
|
||||
elseif mods.pattern:sub(1, 1) == "^" then
|
||||
mods.fuzzy, mods.exact_prefix = false, true
|
||||
mods.pattern = mods.pattern:sub(2)
|
||||
mods.entropy = mods.entropy + 20
|
||||
end
|
||||
if mods.pattern:sub(-1, -1) == "$" then
|
||||
mods.fuzzy = false
|
||||
mods.exact_suffix = true
|
||||
mods.pattern = mods.pattern:sub(1, -2)
|
||||
mods.entropy = mods.entropy + 20
|
||||
end
|
||||
local rare_chars = #mods.pattern:gsub("[%w%s]", "")
|
||||
mods.entropy = mods.entropy + math.min(#mods.pattern, 20) + rare_chars * 2
|
||||
if not mods.ignorecase and not is_lower then
|
||||
mods.entropy = mods.entropy * 2
|
||||
end
|
||||
if mods.ignorecase then
|
||||
mods.pattern = mods.pattern:lower()
|
||||
end
|
||||
end
|
||||
|
||||
for c = 1, #mods.pattern do
|
||||
mods.chars[c] = mods.pattern:sub(c, c)
|
||||
end
|
||||
|
@ -413,12 +426,31 @@ function M:fuzzy_positions(str, pattern, from)
|
|||
return ret
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@param pattern string
|
||||
---@return number? score, number? from, number? to, string? str
|
||||
function M:regex(str, pattern)
|
||||
local ok, re = pcall(vim.regex, pattern)
|
||||
if not ok then
|
||||
return
|
||||
end
|
||||
local from, to = re:match_str(str)
|
||||
if from and to then
|
||||
return self.score:get(str, from, to), from, to, str
|
||||
end
|
||||
end
|
||||
|
||||
---@param item snacks.picker.Item
|
||||
---@param mods snacks.picker.matcher.Mods
|
||||
---@return number? score, number? from, number? to, string? str
|
||||
function M:_match(item, mods)
|
||||
self.score.is_file = item.file ~= nil
|
||||
local str = item.text
|
||||
|
||||
if mods.regex then
|
||||
return self:regex(str, mods.pattern)
|
||||
end
|
||||
|
||||
if mods.field then
|
||||
if item[mods.field] == nil then
|
||||
if mods.inverse then
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue