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:
@@ -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.
|
||||
-- ==================================================
|
||||
|
||||
222
lua/telescope/algos/linked_list.lua
Normal file
222
lua/telescope/algos/linked_list.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user