feat: quickfix (#293)

* feat: quickfix (not implemented)

* [WIP]: Wed 09 Dec 2020 11:11:30 PM EST

* somewhat working linked list impl

* getting closer

* might be working

* might be working for real

* works and implemented basic example

* dont forget to close prompt

* fix descending and add more tests

* test fixes

* fix test

* more logging

* Fix some more tests

* Fix logging messing up tests

* fix: lint

* fix: multi select stuffs
This commit is contained in:
TJ DeVries
2021-01-11 13:29:37 -05:00
committed by GitHub
parent de80a9837c
commit 8783bea06e
19 changed files with 1152 additions and 369 deletions

View File

@@ -38,6 +38,16 @@ function actions.add_selection(prompt_bufnr)
current_picker:add_selection(current_picker:get_selection_row())
end
function actions.remove_selection(prompt_bufnr)
local current_picker = actions.get_current_picker(prompt_bufnr)
current_picker:remove_selection(current_picker:get_selection_row())
end
function actions.toggle_selection(prompt_bufnr)
local current_picker = actions.get_current_picker(prompt_bufnr)
current_picker:toggle_selection(current_picker:get_selection_row())
end
--- Get the current entry
function actions.get_selected_entry()
return state.get_global_key('selected_entry')
@@ -273,6 +283,45 @@ actions.git_staging_toggle = function(prompt_bufnr)
require('telescope.builtin').git_status()
end
local entry_to_qf = function(entry)
return {
bufnr = entry.bufnr,
filename = entry.filename,
lnum = entry.lnum,
col = entry.col,
text = entry.value,
}
end
actions.send_selected_to_qflist = function(prompt_bufnr)
local picker = actions.get_current_picker(prompt_bufnr)
local qf_entries = {}
for entry in pairs(picker.multi_select) do
table.insert(qf_entries, entry_to_qf(entry))
end
actions.close(prompt_bufnr)
vim.fn.setqflist(qf_entries, 'r')
vim.cmd [[copen]]
end
actions.send_to_qflist = function(prompt_bufnr)
local picker = actions.get_current_picker(prompt_bufnr)
local manager = picker.manager
local qf_entries = {}
for entry in manager:iter() do
table.insert(qf_entries, entry_to_qf(entry))
end
actions.close(prompt_bufnr)
vim.fn.setqflist(qf_entries, 'r')
vim.cmd [[copen]]
end
-- ==================================================
-- Transforms modules and sets the corect metatables.
-- ==================================================

View File

@@ -0,0 +1,222 @@
local LinkedList = {}
LinkedList.__index = LinkedList
function LinkedList:new(opts)
opts = opts or {}
local track_at = opts.track_at
return setmetatable({
size = 0,
head = false,
tail = false,
-- track_at: Track at can track a particular node
-- Use to keep a node tracked at a particular index
-- This greatly decreases looping for checking values at this location.
track_at = track_at,
_tracked_node = nil,
tracked = nil,
}, self)
end
function LinkedList:_increment()
self.size = self.size + 1
return self.size
end
local create_node = function(item)
return {
item = item
}
end
function LinkedList:append(item)
local final_size = self:_increment()
local node = create_node(item)
if not self.head then
self.head = node
end
if self.tail then
self.tail.next = node
node.prev = self.tail
end
self.tail = node
if self.track_at then
if final_size == self.track_at then
self.tracked = item
self._tracked_node = node
end
end
end
function LinkedList:prepend(item)
local final_size = self:_increment()
local node = create_node(item)
if not self.tail then
self.tail = node
end
if self.head then
self.head.prev = node
node.next = self.head
end
self.head = node
if self.track_at then
if final_size == self.track_at then
self._tracked_node = self.tail
elseif final_size > self.track_at then
self._tracked_node = self._tracked_node.prev
else
return
end
self.tracked = self._tracked_node.item
end
end
-- [a, b, c]
-- b.prev = a
-- b.next = c
--
-- a.next = b
-- c.prev = c
--
-- insert d after b
-- [a, b, d, c]
--
-- b.next = d
-- b.prev = a
--
-- Place "item" after "node" (which is at index `index`)
function LinkedList:place_after(index, node, item)
local new_node = create_node(item)
assert(node.prev ~= node)
assert(node.next ~= node)
local final_size = self:_increment()
-- Update tail to be the next node.
if self.tail == node then
self.tail = new_node
end
new_node.prev = node
new_node.next = node.next
node.next = new_node
if new_node.prev then
new_node.prev.next = new_node
end
if new_node.next then
new_node.next.prev = new_node
end
if self.track_at then
if index == self.track_at then
self._tracked_node = new_node
elseif index < self.track_at then
if final_size == self.track_at then
self._tracked_node = self.tail
elseif final_size > self.track_at then
self._tracked_node = self._tracked_node.prev
else
return
end
end
self.tracked = self._tracked_node.item
end
end
function LinkedList:place_before(index, node, item)
local new_node = create_node(item)
assert(node.prev ~= node)
assert(node.next ~= node)
local final_size = self:_increment()
-- Update head to be the node we are inserting.
if self.head == node then
self.head = new_node
end
new_node.prev = node.prev
new_node.next = node
node.prev = new_node
-- node.next = node.next
if new_node.prev then
new_node.prev.next = new_node
end
if new_node.next then
new_node.next.prev = new_node
end
if self.track_at then
if index == self.track_at - 1 then
self._tracked_node = node
elseif index < self.track_at then
if final_size == self.track_at then
self._tracked_node = self.tail
elseif final_size > self.track_at then
self._tracked_node = self._tracked_node.prev
else
return
end
end
self.tracked = self._tracked_node.item
end
end
-- Do you even do this in linked lists...?
-- function LinkedList:remove(item)
-- end
function LinkedList:iter()
local current_node = self.head
return function()
local node = current_node
if not node then
return nil
end
current_node = current_node.next
return node.item
end
end
function LinkedList:ipairs()
local index = 0
local current_node = self.head
return function()
local node = current_node
if not node then
return nil
end
current_node = current_node.next
index = index + 1
return index, node.item, node
end
end
return LinkedList

View File

@@ -34,7 +34,7 @@ function config.set_defaults(defaults)
set("sorting_strategy", "descending")
set("selection_strategy", "reset")
set("scroll_strategy", nil)
set("scroll_strategy", "cycle")
set("layout_strategy", "horizontal")
set("layout_defaults", {})
@@ -53,7 +53,7 @@ function config.set_defaults(defaults)
set("borderchars", { '', '', '', '', '', '', '', ''})
set("get_status_text", function(self)
return string.format("%s / %s", self.stats.processed - self.stats.filtered, self.stats.processed)
return string.format("%s / %s", (self.stats.processed or 0) - (self.stats.filtered or 0), self.stats.processed)
end)
-- Builtin configuration

View File

@@ -1,131 +1,192 @@
local log = require("telescope.log")
local LinkedList = require('telescope.algos.linked_list')
--[[
OK, new idea.
We can do linked list here.
To convert at the end to quickfix, just run the list.
...
start node
end node
if past loop of must have scores,
then we can just add to end node and shift end node to current node.
etc.
always inserts a row, because we clear everything before?
can also optimize by keeping worst acceptable score around.
--]]
local EntryManager = {}
EntryManager.__index = EntryManager
function EntryManager:new(max_results, set_entry, info)
function EntryManager:new(max_results, set_entry, info, id)
log.trace("Creating entry_manager...")
info = info or {}
info.looped = 0
info.inserted = 0
info.find_loop = 0
-- state contains list of
-- {
-- score = ...
-- line = ...
-- metadata ? ...
-- }
local entry_state = {}
-- { entry, score }
-- Stored directly in a table, accessed as [1], [2]
set_entry = set_entry or function() end
return setmetatable({
set_entry = set_entry,
max_results = max_results,
worst_acceptable_score = math.huge,
entry_state = entry_state,
id = id,
linked_states = LinkedList:new { track_at = max_results },
info = info,
num_results = function()
return #entry_state
end,
get_ordinal = function(em, index)
return em:get_entry(index).ordinal
end,
get_entry = function(_, index)
return (entry_state[index] or {}).entry
end,
get_score = function(_, index)
return (entry_state[index] or {}).score
end,
find_entry = function(_, entry)
if entry == nil then
return nil
end
for k, v in ipairs(entry_state) do
local existing_entry = v.entry
-- FIXME: This has the problem of assuming that display will not be the same for two different entries.
if existing_entry == entry then
return k
end
end
return nil
end,
_get_state = function()
return entry_state
end,
max_results = max_results,
set_entry = set_entry,
worst_acceptable_score = math.huge,
}, self)
end
function EntryManager:should_save_result(index)
return index <= self.max_results
function EntryManager:num_results()
return self.linked_states.size
end
function EntryManager:get_container(index)
local count = 0
for val in self.linked_states:iter() do
count = count + 1
if count == index then
return val
end
end
return {}
end
function EntryManager:get_entry(index)
return self:get_container(index)[1]
end
function EntryManager:get_score(index)
return self:get_container(index)[2]
end
function EntryManager:get_ordinal(index)
return self:get_entry(index).ordinal
end
function EntryManager:find_entry(entry)
local info = self.info
local count = 0
for container in self.linked_states:iter() do
count = count + 1
if container[1] == entry then
info.find_loop = info.find_loop + count
return count
end
end
info.find_loop = info.find_loop + count
return nil
end
function EntryManager:_update_score_from_tracked()
local linked = self.linked_states
if linked.tracked then
self.worst_acceptable_score = math.min(self.worst_acceptable_score, linked.tracked[2])
end
end
function EntryManager:_insert_container_before(picker, index, linked_node, new_container)
self.linked_states:place_before(index, linked_node, new_container)
self.set_entry(picker, index, new_container[1], new_container[2], true)
self:_update_score_from_tracked()
end
function EntryManager:_insert_container_after(picker, index, linked_node, new_container)
self.linked_states:place_after(index, linked_node, new_container)
self.set_entry(picker, index, new_container[1], new_container[2], true)
self:_update_score_from_tracked()
end
function EntryManager:_append_container(picker, new_container, should_update)
self.linked_states:append(new_container)
self.worst_acceptable_score = math.min(self.worst_acceptable_score, new_container[2])
if should_update then
self.set_entry(picker, self.linked_states.size, new_container[1], new_container[2])
end
end
function EntryManager:add_entry(picker, score, entry)
score = score or 0
if score >= self.worst_acceptable_score then
return
end
for index, item in ipairs(self.entry_state) do
self.info.looped = self.info.looped + 1
if item.score > score then
return self:insert(picker, index, {
score = score,
entry = entry,
})
end
-- Don't add results that are too bad.
if not self:should_save_result(index) then
if picker and picker.id then
if picker.request_number ~= self.id then
error("ADDING ENTRY TOO LATE!")
return
end
end
return self:insert(picker, {
score = score,
entry = entry,
})
end
score = score or 0
function EntryManager:insert(picker, index, entry)
if entry == nil then
entry = index
index = #self.entry_state + 1
local max_res = self.max_results
local worst_score = self.worst_acceptable_score
local size = self.linked_states.size
local info = self.info
info.maxed = info.maxed or 0
local new_container = { entry, score, }
-- Short circuit for bad scores -- they never need to be displayed.
-- Just save them and we'll deal with them later.
if score >= worst_score then
return self.linked_states:append(new_container)
end
-- To insert something, we place at the next available index (or specified index)
-- and then shift all the corresponding items one place.
local next_entry, last_score
repeat
self.info.inserted = self.info.inserted + 1
next_entry = self.entry_state[index]
self.set_entry(picker, index, entry.entry, entry.score)
self.entry_state[index] = entry
last_score = entry.score
index = index + 1
entry = next_entry
until not next_entry or not self:should_save_result(index)
if not self:should_save_result(index) then
self.worst_acceptable_score = last_score
-- Short circuit for first entry.
if size == 0 then
self.linked_states:prepend(new_container)
self.set_entry(picker, 1, entry, score)
return
end
for index, container, node in self.linked_states:ipairs() do
info.looped = info.looped + 1
if container[2] > score then
-- print("Inserting: ", picker, index, node, new_container)
return self:_insert_container_before(picker, index, node, new_container)
end
-- Don't add results that are too bad.
if index >= max_res then
info.maxed = info.maxed + 1
return self:_append_container(picker, new_container, false)
end
end
if self.linked_states.size >= max_res then
self.worst_acceptable_score = math.min(self.worst_acceptable_score, score)
end
return self:_insert_container_after(picker, size + 1, self.linked_states.tail, new_container)
end
function EntryManager:iter()
return coroutine.wrap(function()
for val in self.linked_states:iter() do
coroutine.yield(val[1])
end
end)
end
return EntryManager

View File

@@ -221,14 +221,17 @@ function Picker:is_done()
end
function Picker:clear_extra_rows(results_bufnr)
if self:is_done() then return end
if self:is_done() then
log.trace("Not clearing due to being already complete")
return
end
if not vim.api.nvim_buf_is_valid(results_bufnr) then
log.debug("Invalid results_bufnr for clearing:", results_bufnr)
return
end
local worst_line
local worst_line, ok, msg
if self.sorting_strategy == 'ascending' then
local num_results = self.manager:num_results()
worst_line = self.max_results - num_results
@@ -237,7 +240,7 @@ function Picker:clear_extra_rows(results_bufnr)
return
end
pcall(vim.api.nvim_buf_set_lines, results_bufnr, num_results, self.max_results, false, {})
ok, msg = pcall(vim.api.nvim_buf_set_lines, results_bufnr, num_results, -1, false, {})
else
worst_line = self:get_row(self.manager:num_results())
if worst_line <= 0 then
@@ -245,10 +248,14 @@ function Picker:clear_extra_rows(results_bufnr)
end
local empty_lines = utils.repeated_table(worst_line, "")
pcall(vim.api.nvim_buf_set_lines, results_bufnr, 0, worst_line, false, empty_lines)
ok, msg = pcall(vim.api.nvim_buf_set_lines, results_bufnr, 0, worst_line, false, empty_lines)
end
log.trace("Clearing:", worst_line)
if not ok then
log.debug(msg)
end
log.debug("Clearing:", worst_line)
end
function Picker:highlight_displayed_rows(results_bufnr, prompt)
@@ -296,6 +303,9 @@ function Picker:highlight_one_row(results_bufnr, prompt, display, row)
)
end
end
local entry = self.manager:get_entry(self:get_index(row))
self.highlighter:hi_multiselect(row, entry)
end
function Picker:can_select_row(row)
@@ -416,7 +426,9 @@ function Picker:find()
local debounced_status = debounce.throttle_leading(update_status, 50)
self.request_number = 0
local on_lines = function(_, _, _, first_line, last_line)
self.request_number = self.request_number + 1
self:_reset_track()
if not vim.api.nvim_buf_is_valid(prompt_bufnr) then
@@ -424,6 +436,9 @@ function Picker:find()
return
end
if not first_line then first_line = 0 end
if not last_line then last_line = 1 end
if first_line > 0 or last_line > 1 then
log.debug("ON_LINES: Bad range", first_line, last_line)
return
@@ -434,13 +449,8 @@ function Picker:find()
self.sorter:_start(prompt)
end
-- TODO: Statusbar possibilities here.
-- vim.api.nvim_buf_set_virtual_text(prompt_bufnr, 0, 1, { {"hello", "Error"} }, {})
-- TODO: Entry manager should have a "bulk" setter. This can prevent a lot of redraws from display
self.manager = EntryManager:new(self.max_results, self.entry_adder)
-- self.manager = EntryManager:new(self.max_results, self.entry_adder, self.stats)
self.manager = EntryManager:new(self.max_results, self.entry_adder, self.stats, self.request_number)
local process_result = function(entry)
if self:is_done() then return end
@@ -690,24 +700,48 @@ end
function Picker:add_selection(row)
local entry = self.manager:get_entry(self:get_index(row))
self.multi_select[entry] = true
self.highlighter:hi_multiselect(row, entry)
end
function Picker:add_selection(row)
local entry = self.manager:get_entry(self:get_index(row))
self.multi_select[entry] = true
self.highlighter:hi_multiselect(row, entry)
end
function Picker:remove_selection(row)
local entry = self.manager:get_entry(self:get_index(row))
self.multi_select[entry] = nil
self.highlighter:hi_multiselect(row, entry)
end
function Picker:toggle_selection(row)
local entry = self.manager:get_entry(self:get_index(row))
if self.multi_select[entry] then
self:remove_selection(row)
else
self:add_selection(row)
end
end
function Picker:display_multi_select(results_bufnr)
if true then return end
-- for entry, _ in pairs(self.multi_select) do
-- local index = self.manager:find_entry(entry)
-- if index then
-- vim.api.nvim_buf_add_highlight(
-- results_bufnr,
-- ns_telescope_selection,
-- "TelescopeMultiSelection",
-- self:get_row(index),
-- 0,
-- -1
-- )
-- end
-- end
for entry, _ in pairs(self.multi_select) do
local index = self.manager:find_entry(entry)
if index then
vim.api.nvim_buf_add_highlight(
results_bufnr,
a.nvim_create_namespace('telescope_selection'),
"TelescopeMultiSelection",
self:get_row(index),
0,
-1
)
end
end
end
function Picker:reset_selection()
@@ -718,25 +752,30 @@ function Picker:reset_selection()
end
function Picker:set_selection(row)
-- TODO: Loop around behavior?
-- TODO: Scrolling past max results
row = self.scroller(self.max_results, self.manager:num_results(), row)
if not self:can_select_row(row) then
-- If the current selected row exceeds number of currently displayed
-- elements we have to reset it. Affectes sorting_strategy = 'row'.
-- elements we have to reset it. Affects sorting_strategy = 'row'.
if not self:can_select_row(self:get_selection_row()) then
row = self:get_row(self.manager:num_results())
else
log.debug("Cannot select row:", row, self.manager:num_results(), self.max_results)
log.trace("Cannot select row:", row, self.manager:num_results(), self.max_results)
return
end
end
-- local entry = self.manager:get_entry(self.max_results - row + 1)
local entry = self.manager:get_entry(self:get_index(row))
local status = state.get_status(self.prompt_bufnr)
local results_bufnr = status.results_bufnr
local results_bufnr = self.results_bufnr
if row > a.nvim_buf_line_count(results_bufnr) then
error(string.format(
"Should not be possible to get row this large %s %s",
row,
a.nvim_buf_line_count(results_bufnr)
))
end
state.set_global_key("selected_entry", entry)
@@ -764,6 +803,7 @@ function Picker:set_selection(row)
self.highlighter:hi_display(self._selection_row, ' ', display_highlights)
self.highlighter:hi_sorter(self._selection_row, prompt, display)
self.highlighter:hi_multiselect(self._selection_row, self._selection_entry)
end
end
@@ -785,9 +825,7 @@ function Picker:set_selection(row)
self.highlighter:hi_selection(row, caret)
self.highlighter:hi_display(row, ' ', display_highlights)
self.highlighter:hi_sorter(row, prompt, display)
-- TODO: Actually implement this for real TJ, don't leave around half implemented code plz :)
-- self:display_multi_select(results_bufnr)
self.highlighter:hi_multiselect(row, entry)
end)
if not set_ok then
@@ -814,7 +852,7 @@ function Picker:set_selection(row)
end
function Picker:entry_adder(index, entry, score)
function Picker:entry_adder(index, entry, score, insert)
local row = self:get_row(index)
-- If it's less than 0, then we don't need to show it at all.
@@ -838,17 +876,39 @@ function Picker:entry_adder(index, entry, score)
self:_increment("displayed")
-- TODO: Don't need to schedule this if we schedule the adder.
local offset = insert and 0 or 1
local scheduled_request = self.request_number
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(self.results_bufnr) then
log.debug("ON_ENTRY: Invalid buffer")
return
end
local set_ok = pcall(vim.api.nvim_buf_set_lines, self.results_bufnr, row, row + 1, false, {display})
if self.request_number ~= scheduled_request then
log.debug("Cancelling request number:", self.request_number, " // ", scheduled_request)
return
end
local line_count = vim.api.nvim_buf_line_count(self.results_bufnr)
if row > line_count then
return
end
if insert then
if self.sorting_strategy == 'descending' then
vim.api.nvim_buf_set_lines(self.results_bufnr, 0, 1, false, {})
end
end
local set_ok, msg = pcall(vim.api.nvim_buf_set_lines, self.results_bufnr, row, row + offset, false, {display})
if set_ok and display_highlights then
self.highlighter:hi_display(row, prefix, display_highlights)
end
if not set_ok then
log.debug("Failed to set lines...", msg)
end
-- This pretty much only fails when people leave newlines in their results.
-- So we'll clean it up for them if it fails.
if not set_ok and display:find("\n") then
@@ -893,7 +953,7 @@ function Picker:_track(key, func, ...)
end
function Picker:_increment(key)
self.stats[key] = self.stats[key] + 1
self.stats[key] = (self.stats[key] or 0) + 1
end

