mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 02:38:46 +00:00
feat(picker.list): better way of highlighting field patterns
This commit is contained in:
parent
fd854ab9ef
commit
924a988d9a
6 changed files with 135 additions and 83 deletions
|
@ -1,7 +1,7 @@
|
|||
local M = {}
|
||||
|
||||
---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number}
|
||||
---@alias snacks.picker.Text {[1]:string, [2]:string?, virtual?:boolean}
|
||||
---@alias snacks.picker.Extmark vim.api.keyset.set_extmark|{col:number, row?:number, field?:string}
|
||||
---@alias snacks.picker.Text {[1]:string, [2]:string?, virtual?:boolean, field?:string}
|
||||
---@alias snacks.picker.Highlight snacks.picker.Text|snacks.picker.Extmark
|
||||
---@alias snacks.picker.format fun(item:snacks.picker.Item, picker:snacks.Picker):snacks.picker.Highlight[]
|
||||
---@alias snacks.picker.preview fun(ctx: snacks.picker.preview.ctx):boolean?
|
||||
|
|
|
@ -316,6 +316,11 @@ function M:set_selected(items)
|
|||
self:update()
|
||||
end
|
||||
|
||||
---@param item snacks.picker.Item
|
||||
function M:is_selected(item)
|
||||
return self.selected_map[self:select_key(item)] ~= nil
|
||||
end
|
||||
|
||||
function M:unpause()
|
||||
if not self.paused then
|
||||
return
|
||||
|
@ -326,93 +331,57 @@ end
|
|||
|
||||
---@param item snacks.picker.Item
|
||||
function M:format(item)
|
||||
local line = self.picker.format(item, self.picker)
|
||||
local parts = {} ---@type string[]
|
||||
local ret = {} ---@type snacks.picker.Extmark[]
|
||||
local selected = self.selected_map[self:select_key(item)] ~= nil
|
||||
|
||||
local selw = vim.api.nvim_strwidth(self.picker.opts.icons.ui.selected)
|
||||
parts[#parts + 1] = string.rep(" ", selw)
|
||||
if selected then
|
||||
ret[#ret + 1] = {
|
||||
virt_text = { {
|
||||
self.picker.opts.icons.ui.selected or parts[1],
|
||||
"SnacksPickerSelected",
|
||||
} },
|
||||
virt_text_pos = "overlay",
|
||||
line_hl_group = "SnacksPickerSelectedLine",
|
||||
col = 0,
|
||||
hl_mode = "combine",
|
||||
}
|
||||
-- Add selected and debug info
|
||||
local prefix = {} ---@type snacks.picker.Highlight[]
|
||||
vim.list_extend(prefix, Snacks.picker.format.selected(item, self.picker))
|
||||
if self.picker.opts.debug.scores then
|
||||
vim.list_extend(prefix, Snacks.picker.format.debug(item, self.picker))
|
||||
end
|
||||
local text, extmarks = Snacks.picker.highlight.to_text(prefix)
|
||||
|
||||
-- Add the formatted item
|
||||
local line = self.picker.format(item, self.picker)
|
||||
while #line > 0 and type(line[#line][1]) == "string" and line[#line][1]:find("^%s*$") do
|
||||
table.remove(line)
|
||||
end
|
||||
local line_text, line_extmarks = Snacks.picker.highlight.to_text(line, { offset = #text })
|
||||
vim.list_extend(extmarks, line_extmarks)
|
||||
text = text .. line_text
|
||||
|
||||
if self.picker.opts.debug.scores then
|
||||
local score = item.score
|
||||
if not self.picker.matcher.sorting then
|
||||
score = self.picker.matcher.DEFAULT_SCORE
|
||||
if item.score_add then
|
||||
score = score + item.score_add
|
||||
end
|
||||
if item.score_mul then
|
||||
score = score * item.score_mul
|
||||
-- Highlight match positions for field patterns
|
||||
local fields = self.matcher:fields()
|
||||
for _, extmark in ipairs(extmarks) do
|
||||
if extmark.col and extmark.end_col and extmark.field and vim.tbl_contains(fields, extmark.field) then
|
||||
local field = extmark.field --[[@as string]]
|
||||
---@type snacks.picker.Item
|
||||
local it = {
|
||||
idx = 1,
|
||||
score = 0,
|
||||
file = item.file,
|
||||
text = "",
|
||||
}
|
||||
it[field] = text:sub(extmark.col + 1, extmark.end_col)
|
||||
local positions = self.matcher:positions(it)
|
||||
for _, pos in ipairs(positions[field] or {}) do
|
||||
table.insert(extmarks, {
|
||||
col = pos - 1 + extmark.col,
|
||||
end_col = pos + extmark.col,
|
||||
hl_group = "SnacksPickerMatch",
|
||||
})
|
||||
end
|
||||
end
|
||||
local score_str = ("%.2f "):format(score)
|
||||
parts[#parts + 1] = score_str
|
||||
ret[#ret + 1] = {
|
||||
col = selw,
|
||||
end_col = selw + vim.api.nvim_strwidth(score_str),
|
||||
hl_group = "Number",
|
||||
}
|
||||
selw = selw + vim.api.nvim_strwidth(score_str)
|
||||
end
|
||||
|
||||
local col = selw
|
||||
for _, text in ipairs(line) do
|
||||
if type(text[1]) == "string" then
|
||||
---@cast text snacks.picker.Text
|
||||
if text.virtual then
|
||||
table.insert(ret, {
|
||||
col = col,
|
||||
virt_text = { { text[1], text[2] } },
|
||||
virt_text_pos = "overlay",
|
||||
hl_mode = "combine",
|
||||
})
|
||||
parts[#parts + 1] = string.rep(" ", vim.api.nvim_strwidth(text[1]))
|
||||
else
|
||||
table.insert(ret, {
|
||||
col = col,
|
||||
end_col = col + #text[1],
|
||||
hl_group = text[2],
|
||||
})
|
||||
parts[#parts + 1] = text[1]
|
||||
end
|
||||
col = col + #parts[#parts]
|
||||
else
|
||||
text = vim.deepcopy(text)
|
||||
---@cast text snacks.picker.Extmark
|
||||
-- fix extmark col and end_col
|
||||
text.col = text.col + selw
|
||||
if text.end_col then
|
||||
text.end_col = text.end_col + selw
|
||||
end
|
||||
table.insert(ret, text)
|
||||
end
|
||||
end
|
||||
local str = table.concat(parts, ""):gsub("\n", " ")
|
||||
local positions = self.matcher:positions({ text = str:gsub("%s*$", ""), idx = 1, score = 0 })
|
||||
for _, pos in ipairs(positions) do
|
||||
table.insert(ret, {
|
||||
-- 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
|
||||
table.insert(extmarks, {
|
||||
col = pos - 1,
|
||||
end_col = pos,
|
||||
hl_group = "SnacksPickerMatch",
|
||||
})
|
||||
end
|
||||
return str, ret
|
||||
return text, extmarks
|
||||
end
|
||||
|
||||
---@param item snacks.picker.Item
|
||||
|
@ -424,6 +393,7 @@ function M:_render(item, row)
|
|||
local col = extmark.col
|
||||
extmark.col = nil
|
||||
extmark.row = nil
|
||||
extmark.field = nil
|
||||
vim.api.nvim_buf_set_extmark(self.win.buf, ns, row - 1, col, extmark)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -262,23 +262,38 @@ function M:match(item)
|
|||
return score
|
||||
end
|
||||
|
||||
--- Returns the fields that are used in the pattern.
|
||||
---@return string[]
|
||||
function M:fields()
|
||||
local ret = {} ---@type table<string,boolean>
|
||||
for _, any in ipairs(self.mods) do
|
||||
for _, mods in ipairs(any) do
|
||||
ret[mods.field or "text"] = true
|
||||
end
|
||||
end
|
||||
return vim.tbl_keys(ret)
|
||||
end
|
||||
|
||||
--- Returns the positions of the matched pattern in the item.
|
||||
--- All search patterns are combined with OR.
|
||||
---@param item snacks.picker.Item
|
||||
function M:positions(item)
|
||||
local all = {} ---@type snacks.picker.matcher.Mods[]
|
||||
local ret = {} ---@type number[]
|
||||
local ret = {} ---@type table<string,number[]>
|
||||
for _, any in ipairs(self.mods) do
|
||||
vim.list_extend(all, any)
|
||||
end
|
||||
for _, mods in ipairs(all) do
|
||||
local _, from, to, str = self:_match(item, mods)
|
||||
if from and to and str then
|
||||
local field = mods.field or "text"
|
||||
ret[field] = ret[field] or {}
|
||||
local pos = ret[field]
|
||||
if mods.fuzzy then
|
||||
vim.list_extend(ret, self:fuzzy_positions(str, mods.chars, from))
|
||||
vim.list_extend(pos, self:fuzzy_positions(str, mods.chars, from))
|
||||
else
|
||||
for c = from, to do
|
||||
ret[#ret + 1] = c
|
||||
pos[#pos + 1] = c
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,15 +43,15 @@ function M.filename(item, picker)
|
|||
local dir, file = path:match("^(.*)/(.+)$")
|
||||
if file and dir then
|
||||
if picker.opts.formatters.file.filename_first then
|
||||
ret[#ret + 1] = { file, "SnacksPickerFile" }
|
||||
ret[#ret + 1] = { file, "SnacksPickerFile", field = "file" }
|
||||
ret[#ret + 1] = { " " }
|
||||
ret[#ret + 1] = { dir, "SnacksPickerDir" }
|
||||
ret[#ret + 1] = { dir, "SnacksPickerDir", field = "file" }
|
||||
else
|
||||
ret[#ret + 1] = { dir .. "/", "SnacksPickerDir" }
|
||||
ret[#ret + 1] = { file, "SnacksPickerFile" }
|
||||
ret[#ret + 1] = { dir .. "/", "SnacksPickerDir", field = "file" }
|
||||
ret[#ret + 1] = { file, "SnacksPickerFile", field = "file" }
|
||||
end
|
||||
else
|
||||
ret[#ret + 1] = { path, "SnacksPickerFile" }
|
||||
ret[#ret + 1] = { path, "SnacksPickerFile", field = "file" }
|
||||
end
|
||||
if item.pos then
|
||||
ret[#ret + 1] = { ":", "SnacksPickerDelim" }
|
||||
|
@ -387,4 +387,29 @@ function M.buffer(item, picker)
|
|||
return ret
|
||||
end
|
||||
|
||||
function M.selected(item, picker)
|
||||
local selected = picker.list:is_selected(item)
|
||||
local icon = picker.opts.icons.ui.selected
|
||||
local icon_width = vim.api.nvim_strwidth(icon)
|
||||
local ret = {} ---@type snacks.picker.Highlight[]
|
||||
ret[#ret + 1] = { selected and icon or string.rep(" ", icon_width), "SnacksPickerSelected", virtual = true }
|
||||
return ret
|
||||
end
|
||||
|
||||
function M.debug(item, picker)
|
||||
local score = item.score
|
||||
if not picker.matcher.sorting then
|
||||
score = picker.matcher.DEFAULT_SCORE
|
||||
if item.score_add then
|
||||
score = score + item.score_add
|
||||
end
|
||||
if item.score_mul then
|
||||
score = score * item.score_mul
|
||||
end
|
||||
end
|
||||
local ret = {} ---@type snacks.picker.Highlight[]
|
||||
ret[#ret + 1] = { ("%.2f "):format(score), "Number" }
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
@ -175,4 +175,46 @@ function M.winhl(prefix, links)
|
|||
return table.concat(ret, ",")
|
||||
end
|
||||
|
||||
---@param line snacks.picker.Highlight[]
|
||||
---@param opts? {offset?:number}
|
||||
function M.to_text(line, opts)
|
||||
local offset = opts and opts.offset or 0
|
||||
local ret = {} ---@type snacks.picker.Extmark[]
|
||||
local col = offset
|
||||
local parts = {} ---@type string[]
|
||||
for _, text in ipairs(line) do
|
||||
if type(text[1]) == "string" then
|
||||
---@cast text snacks.picker.Text
|
||||
if text.virtual then
|
||||
table.insert(ret, {
|
||||
col = col,
|
||||
virt_text = { { text[1], text[2] } },
|
||||
virt_text_pos = "overlay",
|
||||
hl_mode = "combine",
|
||||
})
|
||||
parts[#parts + 1] = string.rep(" ", vim.api.nvim_strwidth(text[1]))
|
||||
else
|
||||
table.insert(ret, {
|
||||
col = col,
|
||||
end_col = col + #text[1],
|
||||
hl_group = text[2],
|
||||
field = text.field,
|
||||
})
|
||||
parts[#parts + 1] = text[1]
|
||||
end
|
||||
col = col + #parts[#parts]
|
||||
else
|
||||
text = vim.deepcopy(text)
|
||||
---@cast text snacks.picker.Extmark
|
||||
-- fix extmark col and end_col
|
||||
text.col = text.col + offset
|
||||
if text.end_col then
|
||||
text.end_col = text.end_col + offset
|
||||
end
|
||||
table.insert(ret, text)
|
||||
end
|
||||
end
|
||||
return table.concat(parts), ret
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue