mirror of
https://github.com/zoriya/telescope.nvim.git
synced 2026-08-16 02:45:09 +00:00
feat(fps): Add refresh style display for telescope
This commit is contained in:
@@ -1,244 +0,0 @@
|
||||
local assert = require "luassert"
|
||||
local builtin = require "telescope.builtin"
|
||||
local log = require "telescope.log"
|
||||
|
||||
local Job = require "plenary.job"
|
||||
local Path = require "plenary.path"
|
||||
|
||||
local tester = {}
|
||||
|
||||
tester.debug = false
|
||||
|
||||
local replace_terms = function(input)
|
||||
return vim.api.nvim_replace_termcodes(input, true, false, true)
|
||||
end
|
||||
|
||||
local nvim_feed = function(text, feed_opts)
|
||||
feed_opts = feed_opts or "m"
|
||||
|
||||
vim.api.nvim_feedkeys(text, feed_opts, true)
|
||||
end
|
||||
|
||||
local writer = function(val)
|
||||
if type(val) == "table" then
|
||||
val = vim.json.encode(val) .. "\n"
|
||||
end
|
||||
|
||||
if tester.debug then
|
||||
print(val)
|
||||
else
|
||||
io.stderr:write(val)
|
||||
end
|
||||
end
|
||||
|
||||
local execute_test_case = function(location, key, spec)
|
||||
local ok, actual = pcall(spec[2])
|
||||
|
||||
if not ok then
|
||||
writer {
|
||||
location = "Error: " .. location,
|
||||
case = key,
|
||||
expected = "To succeed and return: " .. tostring(spec[1]),
|
||||
actual = actual,
|
||||
|
||||
_type = spec._type,
|
||||
}
|
||||
else
|
||||
writer {
|
||||
location = location,
|
||||
case = key,
|
||||
expected = spec[1],
|
||||
actual = actual,
|
||||
|
||||
_type = spec._type,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local end_test_cases = function()
|
||||
vim.cmd [[qa!]]
|
||||
end
|
||||
|
||||
local invalid_test_case = function(k)
|
||||
writer { case = k, expected = "<a valid key>", actual = k }
|
||||
|
||||
end_test_cases()
|
||||
end
|
||||
|
||||
tester.picker_feed = function(input, test_cases)
|
||||
input = replace_terms(input)
|
||||
|
||||
return coroutine.wrap(function()
|
||||
for i = 1, #input do
|
||||
local char = input:sub(i, i)
|
||||
nvim_feed(char, "")
|
||||
|
||||
-- TODO: I'm not 100% sure this is a hack or not...
|
||||
-- it's possible these characters could still have an on_complete... but i'm not sure.
|
||||
if string.match(char, "%g") then
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
if tester.debug then
|
||||
vim.wait(200)
|
||||
end
|
||||
end
|
||||
|
||||
vim.wait(10)
|
||||
|
||||
if tester.debug then
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
vim.defer_fn(function()
|
||||
if test_cases.post_typed then
|
||||
for k, v in ipairs(test_cases.post_typed) do
|
||||
execute_test_case("post_typed", k, v)
|
||||
end
|
||||
end
|
||||
|
||||
nvim_feed(replace_terms "<CR>", "")
|
||||
end, 20)
|
||||
|
||||
vim.defer_fn(function()
|
||||
if test_cases.post_close then
|
||||
for k, v in ipairs(test_cases.post_close) do
|
||||
execute_test_case("post_close", k, v)
|
||||
end
|
||||
end
|
||||
|
||||
if tester.debug then
|
||||
return
|
||||
end
|
||||
|
||||
vim.defer_fn(end_test_cases, 20)
|
||||
end, 40)
|
||||
|
||||
coroutine.yield()
|
||||
end)
|
||||
end
|
||||
|
||||
local _VALID_KEYS = {
|
||||
post_typed = true,
|
||||
post_close = true,
|
||||
}
|
||||
|
||||
tester.builtin_picker = function(builtin_key, input, test_cases, opts)
|
||||
opts = opts or {}
|
||||
tester.debug = opts.debug or false
|
||||
|
||||
for k, _ in pairs(test_cases) do
|
||||
if not _VALID_KEYS[k] then
|
||||
return invalid_test_case(k)
|
||||
end
|
||||
end
|
||||
|
||||
opts.on_complete = {
|
||||
tester.picker_feed(input, test_cases),
|
||||
}
|
||||
|
||||
builtin[builtin_key](opts)
|
||||
end
|
||||
|
||||
local get_results_from_file = function(file)
|
||||
local j = Job:new {
|
||||
command = "nvim",
|
||||
args = {
|
||||
"--noplugin",
|
||||
"-u",
|
||||
"scripts/minimal_init.vim",
|
||||
"-c",
|
||||
string.format([[lua require("telescope.pickers._test")._execute("%s")]], file),
|
||||
},
|
||||
}
|
||||
|
||||
j:sync(10000)
|
||||
|
||||
local results = j:stderr_result()
|
||||
local result_table = {}
|
||||
for _, v in ipairs(results) do
|
||||
table.insert(result_table, vim.json.decode(v))
|
||||
end
|
||||
|
||||
return result_table
|
||||
end
|
||||
|
||||
local asserters = {
|
||||
_default = assert.are.same,
|
||||
|
||||
are = assert.are.same,
|
||||
are_not = assert.are_not.same,
|
||||
}
|
||||
|
||||
local check_results = function(results)
|
||||
-- TODO: We should get all the test cases here that fail, not just the first one.
|
||||
for _, v in ipairs(results) do
|
||||
local assertion = asserters[v._type or "default"]
|
||||
|
||||
assertion(v.expected, v.actual, string.format("Test Case: %s // %s", v.location, v.case))
|
||||
end
|
||||
end
|
||||
|
||||
tester.run_string = function(contents)
|
||||
local tempname = vim.fn.tempname()
|
||||
|
||||
contents = [[
|
||||
local tester = require('telescope.pickers._test')
|
||||
local helper = require('telescope.pickers._test_helpers')
|
||||
|
||||
helper.make_globals()
|
||||
]] .. contents
|
||||
|
||||
vim.fn.writefile(vim.split(contents, "\n"), tempname)
|
||||
local result_table = get_results_from_file(tempname)
|
||||
vim.fn.delete(tempname)
|
||||
|
||||
check_results(result_table)
|
||||
end
|
||||
|
||||
tester.run_file = function(filename)
|
||||
local file = "./lua/tests/pickers/" .. filename .. ".lua"
|
||||
|
||||
if not Path:new(file):exists() then
|
||||
assert.are.same("<An existing file>", file)
|
||||
end
|
||||
|
||||
local result_table = get_results_from_file(file)
|
||||
|
||||
check_results(result_table)
|
||||
end
|
||||
|
||||
tester.not_ = function(val)
|
||||
val._type = "are_not"
|
||||
return val
|
||||
end
|
||||
|
||||
tester._execute = function(filename)
|
||||
-- Important so that the outputs don't get mixed
|
||||
log.use_console = false
|
||||
|
||||
vim.cmd(string.format("luafile %s", filename))
|
||||
|
||||
local f = loadfile(filename)
|
||||
if not f then
|
||||
writer {
|
||||
location = "Error: " .. filename,
|
||||
case = filename,
|
||||
expected = "To succeed",
|
||||
actual = nil,
|
||||
}
|
||||
end
|
||||
|
||||
local ok, msg = pcall(f)
|
||||
if not ok then
|
||||
writer {
|
||||
location = "Error: " .. msg,
|
||||
case = msg,
|
||||
expected = msg,
|
||||
}
|
||||
end
|
||||
|
||||
end_test_cases()
|
||||
end
|
||||
|
||||
return tester
|
||||
@@ -1,56 +0,0 @@
|
||||
local test_helpers = {}
|
||||
|
||||
test_helpers.get_picker = function()
|
||||
local state = require "telescope.state"
|
||||
return state.get_status(vim.api.nvim_get_current_buf()).picker
|
||||
end
|
||||
|
||||
test_helpers.get_results_bufnr = function()
|
||||
local state = require "telescope.state"
|
||||
return state.get_status(vim.api.nvim_get_current_buf()).results_bufnr
|
||||
end
|
||||
|
||||
test_helpers.get_file = function()
|
||||
return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t")
|
||||
end
|
||||
|
||||
test_helpers.get_prompt = function()
|
||||
return vim.api.nvim_buf_get_lines(0, 0, -1, false)[1]
|
||||
end
|
||||
|
||||
test_helpers.get_results = function()
|
||||
return vim.api.nvim_buf_get_lines(test_helpers.get_results_bufnr(), 0, -1, false)
|
||||
end
|
||||
|
||||
test_helpers.get_best_result = function()
|
||||
local results = test_helpers.get_results()
|
||||
local picker = test_helpers.get_picker()
|
||||
|
||||
if picker.sorting_strategy == "ascending" then
|
||||
return results[1]
|
||||
else
|
||||
return results[#results]
|
||||
end
|
||||
end
|
||||
|
||||
test_helpers.get_selection = function()
|
||||
local state = require "telescope.state"
|
||||
return state.get_global_key "selected_entry"
|
||||
end
|
||||
|
||||
test_helpers.get_selection_value = function()
|
||||
return test_helpers.get_selection().value
|
||||
end
|
||||
|
||||
test_helpers.make_globals = function()
|
||||
GetFile = test_helpers.get_file -- luacheck: globals GetFile
|
||||
GetPrompt = test_helpers.get_prompt -- luacheck: globals GetPrompt
|
||||
|
||||
GetResults = test_helpers.get_results -- luacheck: globals GetResults
|
||||
GetBestResult = test_helpers.get_best_result -- luacheck: globals GetBestResult
|
||||
|
||||
GetSelection = test_helpers.get_selection -- luacheck: globals GetSelection
|
||||
GetSelectionValue = test_helpers.get_selection_value -- luacheck: globals GetSelectionValue
|
||||
end
|
||||
|
||||
return test_helpers
|
||||
@@ -2,8 +2,11 @@ local a = vim.api
|
||||
local log = require "telescope.log"
|
||||
local conf = require("telescope.config").values
|
||||
|
||||
local strdisplaywidth = require("plenary.strings").strdisplaywidth
|
||||
|
||||
local highlights = {}
|
||||
|
||||
local ns_telescope_matching = a.nvim_create_namespace "telescope_matching"
|
||||
local ns_telescope_selection = a.nvim_create_namespace "telescope_selection"
|
||||
local ns_telescope_multiselection = a.nvim_create_namespace "telescope_multiselection"
|
||||
local ns_telescope_entry = a.nvim_create_namespace "telescope_entry"
|
||||
@@ -14,10 +17,43 @@ Highlighter.__index = Highlighter
|
||||
function Highlighter:new(picker)
|
||||
return setmetatable({
|
||||
picker = picker,
|
||||
offset = picker._prefix_width,
|
||||
}, self)
|
||||
end
|
||||
|
||||
function Highlighter:hi_display(row, prefix, display_highlights)
|
||||
local DISPLAY_HIGHLIGHTS_PRIORITY = 110
|
||||
local SORTER_HIGHLIGHTS_PRIORITY = 120
|
||||
local SELECTION_HIGHLIGHTS_PRIORITY = 130
|
||||
|
||||
function Highlighter:highlight(row, opts)
|
||||
assert(row, "Must pass a row")
|
||||
|
||||
local picker = self.picker
|
||||
|
||||
local entry = opts.entry or picker:_get_entry_from_row(row)
|
||||
local prompt = opts.prompt or picker:_get_prompt()
|
||||
local is_selected = opts.is_selected or (picker._selection_row == row)
|
||||
local is_multi_selected = opts.is_multi_selected or picker:is_multi_selected(entry)
|
||||
|
||||
if is_selected then
|
||||
self:hi_selection(row)
|
||||
end
|
||||
|
||||
if not opts.skip_display then
|
||||
local display = opts.display
|
||||
local display_highlights = opts.display_highlights
|
||||
if not display then
|
||||
display, display_highlights = picker:_resolve_entry_display(entry)
|
||||
end
|
||||
|
||||
self:hi_display(row, display_highlights)
|
||||
self:hi_sorter(row, prompt, display)
|
||||
end
|
||||
|
||||
self:hi_multiselect(row, is_multi_selected)
|
||||
end
|
||||
|
||||
function Highlighter:hi_display(row, display_highlights)
|
||||
-- This is the bug that made my highlight fixes not work.
|
||||
-- We will leave the solution commented, so the test fails.
|
||||
if not display_highlights or vim.tbl_isempty(display_highlights) then
|
||||
@@ -25,95 +61,143 @@ function Highlighter:hi_display(row, prefix, display_highlights)
|
||||
end
|
||||
|
||||
local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr")
|
||||
if not a.nvim_buf_is_valid(results_bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
a.nvim_buf_clear_namespace(results_bufnr, ns_telescope_entry, row, row + 1)
|
||||
local len_prefix = #prefix
|
||||
|
||||
for _, hl_block in ipairs(display_highlights) do
|
||||
a.nvim_buf_add_highlight(
|
||||
results_bufnr,
|
||||
ns_telescope_entry,
|
||||
hl_block[2],
|
||||
row,
|
||||
len_prefix + hl_block[1][1],
|
||||
len_prefix + hl_block[1][2]
|
||||
)
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_entry, row, self.offset + hl_block[1][1], {
|
||||
end_col = self.offset + hl_block[1][2],
|
||||
hl_group = hl_block[2],
|
||||
priority = DISPLAY_HIGHLIGHTS_PRIORITY,
|
||||
strict = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function Highlighter:clear_display()
|
||||
function Highlighter:clear()
|
||||
if
|
||||
not self
|
||||
or not self.picker
|
||||
or not self.picker.results_bufnr
|
||||
or not vim.api.nvim_buf_is_valid(self.picker.results_bufnr)
|
||||
or not a.nvim_buf_is_valid(self.picker.results_bufnr)
|
||||
then
|
||||
return
|
||||
end
|
||||
|
||||
a.nvim_buf_clear_namespace(self.picker.results_bufnr, ns_telescope_entry, 0, -1)
|
||||
a.nvim_buf_clear_namespace(self.picker.results_bufnr, ns_telescope_matching, 0, -1)
|
||||
end
|
||||
|
||||
function Highlighter:hi_sorter(row, prompt, display)
|
||||
local picker = self.picker
|
||||
local sorter = picker.sorter
|
||||
if not picker.sorter or not picker.sorter.highlighter then
|
||||
return
|
||||
end
|
||||
|
||||
local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr")
|
||||
picker:highlight_one_row(results_bufnr, prompt, display, row)
|
||||
if not a.nvim_buf_is_valid(results_bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
local sorter_highlights = sorter:highlighter(prompt, display)
|
||||
|
||||
if sorter_highlights then
|
||||
for _, hl in ipairs(sorter_highlights) do
|
||||
local highlight, start, finish
|
||||
if type(hl) == "table" then
|
||||
highlight = hl.highlight or "TelescopeMatching"
|
||||
start = hl.start
|
||||
finish = hl.finish or hl.start
|
||||
elseif type(hl) == "number" then
|
||||
highlight = "TelescopeMatching"
|
||||
start = hl
|
||||
finish = hl
|
||||
else
|
||||
error "Invalid higlighter fn"
|
||||
end
|
||||
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_matching, row, start + self.offset - 1, {
|
||||
end_col = self.offset + finish,
|
||||
hl_group = highlight,
|
||||
priority = SORTER_HIGHLIGHTS_PRIORITY,
|
||||
strict = false,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Highlighter:hi_selection(row, caret)
|
||||
caret = vim.F.if_nil(caret, "")
|
||||
function Highlighter:hi_selection(row)
|
||||
local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr")
|
||||
if not a.nvim_buf_is_valid(results_bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
a.nvim_buf_clear_namespace(results_bufnr, ns_telescope_selection, 0, -1)
|
||||
a.nvim_buf_add_highlight(results_bufnr, ns_telescope_selection, "TelescopeSelectionCaret", row, 0, #caret)
|
||||
|
||||
a.nvim_buf_set_extmark(
|
||||
results_bufnr,
|
||||
ns_telescope_selection,
|
||||
row,
|
||||
#caret,
|
||||
{ end_line = row + 1, hl_eol = conf.hl_result_eol, hl_group = "TelescopeSelection" }
|
||||
)
|
||||
-- If there isn't anything _on_ the line, then it's some edge case with
|
||||
-- loading the buffer or something like that.
|
||||
--
|
||||
-- We can just skip and we'll get the updates later.
|
||||
if a.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- TODO: Someone will complain about the highlighting here I'm sure.
|
||||
-- I don't know what to tell them except what are you doing w/ highlighting
|
||||
local caret = self.picker.selection_caret
|
||||
local offset = self.offset
|
||||
|
||||
-- Highlight the caret
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_selection, row, 0, {
|
||||
virt_text = { { caret, "TelescopeSelectionCaret" } },
|
||||
virt_text_pos = "overlay",
|
||||
end_col = offset,
|
||||
hl_group = "TelescopeSelectionCaret",
|
||||
priority = SELECTION_HIGHLIGHTS_PRIORITY,
|
||||
strict = true,
|
||||
})
|
||||
|
||||
-- Highlight the text after the caret
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_selection, row, offset, {
|
||||
end_line = row + 1,
|
||||
hl_eol = conf.hl_result_eol,
|
||||
hl_group = "TelescopeSelection",
|
||||
priority = SELECTION_HIGHLIGHTS_PRIORITY,
|
||||
})
|
||||
end
|
||||
|
||||
function Highlighter:hi_multiselect(row, is_selected)
|
||||
local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr")
|
||||
if not a.nvim_buf_is_valid(results_bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
a.nvim_buf_clear_namespace(results_bufnr, ns_telescope_multiselection, row, row + 1)
|
||||
|
||||
local line = a.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1]
|
||||
if not line then
|
||||
return
|
||||
end
|
||||
|
||||
if is_selected then
|
||||
vim.api.nvim_buf_add_highlight(results_bufnr, ns_telescope_multiselection, "TelescopeMultiSelection", row, 0, -1)
|
||||
if self.picker.multi_icon then
|
||||
local line = vim.api.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1]
|
||||
local pos = line:find(self.picker.multi_icon)
|
||||
if pos and pos <= math.max(#self.picker.selection_caret, #self.picker.entry_prefix) then
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
results_bufnr,
|
||||
ns_telescope_multiselection,
|
||||
"TelescopeMultiIcon",
|
||||
row,
|
||||
pos - 1,
|
||||
pos - 1 + #self.picker.multi_icon
|
||||
)
|
||||
end
|
||||
end
|
||||
else
|
||||
local existing_marks = vim.api.nvim_buf_get_extmarks(
|
||||
results_bufnr,
|
||||
ns_telescope_multiselection,
|
||||
{ row, 0 },
|
||||
{ row, -1 },
|
||||
{}
|
||||
)
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_multiselection, row, self.offset, {
|
||||
end_col = #line,
|
||||
hl_group = "TelescopeMultiSelection",
|
||||
})
|
||||
|
||||
-- This is still kind of weird to me, since it seems like I'm erasing stuff
|
||||
-- when I shouldn't... Perhaps it's about the gravity of the extmark?
|
||||
if #existing_marks > 0 then
|
||||
log.trace("Clearing highlight multi select row: ", row)
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(results_bufnr, ns_telescope_multiselection, row, row + 1)
|
||||
-- TEST WITH MULTI-BYTE CHARS
|
||||
if self.picker.multi_icon and self.offset > 0 then
|
||||
local icon = self.picker.multi_icon
|
||||
local cols = strdisplaywidth(icon)
|
||||
a.nvim_buf_set_extmark(results_bufnr, ns_telescope_multiselection, row, self.offset - cols, {
|
||||
end_col = self.offset,
|
||||
virt_text = { { self.picker.multi_icon, "TelescopeMultiIcon" } },
|
||||
virt_text_pos = "overlay",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
local M = {}
|
||||
|
||||
M.set_prompt = function(picker)
|
||||
self._current_prefix_hl_group = hl_group or nil
|
||||
|
||||
if self.prompt_prefix ~= "" then
|
||||
vim.api.nvim_buf_add_highlight(
|
||||
self.prompt_bufnr,
|
||||
ns_telescope_prompt_prefix,
|
||||
self._current_prefix_hl_group or "TelescopePromptPrefix",
|
||||
0,
|
||||
0,
|
||||
strdisplaywidth(self.prompt_prefix)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -40,7 +40,7 @@ local scroll_calculators = {
|
||||
end,
|
||||
}
|
||||
|
||||
scroller.create = function(scroll_strategy, sorting_strategy)
|
||||
scroller.new = function(scroll_strategy, sorting_strategy)
|
||||
local range_fn = range_calculators[sorting_strategy]
|
||||
if not range_fn then
|
||||
error(debug.traceback("Unknown sorting strategy: " .. sorting_strategy))
|
||||
|
||||
Reference in New Issue
Block a user