View File

@@ -1,10 +1,14 @@
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
@@ -15,7 +19,53 @@ local nvim_feed = function(text, feed_opts)
vim.api.nvim_feedkeys(text, feed_opts, true)
end
tester.picker_feed = function(input, test_cases, debug)
local writer = function(val)
if type(val) == "table" then
val = vim.fn.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()
@@ -28,75 +78,66 @@ tester.picker_feed = function(input, test_cases, debug)
if string.match(char, "%g") then
coroutine.yield()
end
if tester.debug then
vim.wait(200)
end
end
vim.wait(10, function() end)
vim.wait(10)
local timer = vim.loop.new_timer()
timer:start(20, 0, vim.schedule_wrap(function()
if test_cases.post_close then
for k, v in ipairs(test_cases.post_close) do
io.stderr:write(vim.fn.json_encode({ case = k, expected = v[1], actual = v[2]() }))
io.stderr:write("\n")
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
if debug then
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(function()
vim.cmd [[qa!]]
end, 10)
end))
vim.defer_fn(end_test_cases, 20)
end, 40)
if not debug then
vim.schedule(function()
if test_cases.post_typed then
for k, v in ipairs(test_cases.post_typed) do
io.stderr:write(vim.fn.json_encode({ case = k, expected = v[1], actual = v[2]() }))
io.stderr:write("\n")
end
end
nvim_feed(replace_terms("<CR>"), "")
end)
end
coroutine.yield()
end)
end
-- local test_cases = {
-- post_typed = {
-- },
-- post_close = {
-- { "README.md", function() return "README.md" end },
-- },
-- }
local _VALID_KEYS = {
post_typed = true,
post_close = true,
}
tester.builtin_picker = function(key, input, test_cases, opts)
tester.builtin_picker = function(builtin_key, input, test_cases, opts)
opts = opts or {}
local debug = opts.debug or false
tester.debug = opts.debug or false
for k, _ in pairs(test_cases) do
if not _VALID_KEYS[k] then
-- TODO: Make an error type for the json protocol.
io.stderr:write(vim.fn.json_encode({ case = k, expected = '<a valid key>', actual = k }))
io.stderr:write("\n")
vim.cmd [[qa!]]
return invalid_test_case(k)
end
end
opts.on_complete = {
tester.picker_feed(input, test_cases, debug)
tester.picker_feed(input, test_cases),
}
builtin[key](opts)
builtin[builtin_key](opts)
end
local get_results_from_file = function(file)
@@ -107,11 +148,14 @@ local get_results_from_file = function(file)
'-u',
'scripts/minimal_init.vim',
'-c',
'luafile ' .. file
string.format(
[[lua require("telescope.pickers._test")._execute("%s")]],
file
),
},
}
j:sync()
j:sync(1000)
local results = j:stderr_result()
local result_table = {}
@@ -122,10 +166,27 @@ local get_results_from_file = function(file)
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
assert.are.same(v.expected, v.actual)
local assertion = asserters[v._type or 'default']
assertion(
v.expected,
v.actual,
string.format("Test Case: %s // %s",
v.location,
v.case)
)
end
end
@@ -144,14 +205,52 @@ tester.run_string = function(contents)
vim.fn.delete(tempname)
check_results(result_table)
-- assert.are.same(result_table.expected, result_table.actual)
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)
assert.are.same(result_table.expected, result_table.actual)
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

View File

@@ -22,9 +22,15 @@ test_helpers.get_results = function()
return vim.api.nvim_buf_get_lines(test_helpers.get_results_bufnr(), 0, -1, false)
end
test_helpers.get_last_result = function()
test_helpers.get_best_result = function()
local results = test_helpers.get_results()
return results[#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()
@@ -41,7 +47,7 @@ test_helpers.make_globals = function()
GetPrompt = test_helpers.get_prompt -- luacheck: globals GetPrompt
GetResults = test_helpers.get_results -- luacheck: globals GetResults
GetLastResult = test_helpers.get_last_result -- luacheck: globals GetLastResult
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

View File

@@ -3,6 +3,7 @@ local a = vim.api
local highlights = {}
local ns_telescope_selection = a.nvim_create_namespace('telescope_selection')
local ns_telescope_multiselection = a.nvim_create_namespace('telescope_mulitselection')
local ns_telescope_entry = a.nvim_create_namespace('telescope_entry')
local Highlighter = {}
@@ -75,6 +76,28 @@ function Highlighter:hi_selection(row, caret)
)
end
function Highlighter:hi_multiselect(row, entry)
local results_bufnr = assert(self.picker.results_bufnr, "Must have a results bufnr")
if self.picker.multi_select[entry] then
vim.api.nvim_buf_add_highlight(
results_bufnr,
ns_telescope_multiselection,
"TelescopeMultiSelection",
row,
0,
-1
)
else
vim.api.nvim_buf_clear_namespace(
results_bufnr,
ns_telescope_multiselection,
row,
row + 1
)
end
end
highlights.new = function(...)
return Highlighter:new(...)
end

View File

@@ -1,56 +1,75 @@
local scroller = {}
local calc_count_fn = function(sorting_strategy)
if sorting_strategy == 'ascending' then
return function(a, b) return math.min(a, b) end
else
return function(a, b, row)
if a == b or not row then
return math.max(a, b)
else
local x = a - b
if row < x then
return math.max(a, b) - 1, true
elseif row == a then
return x, true
else
return math.max(a, b)
end
local range_calculators = {
ascending = function(max_results, num_results)
return 0, math.min(max_results, num_results)
end,
descending = function(max_results, num_results)
return math.max(max_results - num_results, 0), max_results
end,
}
local scroll_calculators = {
cycle = function(range_fn)
return function(max_results, num_results, row)
local start, finish = range_fn(max_results, num_results)
if row >= finish then
return start
elseif row < start then
return finish - 1
end
return row
end
end,
limit = function(range_fn)
return function(max_results, num_results, row)
local start, finish = range_fn(max_results, num_results)
if row >= finish then
return finish - 1
elseif row < start then
return start
end
return row
end
end,
}
scroller.create = 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))
end
end
scroller.create = function(strategy, sorting_strategy)
local calc_count = calc_count_fn(sorting_strategy)
local scroll_fn = scroll_calculators[scroll_strategy]
if not scroll_fn then
error(debug.traceback("Unknown scroll strategy: " .. (scroll_strategy or '')))
end
if strategy == 'cycle' then
return function(max_results, num_results, row)
local count, b = calc_count(max_results, num_results, row)
if b then return count end
local calculator = scroll_fn(range_fn)
return function(max_results, num_results, row)
local result = calculator(max_results, num_results, row)
if row >= count then
return 0
elseif row < 0 then
return count - 1
end
return row
if result < 0 then
error(string.format(
"Must never return a negative row: { result = %s, args = { %s %s %s } }",
result, max_results, num_results, row
))
end
elseif strategy == 'limit' or strategy == nil then
return function(max_results, num_results, row)
local count = calc_count(max_results, num_results)
if row >= count then
return count - 1
elseif row < 0 then
return 0
end
return row
if result >= max_results then
error(string.format(
"Must never exceed max results: { result = %s, args = { %s %s %s } }",
result, max_results, num_results, row
))
end
else
error("Unsupported strategy: " .. strategy)
return result
end
end

