From 64e59060b1750d0c86761693b6847c3db07afcd2 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Thu, 8 Apr 2021 10:35:44 -0400 Subject: [PATCH] feat: asyncify pickers - except for live_grep (#709) * something kind of works already * yayayayayayayayayayayayayayayayayayayayayayayayayayayayayayayayaya * use async for everything besides live jobs * fix: fixup autocmds previewer * fix: lints for prime * temp: Add example of how we can think about async sorters * feat: Allow picker to decide when to cancel * fix: simplify scoring logic and tests * fixup: name * fix: Move back towards more backwards compat methods * fixup: Remove results from opts * fixup: remove trailing quote * fixup: Attempt to clean up some more async items. Next is status * wip: Add todo for when bfredl implements extmarks over the EOL * wip * fixup: got em * fixup: cleaning * fixup: docs --- .gitignore | 2 + lua/telescope/builtin/files.lua | 14 +- lua/telescope/builtin/internal.lua | 2 - lua/telescope/entry_manager.lua | 10 +- lua/telescope/finders.lua | 187 ++---------- lua/telescope/finders/async_job_finder.lua | 73 +++++ .../finders/async_oneshot_finder.lua | 81 +++++ lua/telescope/finders/async_static_finder.lua | 41 +++ lua/telescope/log.lua | 3 +- lua/telescope/pickers.lua | 281 ++++++++---------- lua/telescope/pickers/layout_strategies.lua | 40 ++- lua/telescope/pickers/window.lua | 17 ++ lua/telescope/sorters.lua | 35 ++- lua/tests/automated/telescope_spec.lua | 7 +- 14 files changed, 427 insertions(+), 366 deletions(-) create mode 100644 lua/telescope/finders/async_job_finder.lua create mode 100644 lua/telescope/finders/async_oneshot_finder.lua create mode 100644 lua/telescope/finders/async_static_finder.lua create mode 100644 lua/telescope/pickers/window.lua diff --git a/.gitignore b/.gitignore index 6799a850..ddeae0df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ build/ doc/tags + +.luacheckcache diff --git a/lua/telescope/builtin/files.lua b/lua/telescope/builtin/files.lua index 24d02a01..23ceaa7d 100644 --- a/lua/telescope/builtin/files.lua +++ b/lua/telescope/builtin/files.lua @@ -211,16 +211,16 @@ end files.file_browser = function(opts) opts = opts or {} + opts.depth = opts.depth or 1 opts.cwd = opts.cwd and vim.fn.expand(opts.cwd) or vim.loop.cwd() - - local gen_new_finder = function(path) + opts.new_finder = opts.new_finder or function(path) opts.cwd = path local data = {} scan.scan_dir(path, { hidden = opts.hidden or false, add_dirs = true, - depth = 1, + depth = opts.depth, on_insert = function(entry, typ) table.insert(data, typ == 'directory' and (entry .. os_sep) or entry) end @@ -242,8 +242,8 @@ files.file_browser = function(opts) end pickers.new(opts, { - prompt_title = 'Find Files', - finder = gen_new_finder(opts.cwd), + prompt_title = 'File Browser', + finder = opts.new_finder(opts.cwd), previewer = conf.file_previewer(opts), sorter = conf.file_sorter(opts), attach_mappings = function(prompt_bufnr, map) @@ -253,7 +253,7 @@ files.file_browser = function(opts) local new_cwd = vim.fn.expand(action_state.get_selected_entry().path:sub(1, -2)) local current_picker = action_state.get_current_picker(prompt_bufnr) current_picker.cwd = new_cwd - current_picker:refresh(gen_new_finder(new_cwd), { reset_prompt = true }) + current_picker:refresh(opts.new_finder(new_cwd), { reset_prompt = true }) end) local create_new_file = function() @@ -276,7 +276,7 @@ files.file_browser = function(opts) Path:new(fpath:sub(1, -2)):mkdir({ parents = true }) local new_cwd = vim.fn.expand(fpath) current_picker.cwd = new_cwd - current_picker:refresh(gen_new_finder(new_cwd), { reset_prompt = true }) + current_picker:refresh(opts.new_finder(new_cwd), { reset_prompt = true }) end end diff --git a/lua/telescope/builtin/internal.lua b/lua/telescope/builtin/internal.lua index 7d9d7506..7d2d716d 100644 --- a/lua/telescope/builtin/internal.lua +++ b/lua/telescope/builtin/internal.lua @@ -815,8 +815,6 @@ internal.autocommands = function(opts) inner_loop(line) end - -- print(vim.inspect(autocmd_table)) - pickers.new(opts,{ prompt_title = 'autocommands', finder = finders.new_table { diff --git a/lua/telescope/entry_manager.lua b/lua/telescope/entry_manager.lua index c7350cb8..f32d29f3 100644 --- a/lua/telescope/entry_manager.lua +++ b/lua/telescope/entry_manager.lua @@ -26,7 +26,7 @@ if past loop of must have scores, local EntryManager = {} EntryManager.__index = EntryManager -function EntryManager:new(max_results, set_entry, info, id) +function EntryManager:new(max_results, set_entry, info) log.trace("Creating entry_manager...") info = info or {} @@ -40,7 +40,6 @@ function EntryManager:new(max_results, set_entry, info, id) set_entry = set_entry or function() end return setmetatable({ - id = id, linked_states = LinkedList:new { track_at = max_results }, info = info, max_results = max_results, @@ -128,13 +127,6 @@ function EntryManager:_append_container(picker, new_container, should_update) end function EntryManager:add_entry(picker, score, entry) - if picker and picker.id then - if picker.request_number ~= self.id then - error("ADDING ENTRY TOO LATE!") - return - end - end - score = score or 0 local max_res = self.max_results diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index cf616b05..d2acd051 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -3,6 +3,10 @@ local Job = require('plenary.job') local make_entry = require('telescope.make_entry') local log = require('telescope.log') +local async_static_finder = require('telescope.finders.async_static_finder') +local async_oneshot_finder = require('telescope.finders.async_oneshot_finder') +-- local async_job_finder = require('telescope.finders.async_job_finder') + local finders = {} local _callable_obj = function() @@ -104,179 +108,24 @@ function JobFinder:_find(prompt, process_result, process_complete) self.job:start() end -local OneshotJobFinder = _callable_obj() - -function OneshotJobFinder:new(opts) - opts = opts or {} - - assert(not opts.results, "`results` should be used with finder.new_table") - assert(not opts.static, "`static` should be used with finder.new_oneshot_job") - - local obj = setmetatable({ - fn_command = opts.fn_command, - entry_maker = opts.entry_maker or make_entry.from_string, - - cwd = opts.cwd, - writer = opts.writer, - - maximum_results = opts.maximum_results, - - _started = false, - }, self) - - obj._find = coroutine.wrap(function(finder, _, process_result, process_complete) - local num_execution = 1 - local num_results = 0 - - local results = setmetatable({}, { - __newindex = function(t, k, v) - rawset(t, k, v) - process_result(v) - end - }) - - local job_opts = finder:fn_command(_) - if not job_opts then - error(debug.traceback("expected `job_opts` from fn_command")) - end - - local writer = nil - if job_opts.writer and Job.is_job(job_opts.writer) then - writer = job_opts.writer - elseif job_opts.writer then - writer = Job:new(job_opts.writer) - end - - local on_output = function(_, line) - -- This will call the metamethod, process_result - num_results = num_results + 1 - results[num_results] = finder.entry_maker(line) - end - - local completed = false - local job = Job:new { - command = job_opts.command, - args = job_opts.args, - cwd = job_opts.cwd or finder.cwd, - - maximum_results = finder.maximum_results, - - writer = writer, - - enable_recording = false, - - on_stdout = on_output, - on_stderr = on_output, - - on_exit = function() - process_complete() - completed = true - end, - } - - job:start() - - while true do - finder, _, process_result, process_complete = coroutine.yield() - num_execution = num_execution + 1 - - local current_count = num_results - for index = 1, current_count do - process_result(results[index]) - end - - if completed then - process_complete() - end - end - end) - - return obj -end - -function OneshotJobFinder:old_find(_, process_result, process_complete) - local first_run = false - - if not self._started then - first_run = true - - self._started = true - - end - - -- First time we get called, start on up that job. - -- Every time after that, just use the results LUL - if not first_run then - return - end -end - - - ---[[ ============================================================= -Static Finders - -A static finder has results that never change. -They are passed in directly as a result. --- ============================================================= ]] -local StaticFinder = _callable_obj() - -function StaticFinder:new(opts) - assert(opts, "Options are required. See documentation for usage") - - local input_results - if vim.tbl_islist(opts) then - input_results = opts - else - input_results = opts.results - end - - local entry_maker = opts.entry_maker or make_entry.gen_from_string() - - assert(input_results) - assert(input_results, "Results are required for static finder") - assert(type(input_results) == 'table', "self.results must be a table") - - local results = {} - for k, v in ipairs(input_results) do - local entry = entry_maker(v) - - if entry then - entry.index = k - table.insert(results, entry) - end - end - - return setmetatable({ results = results }, self) -end - -function StaticFinder:_find(_, process_result, process_complete) - for _, v in ipairs(self.results) do - process_result(v) - end - - process_complete() -end - - --- local - - --- Return a new Finder -- -- Use at your own risk. -- This opts dictionary is likely to change, but you are welcome to use it right now. -- I will try not to change it needlessly, but I will change it sometimes and I won't feel bad. finders._new = function(opts) - if opts.results then - print("finder.new is deprecated with `results`. You should use `finder.new_table`") - return StaticFinder:new(opts) - end - + assert(not opts.results, "finder.new is deprecated with `results`. You should use `finder.new_table`") return JobFinder:new(opts) end finders.new_job = function(command_generator, entry_maker, maximum_results, cwd) + -- return async_job_finder { + -- command_generator = command_generator, + -- entry_maker = entry_maker, + -- maximum_results = maximum_results, + -- cwd = cwd, + -- } + return JobFinder:new { fn_command = function(_, prompt) local command_list = command_generator(prompt) @@ -298,18 +147,20 @@ finders.new_job = function(command_generator, entry_maker, maximum_results, cwd) } end ----@param command_list string[] Command list to execute. ----@param opts table +--- One shot job +---@param command_list string[]: Command list to execute. +---@param opts table: stuff --- @key entry_maker function Optional: function(line: string) => table --- @key cwd string finders.new_oneshot_job = function(command_list, opts) opts = opts or {} - command_list = vim.deepcopy(command_list) + assert(not opts.results, "`results` should be used with finder.new_table") + command_list = vim.deepcopy(command_list) local command = table.remove(command_list, 1) - return OneshotJobFinder:new { + return async_oneshot_finder { entry_maker = opts.entry_maker or make_entry.gen_from_string(), cwd = opts.cwd, @@ -331,7 +182,7 @@ end -- results table, the results to run on -- entry_maker function, the function to convert results to entries. finders.new_table = function(t) - return StaticFinder:new(t) + return async_static_finder(t) end return finders diff --git a/lua/telescope/finders/async_job_finder.lua b/lua/telescope/finders/async_job_finder.lua new file mode 100644 index 00000000..ee1b2c78 --- /dev/null +++ b/lua/telescope/finders/async_job_finder.lua @@ -0,0 +1,73 @@ +local log = require('telescope.log') +local Job = require('plenary.job') + +local async_lib = require('plenary.async_lib') +local async = async_lib.async +-- local await = async_lib.await +local void = async_lib.void + +local make_entry = require('telescope.make_entry') + +return function(opts) + local entry_maker = opts.entry_maker or make_entry.gen_from_string() + local fn_command = function(prompt) + local command_list = opts.command_generator(prompt) + if command_list == nil then + return nil + end + + local command = table.remove(command_list, 1) + + return { + command = command, + args = command_list, + } + end + + local job + return setmetatable({ + close = function() end, + }, { + __call = void(async(function(prompt, process_result, process_complete) + print("are we callin anything?", job) + if job and not job.is_shutdown then + log.debug("Shutting down old job") + job:shutdown() + end + + local job_opts = fn_command(prompt) + if not job_opts then return end + + local writer = nil + if job_opts.writer and Job.is_job(job_opts.writer) then + writer = job_opts.writer + elseif opts.writer then + writer = Job:new(job_opts.writer) + end + + job = Job:new { + command = job_opts.command, + args = job_opts.args, + cwd = job_opts.cwd or opts.cwd, + maximum_results = opts.maximum_results, + writer = writer, + enable_recording = false, + + on_stdout = vim.schedule_wrap(function(_, line) + if not line or line == "" then + return + end + + -- TODO: shutdown job here. + process_result(entry_maker(line)) + end), + + on_exit = function() + process_complete() + end, + } + + job:start() + end)), + }) +end diff --git a/lua/telescope/finders/async_oneshot_finder.lua b/lua/telescope/finders/async_oneshot_finder.lua new file mode 100644 index 00000000..23450967 --- /dev/null +++ b/lua/telescope/finders/async_oneshot_finder.lua @@ -0,0 +1,81 @@ +local async_lib = require('plenary.async_lib') +local async = async_lib.async +local await = async_lib.await +local void = async_lib.void + +local AWAITABLE = 1000 + +local make_entry = require('telescope.make_entry') + +local Job = require('plenary.job') + +return function(opts) + opts = opts or {} + + local entry_maker = opts.entry_maker or make_entry.from_string + local cwd = opts.cwd + local fn_command = assert(opts.fn_command, "Must pass `fn_command`") + + local results = {} + local num_results = 0 + + local job_started = false + local job_completed = false + return setmetatable({ + close = function() results = {}; job_started = false end, + results = results, + }, { + __call = void(async(function(_, prompt, process_result, process_complete) + if not job_started then + local job_opts = fn_command() + + local writer + if job_opts.writer and Job.is_job(job_opts.writer) then + writer = job_opts.writer + elseif job_opts.writer then + writer = Job:new(job_opts.writer) + end + + local job = Job:new { + command = job_opts.command, + args = job_opts.args, + cwd = job_opts.cwd or cwd, + maximum_results = opts.maximum_results, + writer = writer, + enable_recording = false, + + on_stdout = vim.schedule_wrap(function(_, line) + num_results = num_results + 1 + + local v = entry_maker(line) + results[num_results] = v + process_result(v) + end), + + on_exit = function() + process_complete() + job_completed = true + end, + } + + job:start() + job_started = true + end + + local current_count = num_results + for index = 1, current_count do + if process_result(results[index]) then + break + end + + if index % AWAITABLE == 0 then + await(async_lib.scheduler()) + end + end + + if job_completed then + process_complete() + end + end)), + }) +end diff --git a/lua/telescope/finders/async_static_finder.lua b/lua/telescope/finders/async_static_finder.lua new file mode 100644 index 00000000..0756551e --- /dev/null +++ b/lua/telescope/finders/async_static_finder.lua @@ -0,0 +1,41 @@ +local async_lib = require('plenary.async_lib') +local async = async_lib.async +local await = async_lib.await +local void = async_lib.void + +local make_entry = require('telescope.make_entry') + +return function(opts) + local input_results + if vim.tbl_islist(opts) then input_results = opts + else input_results = opts.results end + + local entry_maker = opts.entry_maker or make_entry.gen_from_string() + + local results = {} + for k, v in ipairs(input_results) do + local entry = entry_maker(v) + + if entry then + entry.index = k + table.insert(results, entry) + end + end + + return setmetatable({ + results = results, + close = function() end, + }, { + __call = void(async(function(_, _, process_result, process_complete) + for i, v in ipairs(results) do + if process_result(v) then break end + + if i % 1000 == 0 then + await(async_lib.scheduler()) + end + end + + process_complete() + end)), + }) +end diff --git a/lua/telescope/log.lua b/lua/telescope/log.lua index 9beabb1d..c93472bb 100644 --- a/lua/telescope/log.lua +++ b/lua/telescope/log.lua @@ -1,5 +1,6 @@ +local user = vim.loop.os_getenv("USER") return require('plenary.log').new { plugin = 'telescope', - level = (vim.loop.os_getenv("USER") == 'tj' and 'debug') or 'warn', + level = ((user == 'tj' or user == 'tjdevries') and 'debug') or 'warn', } diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index 5ba0ca00..7e658c36 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -1,22 +1,28 @@ local a = vim.api local popup = require('popup') +local async_lib = require('plenary.async_lib') +local async_util = async_lib.util + +local async = async_lib.async +local await = async_lib.await +local channel = async_util.channel + require('telescope') local actions = require('telescope.actions') local action_set = require('telescope.actions.set') local config = require('telescope.config') local debounce = require('telescope.debounce') -local resolve = require('telescope.config.resolve') local log = require('telescope.log') local mappings = require('telescope.mappings') local state = require('telescope.state') local utils = require('telescope.utils') -local layout_strategies = require('telescope.pickers.layout_strategies') local entry_display = require('telescope.pickers.entry_display') -local p_highlights = require('telescope.pickers.highlights') +local p_highlighter = require('telescope.pickers.highlights') local p_scroller = require('telescope.pickers.scroller') +local p_window = require('telescope.pickers.window') local EntryManager = require('telescope.entry_manager') local MultiSelect = require('telescope.pickers.multi') @@ -73,6 +79,7 @@ function Picker:new(opts) cwd = opts.cwd, + _find_id = 0, _completion_callbacks = {}, _multi = MultiSelect:new(), @@ -85,7 +92,6 @@ function Picker:new(opts) sorting_strategy = get_default(opts.sorting_strategy, config.values.sorting_strategy), selection_strategy = get_default(opts.selection_strategy, config.values.selection_strategy), - get_window_options = opts.get_window_options, layout_strategy = layout_strategy, layout_config = get_default( opts.layout_config, @@ -116,12 +122,15 @@ function Picker:new(opts) preview_cutoff = get_default(opts.preview_cutoff, config.values.preview_cutoff), }, self) + obj.get_window_options = opts.get_window_options or p_window.get_window_options + + -- TODO: It's annoying that this is create and everything else is "new" obj.scroller = p_scroller.create( get_default(opts.scroll_strategy, config.values.scroll_strategy), obj.sorting_strategy ) - obj.highlighter = p_highlights.new(obj) + obj.highlighter = p_highlighter.new(obj) if opts.on_complete then for _, on_complete_item in ipairs(opts.on_complete) do @@ -132,52 +141,8 @@ function Picker:new(opts) return obj end -function Picker:_get_initial_window_options() - local popup_border = resolve.win_option(self.window.border) - local popup_borderchars = resolve.win_option(self.window.borderchars) - - local preview = { - title = self.preview_title, - border = popup_border.preview, - borderchars = popup_borderchars.preview, - enter = false, - highlight = false - } - - local results = { - title = self.results_title, - border = popup_border.results, - borderchars = popup_borderchars.results, - enter = false, - } - - local prompt = { - title = self.prompt_title, - border = popup_border.prompt, - borderchars = popup_borderchars.prompt, - enter = true - } - - return { - preview = preview, - results = results, - prompt = prompt, - } -end - -function Picker:get_window_options(max_columns, max_lines) - local layout_strategy = self.layout_strategy - local getter = layout_strategies[layout_strategy] - - if not getter then - error("Not a valid layout strategy: " .. layout_strategy) - end - - return getter(self, max_columns, max_lines) -end - --- Take a row and get an index. ---- @note: Rows are 0-indexed, and `index` is 1 indexed (table index) +---@note: Rows are 0-indexed, and `index` is 1 indexed (table index) ---@param index number: The row being displayed ---@return number The row for the picker to display in function Picker:get_row(index) @@ -308,6 +273,13 @@ function Picker:can_select_row(row) end end +function Picker:_next_find_id() + local find_id = self._find_id + 1 + self._find_id = find_id + + return find_id +end + function Picker:find() self:close_existing_pickers() self:reset_selection() @@ -317,7 +289,7 @@ function Picker:find() self.original_win_id = a.nvim_get_current_win() -- User autocmd run it before create Telescope window - vim.cmd'do User TelescopeFindPre' + vim.cmd [[doautocmd User TelescopeFindPre]] -- Create three windows: -- 1. Prompt window @@ -393,66 +365,70 @@ function Picker:find() local status_updater = self:get_status_updater(prompt_win, prompt_bufnr) local debounced_status = debounce.throttle_leading(status_updater, 50) + -- local debounced_status = status_updater - self.request_number = 0 - local on_lines = function(_, _, _, first_line, last_line) - self.request_number = self.request_number + 1 - self:_reset_track() + local tx, rx = channel.mpsc() + self.__on_lines = tx.send - if not vim.api.nvim_buf_is_valid(prompt_bufnr) then - log.debug("ON_LINES: Invalid prompt_bufnr", prompt_bufnr) - return + local main_loop = async(function() + while true do + await(async_lib.scheduler()) + + local _, _, _, first_line, last_line = await(rx.last()) + self:_reset_track() + + if not vim.api.nvim_buf_is_valid(prompt_bufnr) then + log.debug("ON_LINES: Invalid prompt_bufnr", prompt_bufnr) + 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 + end + + local original_prompt = self:_get_prompt() + local on_input_result = self._on_input_filter_cb(original_prompt) or {} + + local prompt = on_input_result.prompt or original_prompt + local finder = on_input_result.updated_finder + + if finder then + self.finder:close() + self.finder = finder + end + + if self.sorter then + self.sorter:_start(prompt) + end + + -- 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.stats) + + local find_id = self:_next_find_id() + local process_result = self:get_result_processor(find_id, prompt, debounced_status) + local process_complete = self:get_result_completor(self.results_bufnr, find_id, prompt, status_updater) + + local ok, msg = pcall(function() + self.finder(prompt, process_result, vim.schedule_wrap(process_complete)) + end) + + if not ok then + log.warn("Failed with msg: ", msg) + end end + 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 - end - - local original_prompt = self:_get_prompt() - local on_input_result = self._on_input_filter_cb(original_prompt) or {} - - local prompt = on_input_result.prompt or original_prompt - local finder = on_input_result.updated_finder - - if finder then - self.finder:close() - self.finder = finder - end - - if self.sorter then - self.sorter:_start(prompt) - end - - -- 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.stats, self.request_number) - - local process_result = self:get_result_processor(prompt, debounced_status) - local process_complete = self:get_result_completor(self.results_bufnr, prompt, status_updater) - - local ok, msg = pcall(function() - self.finder(prompt, process_result, vim.schedule_wrap(process_complete)) - end) - - if not ok then - log.warn("Failed with msg: ", msg) - end - end - - self.__on_lines = on_lines - - on_lines(nil, nil, nil, 0, 1) + -- on_lines(nil, nil, nil, 0, 1) status_updater() -- Register attach vim.api.nvim_buf_attach(prompt_bufnr, false, { - on_lines = on_lines, + on_lines = tx.send, on_detach = function() - on_lines = nil - -- TODO: Can we add a "cleanup" / "teardown" function that completely removes these. self.finder = nil self.previewer = nil @@ -466,6 +442,8 @@ function Picker:find() end, }) + async_lib.run(main_loop()) + -- TODO: Use WinLeave as well? local on_buf_leave = string.format( [[ autocmd BufLeave ++nested ++once :silent lua require('telescope.pickers').on_close_prompt(%s)]], @@ -659,7 +637,8 @@ function Picker:refresh(finder, opts) if opts.reset_prompt then self:reset_prompt() end self.finder:close() - self.finder = finder + if finder then self.finder = finder end + self.__on_lines(nil, nil, nil, 0, 1) end @@ -695,6 +674,8 @@ function Picker:set_selection(row) local entry = self.manager:get_entry(self:get_index(row)) state.set_global_key("selected_entry", entry) + if not entry then return end + -- TODO: Probably should figure out what the rows are that made this happen... -- Probably something with setting a row that's too high for this? -- Not sure. @@ -775,6 +756,8 @@ function Picker:refresh_previewer() end function Picker:entry_adder(index, entry, _, insert) + if not entry then return end + local row = self:get_row(index) -- If it's less than 0, then we don't need to show it at all. @@ -799,18 +782,14 @@ function Picker:entry_adder(index, entry, _, insert) -- 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 - if self.request_number ~= scheduled_request then - log.trace("Cancelling request number:", self.request_number, " // ", scheduled_request) - return - end - + -- TODO: Does this every get called? + -- local line_count = vim.api.nvim_win_get_height(self.results_win) local line_count = vim.api.nvim_buf_line_count(self.results_bufnr) if row > line_count then return @@ -850,11 +829,6 @@ function Picker:_reset_track() self.stats.filtered = 0 self.stats.highlights = 0 - - self.stats._sort_time = 0 - self.stats._add_time = 0 - self.stats._highlight_time = 0 - self.stats._start = vim.loop.hrtime() end function Picker:_track(key, func, ...) @@ -914,8 +888,7 @@ function Picker:get_status_updater(prompt_win, prompt_bufnr) return end - local expected_prompt_len = #self.prompt_prefix + 1 - local prompt_len = #current_prompt < expected_prompt_len and expected_prompt_len or #current_prompt + local prompt_len = #current_prompt local padding = string.rep(" ", vim.api.nvim_win_get_width(prompt_win) - prompt_len - #text - 3) vim.api.nvim_buf_clear_namespace(prompt_bufnr, ns_telescope_prompt, 0, 1) @@ -927,68 +900,61 @@ function Picker:get_status_updater(prompt_win, prompt_bufnr) {} ) + -- TODO: Wait for bfredl + -- vim.api.nvim_buf_set_extmark(prompt_bufnr, ns_telescope_prompt, 0, 0, { + -- end_line = 0, + -- -- end_col = start_column + #text, + -- virt_text = { { text, "NonText", } }, + -- virt_text_pos = "eol", + -- }) + self:_increment("status") end end -function Picker:get_result_processor(prompt, status_updater) +function Picker:get_result_processor(find_id, prompt, status_updater) + local cb_add = function(score, entry) + self.manager:add_entry(self, score, entry) + status_updater() + end + + local cb_filter = function(_) + self:_increment("filtered") + end + return function(entry) - if self.closed or self:is_done() then return end + if find_id ~= self._find_id + or self.closed + or self:is_done() then + return true + end self:_increment("processed") - if not entry then - log.debug("No entry...") - return - end - - -- TODO: Should we even have valid? - if entry.valid == false then + if not entry or entry.valid == false then return end + -- TODO: Probably should asyncify this / cache this / do something because this probably takes + -- a ton of time on large results. log.trace("Processing result... ", entry) - for _, v in ipairs(self.file_ignore_patterns or {}) do local file = type(entry.value) == 'string' and entry.value or entry.filename if file then if string.find(file, v) then - log.debug("SKIPPING", entry.value, "because", v) + log.trace("SKIPPING", entry.value, "because", v) self:_decrement("processed") return end end end - local sort_ok - local sort_score = 0 - if self.sorter then - sort_ok, sort_score = self:_track("_sort_time", pcall, self.sorter.score, self.sorter, prompt, entry) - - if not sort_ok then - log.warn("Sorting failed with:", prompt, entry, sort_score) - return - end - - if entry.ignore_count ~= nil and entry.ignore_count == true then - self:_decrement("processed") - end - - if sort_score == -1 then - self:_increment("filtered") - log.trace("Filtering out result: ", entry) - return - end - end - - self:_track("_add_time", self.manager.add_entry, self.manager, self, sort_score, entry) - - status_updater() + self.sorter:score(prompt, entry, cb_add, cb_filter) end end -function Picker:get_result_completor(results_bufnr, prompt, status_updater) +function Picker:get_result_completor(results_bufnr, find_id, prompt, status_updater) return function() if self.closed == true or self:is_done() then return end @@ -1030,17 +996,6 @@ function Picker:get_result_completor(results_bufnr, prompt, status_updater) self:clear_extra_rows(results_bufnr) self:highlight_displayed_rows(results_bufnr, prompt) - -- TODO: Cleanup. - self.stats._done = vim.loop.hrtime() - self.stats.time = (self.stats._done - self.stats._start) / 1e9 - - local function do_times(key) - self.stats[key] = self.stats["_" .. key] / 1e9 - end - - do_times("sort_time") - do_times("add_time") - do_times("highlight_time") self:_on_complete() diff --git a/lua/telescope/pickers/layout_strategies.lua b/lua/telescope/pickers/layout_strategies.lua index b8d9aea3..b4c9fda5 100644 --- a/lua/telescope/pickers/layout_strategies.lua +++ b/lua/telescope/pickers/layout_strategies.lua @@ -61,6 +61,40 @@ local config = require('telescope.config') local resolve = require("telescope.config.resolve") +local function get_initial_window_options(picker) + local popup_border = resolve.win_option(picker.window.border) + local popup_borderchars = resolve.win_option(picker.window.borderchars) + + local preview = { + title = picker.preview_title, + border = popup_border.preview, + borderchars = popup_borderchars.preview, + enter = false, + highlight = false + } + + local results = { + title = picker.results_title, + border = popup_border.results, + borderchars = popup_borderchars.results, + enter = false, + } + + local prompt = { + title = picker.prompt_title, + border = popup_border.prompt, + borderchars = popup_borderchars.prompt, + enter = true + } + + return { + preview = preview, + results = results, + prompt = prompt, + } +end + + -- Check if there are any borders. Right now it's a little raw as -- there are a few things that contribute to the border local is_borderless = function(opts) @@ -105,7 +139,7 @@ layout_strategies.horizontal = function(self, max_columns, max_lines) scroll_speed = "The speed when scrolling through the previewer", }) - local initial_options = self:_get_initial_window_options() + local initial_options = get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt @@ -203,7 +237,7 @@ end --- +--------------+ --- layout_strategies.center = function(self, columns, lines) - local initial_options = self:_get_initial_window_options() + local initial_options = get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt @@ -273,7 +307,7 @@ layout_strategies.vertical = function(self, max_columns, max_lines) scroll_speed = "The speed when scrolling through the previewer", }) - local initial_options = self:_get_initial_window_options() + local initial_options = get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt diff --git a/lua/telescope/pickers/window.lua b/lua/telescope/pickers/window.lua new file mode 100644 index 00000000..76c1fe0d --- /dev/null +++ b/lua/telescope/pickers/window.lua @@ -0,0 +1,17 @@ +local p_layouts = require('telescope.pickers.layout_strategies') + +local p_window = {} + +function p_window.get_window_options(picker, max_columns, max_lines) + local layout_strategy = picker.layout_strategy + local getter = p_layouts[layout_strategy] + + if not getter then + error("Not a valid layout strategy: " .. layout_strategy) + end + + return getter(picker, max_columns, max_lines) +end + + +return p_window diff --git a/lua/telescope/sorters.lua b/lua/telescope/sorters.lua index 5ac7086e..4147f59e 100644 --- a/lua/telescope/sorters.lua +++ b/lua/telescope/sorters.lua @@ -32,12 +32,17 @@ Sorter.__index = Sorter --- --- Lower number is better (because it's like a closer match) --- But, any number below 0 means you want that line filtered out. ---- @field scoring_function function Function that has the interface: --- (sorter, prompt, line): number +---@field scoring_function function: Function that has the interface: (sorter, prompt, line): number +---@field tags table: Unique tags collected at filtering for tag completion +---@field filter_function function: Function that can filter results +---@field highlighter function: Highlights results to display them pretty +---@field discard boolean: Whether this is a discardable style sorter or not. +---@field score function: Override the score function if desired. function Sorter:new(opts) opts = opts or {} return setmetatable({ + score = opts.score, state = {}, tags = opts.tags, filter_function = opts.filter_function, @@ -77,13 +82,12 @@ end -- TODO: Consider doing something that makes it so we can skip the filter checks -- if we're not discarding. Also, that means we don't have to check otherwise as well :) -function Sorter:score(prompt, entry) - if not entry or not entry.ordinal then return -1 end +function Sorter:score(prompt, entry, cb_add, cb_filter) + if not entry or not entry.ordinal then return end local ordinal = entry.ordinal - if self:_was_discarded(prompt, ordinal) then - return FILTERED + return cb_filter(entry) end local filter_score @@ -92,14 +96,21 @@ function Sorter:score(prompt, entry) filter_score, prompt = self:filter_function(prompt, entry) end - local score = (filter_score == FILTERED and FILTERED or - self:scoring_function(prompt or "", ordinal, entry)) - - if score == FILTERED then - self:_mark_discarded(prompt, ordinal) + if filter_score == FILTERED then + return cb_filter(entry) end - return score + local score = self:scoring_function(prompt or "", ordinal, entry) + if score == FILTERED then + self:_mark_discarded(prompt, ordinal) + return cb_filter(entry) + end + + if cb_add then + return cb_add(score, entry) + else + return score + end end function Sorter:_was_discarded(prompt, ordinal) diff --git a/lua/tests/automated/telescope_spec.lua b/lua/tests/automated/telescope_spec.lua index 0f7bc85a..6ad09d78 100644 --- a/lua/tests/automated/telescope_spec.lua +++ b/lua/tests/automated/telescope_spec.lua @@ -100,7 +100,12 @@ describe('telescope', function() describe('fzy', function() local sorter = require'telescope.sorters'.get_fzy_sorter() local function score(prompt, line) - return sorter:score(prompt, {ordinal = line}) + return sorter:score( + prompt, + {ordinal = line}, + function(val) return val end, + function() return -1 end + ) end describe("matches", function()