feat(fps): Add refresh style display for telescope

This commit is contained in:
TJ DeVries
2022-06-11 14:57:10 -04:00
parent e6b69b1488
commit 26b357789b
33 changed files with 1550 additions and 1597 deletions
-244
View File
@@ -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
-56
View File
@@ -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
+136 -52
View File
@@ -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
+18
View File
@@ -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
+1 -1
View File
@@ -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))