View File

@@ -0,0 +1,142 @@
local EntryManager = require('telescope.entry_manager')
local eq = assert.are.same
describe('process_result', function()
it('works with one entry', function()
local manager = EntryManager:new(5, nil)
manager:add_entry(nil, 1, "hello")
eq(1, manager:get_score(1))
end)
it('works with two entries', function()
local manager = EntryManager:new(5, nil)
manager:add_entry(nil, 1, "hello")
manager:add_entry(nil, 2, "later")
eq(2, manager.linked_states.size)
eq("hello", manager:get_entry(1))
eq("later", manager:get_entry(2))
end)
it('calls functions when inserting', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
assert(called_count == 0)
manager:add_entry(nil, 1, "hello")
assert(called_count == 1)
end)
it('calls functions when inserting twice', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
assert(called_count == 0)
manager:add_entry(nil, 1, "hello")
manager:add_entry(nil, 2, "world")
assert(called_count == 2)
end)
it('correctly sorts lower scores', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
manager:add_entry(nil, 5, "worse result")
manager:add_entry(nil, 2, "better result")
eq("better result", manager:get_entry(1))
eq("worse result", manager:get_entry(2))
eq(2, called_count)
end)
it('respects max results', function()
local called_count = 0
local manager = EntryManager:new(1, function() called_count = called_count + 1 end)
manager:add_entry(nil, 2, "better result")
manager:add_entry(nil, 5, "worse result")
eq("better result", manager:get_entry(1))
eq(1, called_count)
end)
it('should allow simple entries', function()
local manager = EntryManager:new(5)
local counts_executed = 0
manager:add_entry(nil, 1, setmetatable({}, {
__index = function(t, k)
local val = nil
if k == "ordinal" then
counts_executed = counts_executed + 1
-- This could be expensive, only call later
val = "wow"
end
rawset(t, k, val)
return val
end,
}))
eq("wow", manager:get_ordinal(1))
eq("wow", manager:get_ordinal(1))
eq("wow", manager:get_ordinal(1))
eq(1, counts_executed)
end)
it('should not loop a bunch', function()
local info = {}
local manager = EntryManager:new(5, nil, info)
manager:add_entry(nil, 4, "better result")
manager:add_entry(nil, 3, "better result")
manager:add_entry(nil, 2, "better result")
-- Loops once to find 3 < 4
-- Loops again to find 2 < 3
eq(2, info.looped)
end)
it('should not loop a bunch, part 2', function()
local info = {}
local manager = EntryManager:new(5, nil, info)
manager:add_entry(nil, 4, "better result")
manager:add_entry(nil, 2, "better result")
manager:add_entry(nil, 3, "better result")
-- Loops again to find 2 < 4
-- Loops once to find 3 > 2
-- but less than 4
eq(3, info.looped)
end)
it('should update worst score in all append case', function()
local manager = EntryManager:new(2, nil)
manager:add_entry(nil, 2, "result 2")
manager:add_entry(nil, 3, "result 3")
manager:add_entry(nil, 4, "result 4")
eq(3, manager.worst_acceptable_score)
end)
it('should update worst score in all prepend case', function()
local called_count = 0
local manager = EntryManager:new(2, function() called_count = called_count + 1 end)
manager:add_entry(nil, 5, "worse result")
manager:add_entry(nil, 4, "less worse result")
manager:add_entry(nil, 2, "better result")
-- Once for insert 5
-- Once for prepend 4
-- Once for prepend 2
eq(3, called_count)
eq("better result", manager:get_entry(1))
eq(4, manager.worst_acceptable_score)
end)
end)

View File

@@ -0,0 +1,133 @@
local LinkedList = require('telescope.algos.linked_list')
describe('LinkedList', function()
it('can create a list', function()
local l = LinkedList:new()
assert.are.same(0, l.size)
end)
it('can add a single entry to the list', function()
local l = LinkedList:new()
l:append('hello')
assert.are.same(1, l.size)
end)
it('can iterate over one item', function()
local l = LinkedList:new()
l:append('hello')
for val in l:iter() do
assert.are.same('hello', val)
end
end)
it('iterates in order', function()
local l = LinkedList:new()
l:append('hello')
l:append('world')
local x = {}
for val in l:iter() do
table.insert(x, val)
end
assert.are.same({'hello', 'world'}, x)
end)
it('iterates in order, for prepend', function()
local l = LinkedList:new()
l:prepend('world')
l:prepend('hello')
local x = {}
for val in l:iter() do
table.insert(x, val)
end
assert.are.same({'hello', 'world'}, x)
end)
it('iterates in order, for combo', function()
local l = LinkedList:new()
l:prepend('world')
l:prepend('hello')
l:append('last')
l:prepend('first')
local x = {}
for val in l:iter() do
table.insert(x, val)
end
assert.are.same({'first', 'hello', 'world', 'last'}, x)
assert.are.same(#x, l.size)
end)
it('has ipairs', function()
local l = LinkedList:new()
l:prepend('world')
l:prepend('hello')
l:append('last')
l:prepend('first')
local x = {}
for v in l:iter() do
table.insert(x, v)
end
assert.are.same({'first', 'hello', 'world', 'last'}, x)
local expected = {}
for i, v in ipairs(x) do
table.insert(expected, {i, v})
end
local actual = {}
for i, v in l:ipairs() do
table.insert(actual, {i, v})
end
assert.are.same(expected, actual)
end)
describe('track_at', function()
it('should update tracked when only appending', function()
local l = LinkedList:new { track_at = 2 }
l:append("first")
l:append("second")
l:append("third")
assert.are.same("second", l.tracked)
end)
it('should update tracked when first some prepend and then append', function()
local l = LinkedList:new { track_at = 2 }
l:prepend("first")
l:append("second")
l:append("third")
assert.are.same("second", l.tracked)
end)
it('should update when only prepending', function()
local l = LinkedList:new { track_at = 2 }
l:prepend("third")
l:prepend("second")
l:prepend("first")
assert.are.same("second", l.tracked)
end)
it('should update when lots of prepend and append', function()
local l = LinkedList:new { track_at = 2 }
l:prepend("third")
l:prepend("second")
l:prepend("first")
l:append("fourth")
l:prepend("zeroth")
assert.are.same("first", l.tracked)
end)
end)
end)

View File

@@ -2,6 +2,10 @@ require('plenary.reload').reload_module('telescope')
local tester = require('telescope.pickers._test')
local disp = function(val)
return vim.inspect(val, { newline = " ", indent = "" })
end
describe('builtin.find_files', function()
it('should find the readme', function()
tester.run_file('find_files__readme')
@@ -11,45 +15,76 @@ describe('builtin.find_files', function()
tester.run_file('find_files__with_ctrl_n')
end)
it('should not display devicons when disabled', function()
tester.run_string [[
tester.builtin_picker('find_files', 'README.md', {
post_typed = {
{ "> README.md", GetPrompt },
{ "> README.md", GetLastResult },
},
post_close = {
{ 'README.md', GetFile },
{ 'README.md', GetFile },
}
}, {
disable_devicons = true,
sorter = require('telescope.sorters').get_fzy_sorter(),
})
]]
end)
for _, configuration in ipairs {
{ sorting_strategy = 'descending', },
{ sorting_strategy = 'ascending', },
} do
it('should not display devicons when disabled: ' .. disp(configuration), function()
tester.run_string(string.format([[
local max_results = 5
it('use devicons, if it has it when enabled', function()
if not pcall(require, 'nvim-web-devicons') then
return
end
tester.builtin_picker('find_files', 'README.md', {
post_typed = {
{ "> README.md", GetPrompt },
{ "> README.md", GetBestResult },
},
post_close = {
{ 'README.md', GetFile },
{ 'README.md', GetFile },
}
}, vim.tbl_extend("force", {
disable_devicons = true,
sorter = require('telescope.sorters').get_fzy_sorter(),
results_height = max_results,
layout_strategy = 'center',
}, vim.fn.json_decode([==[%s]==])))
]], vim.fn.json_encode(configuration)))
end)
tester.run_string [[
tester.builtin_picker('find_files', 'README.md', {
post_typed = {
{ "> README.md", GetPrompt },
{ ">  README.md", GetLastResult }
},
post_close = {
{ 'README.md', GetFile },
{ 'README.md', GetFile },
}
}, {
disable_devicons = false,
sorter = require('telescope.sorters').get_fzy_sorter(),
})
]]
end)
it('should only save one line for ascending, but many for descending', function()
local expected
if configuration.sorting_strategy == 'descending' then
expected = 5
else
expected = 1
end
tester.run_string(string.format([[
tester.builtin_picker('find_files', 'README.md', {
post_typed = {
{ %s, function() return #GetResults() end },
},
}, vim.tbl_extend("force", {
disable_devicons = true,
sorter = require('telescope.sorters').get_fzy_sorter(),
results_height = 5,
layout_strategy = 'center',
}, vim.fn.json_decode([==[%s]==])))
]], expected, vim.fn.json_encode(configuration)))
end)
it('use devicons, if it has it when enabled', function()
if not pcall(require, 'nvim-web-devicons') then
return
end
tester.run_string(string.format([[
tester.builtin_picker('find_files', 'README.md', {
post_typed = {
{ "> README.md", GetPrompt },
{ ">  README.md", GetBestResult }
},
post_close = {
{ 'README.md', GetFile },
{ 'README.md', GetFile },
}
}, vim.tbl_extend("force", {
disable_devicons = false,
sorter = require('telescope.sorters').get_fzy_sorter(),
}, vim.fn.json_decode([==[%s]==])))
]], vim.fn.json_encode(configuration)))
end)
end
it('should find the readme, using lowercase', function()
tester.run_string [[

View File

@@ -0,0 +1,9 @@
require('plenary.reload').reload_module('telescope')
local tester = require('telescope.pickers._test')
describe('scrolling strategies', function()
it('should handle cycling for full list', function()
tester.run_file [[find_files__scrolling_descending_cycle]]
end)
end)

View File

@@ -99,7 +99,16 @@ describe('scroller', function()
it('should stay at current results when current results is less than max_results', function()
local current = 5
eq(current - 1, limit_scroller(max_results, current, 4))
eq(max_results - current, limit_scroller(max_results, current, 4))
end)
end)
describe('https://github.com/nvim-telescope/telescope.nvim/pull/293#issuecomment-751463224', function()
it('should handle having many more results than necessary', function()
local scroller = p_scroller.create('cycle', 'descending')
-- 23 112 23
eq(0, scroller(23, 112, 23))
end)
end)
end)

View File

@@ -4,8 +4,6 @@ local log = require('telescope.log')
log.level = 'info'
-- log.use_console = false
local EntryManager = require('telescope.entry_manager')
--[[
lua RELOAD('plenary'); require("plenary.test_harness"):test_directory("busted", "./tests/automated")
--]]
@@ -16,103 +14,6 @@ describe('Picker', function()
assert(true)
end)
end)
describe('process_result', function()
it('works with one entry', function()
local manager = EntryManager:new(5, nil)
manager:add_entry(nil, 1, "hello")
assert.are.same(1, manager:get_score(1))
end)
it('works with two entries', function()
local manager = EntryManager:new(5, nil)
manager:add_entry(nil, 1, "hello")
manager:add_entry(nil, 2, "later")
assert.are.same("hello", manager:get_entry(1))
assert.are.same("later", manager:get_entry(2))
end)
it('calls functions when inserting', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
assert(called_count == 0)
manager:add_entry(nil, 1, "hello")
assert(called_count == 1)
end)
it('calls functions when inserting twice', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
assert(called_count == 0)
manager:add_entry(nil, 1, "hello")
manager:add_entry(nil, 2, "world")
assert(called_count == 2)
end)
it('correctly sorts lower scores', function()
local called_count = 0
local manager = EntryManager:new(5, function() called_count = called_count + 1 end)
manager:add_entry(nil, 5, "worse result")
manager:add_entry(nil, 2, "better result")
assert.are.same("better result", manager:get_entry(1))
assert.are.same("worse result", manager:get_entry(2))
-- once to insert "worse"
-- once to insert "better"
-- and then to move "worse"
assert.are.same(3, called_count)
end)
it('respects max results', function()
local called_count = 0
local manager = EntryManager:new(1, function() called_count = called_count + 1 end)
manager:add_entry(nil, 2, "better result")
manager:add_entry(nil, 5, "worse result")
assert.are.same("better result", manager:get_entry(1))
assert.are.same(1, called_count)
end)
-- TODO: We should decide if we want to add this or not.
-- it('should handle no scores', function()
-- local manager = EntryManager:new(5, nil)
-- manager:add_entry(nil,
-- end)
it('should allow simple entries', function()
local manager = EntryManager:new(5)
local counts_executed = 0
manager:add_entry(nil, 1, setmetatable({}, {
__index = function(t, k)
local val = nil
if k == "ordinal" then
counts_executed = counts_executed + 1
-- This could be expensive, only call later
val = "wow"
end
rawset(t, k, val)
return val
end,
}))
assert.are.same("wow", manager:get_ordinal(1))
assert.are.same("wow", manager:get_ordinal(1))
assert.are.same("wow", manager:get_ordinal(1))
assert.are.same(1, counts_executed)
end)
end)
end)
describe('Sorters', function()

View File

@@ -1,7 +1,8 @@
local tester = require('telescope.pickers._test')
local helper = require('telescope.pickers._test_helpers')
tester.builtin_picker('find_files', 'README.md', {
post_close = {
{'README.md', function() return vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":.") end },
{'README.md', helper.get_file },
}
})

View File

@@ -0,0 +1,14 @@
require('plenary.reload').reload_module('plenary')
require('plenary.reload').reload_module('telescope')
local tester = require('telescope.pickers._test')
local helper = require('telescope.pickers._test_helpers')
tester.builtin_picker('find_files', 'telescope<c-n>', {
post_close = {
tester.not_ { 'plugin/telescope.vim', helper.get_file },
},
}, {
sorting_strategy = "descending",
scroll_strategy = "cycle",
})

View File

@@ -3,7 +3,7 @@ local helper = require('telescope.pickers._test_helpers')
tester.builtin_picker('find_files', 'fixtures/file<c-p>', {
post_close = {
{ 'lua/tests/fixtures/file_2.txt', helper.get_file }
{ 'lua/tests/fixtures/file_abc.txt', helper.get_selection_value },
},
})

View File

@@ -2,8 +2,8 @@ local ffi = require("ffi")
-- ffi.load("/home/tj/build/neovim/build/include/eval/funcs.h.generated.h")
ffi.cdef [[
typedef unsigned char char_u;
char_u *shorten_dir(char_u *str);
typedef unsigned char char_u;
char_u *shorten_dir(char_u *str);
]]
local text = "scratch/file.lua"