diff options
| author | TJ DeVries <devries.timothyj@gmail.com> | 2021-08-20 11:11:24 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-08-20 11:11:24 -0400 |
| commit | a97af306c4e9c9a6fa7c886c0ffe3079822c5203 (patch) | |
| tree | f5e2b50a767e93618d0d8fdddb8a964c90633c8a /lua/telescope | |
| parent | d6d28dbe324de9826a579155076873888169ba0f (diff) | |
feat(performance): Major performance improvements using async v2 from @oberblastmeister (#987)
* start: Working w/ async jobs
* short circuit to using bad finder if you pass writer.
Diffstat (limited to 'lua/telescope')
| -rw-r--r-- | lua/telescope/_.lua | 296 | ||||
| -rw-r--r-- | lua/telescope/_compat.lua | 56 | ||||
| -rw-r--r-- | lua/telescope/actions/init.lua | 12 | ||||
| -rw-r--r-- | lua/telescope/actions/state.lua | 2 | ||||
| -rw-r--r-- | lua/telescope/builtin/files.lua | 7 | ||||
| -rw-r--r-- | lua/telescope/builtin/lsp.lua | 23 | ||||
| -rw-r--r-- | lua/telescope/config.lua | 9 | ||||
| -rw-r--r-- | lua/telescope/entry_manager.lua | 15 | ||||
| -rw-r--r-- | lua/telescope/finders.lua | 55 | ||||
| -rw-r--r-- | lua/telescope/finders/async_job_finder.lua | 94 | ||||
| -rw-r--r-- | lua/telescope/finders/async_oneshot_finder.lua | 82 | ||||
| -rw-r--r-- | lua/telescope/finders/async_static_finder.lua | 11 | ||||
| -rw-r--r-- | lua/telescope/init.lua | 2 | ||||
| -rw-r--r-- | lua/telescope/path.lua | 103 | ||||
| -rw-r--r-- | lua/telescope/pickers.lua | 278 | ||||
| -rw-r--r-- | lua/telescope/pickers/highlights.lua | 2 | ||||
| -rw-r--r-- | lua/telescope/sorters.lua | 51 |
17 files changed, 613 insertions, 485 deletions
diff --git a/lua/telescope/_.lua b/lua/telescope/_.lua new file mode 100644 index 0000000..a325c7d --- /dev/null +++ b/lua/telescope/_.lua @@ -0,0 +1,296 @@ +local uv = vim.loop + +local Object = require "plenary.class" +local log = require "plenary.log" + +local async = require "plenary.async" +local channel = require("plenary.async").control.channel + +local M = {} + +local AsyncJob = {} +AsyncJob.__index = AsyncJob + +function AsyncJob.new(opts) + local self = setmetatable({}, AsyncJob) + + self.command, self.uv_opts = M.convert_opts(opts) + + self.stdin = opts.stdin or M.NullPipe() + self.stdout = opts.stdout or M.NullPipe() + self.stderr = opts.stderr or M.NullPipe() + + if opts.cwd then + -- TODO: not vim.fn + self.uv_opts.cwd = vim.fn.expand(opts.cwd) + end + + self.uv_opts.stdio = { + self.stdin.handle, + self.stdout.handle, + self.stderr.handle, + } + + return self +end + +function AsyncJob:_for_each_pipe(f, ...) + for _, pipe in ipairs { self.stdin, self.stdout, self.stderr } do + f(pipe, ...) + end +end + +function AsyncJob:close(force) + if force == nil then + force = true + end + + self:_for_each_pipe(function(p) + p:close(force) + end) + if not self.handle:is_closing() then + self.handle:close() + end + + log.debug "[async_job] closed" +end + +M.spawn = function(opts) + local self = AsyncJob.new(opts) + + self.handle = uv.spawn( + self.command, + self.uv_opts, + async.void(function() + self:close(false) + end) + ) + + return self +end + +---@class uv_pipe_t +--- A pipe handle from libuv +---@field read_start function: Start reading +---@field read_stop function: Stop reading +---@field close function: Close the handle +---@field is_closing function: Whether handle is currently closing +---@field is_active function: Whether the handle is currently reading + +---@class BasePipe +---@field super Object: Always available +---@field handle uv_pipe_t: A pipe handle +---@field extend function: Extend +local BasePipe = Object:extend() + +function BasePipe:new() + self.eof_tx, self.eof_rx = channel.oneshot() +end + +function BasePipe:close(force) + if force == nil then + force = true + end + + assert(self.handle, "Must have a pipe to close. Otherwise it's weird!") + + if self.handle:is_closing() then + return + end + + -- If we're not forcing the stop, allow waiting for eof + -- This ensures that we don't end up with weird race conditions + if not force then + self.eof_rx() + end + + self.handle:read_stop() + if not self.handle:is_closing() then + self.handle:close() + end + + self._closed = true +end + +---@class LinesPipe : BasePipe +local LinesPipe = BasePipe:extend() + +function LinesPipe:new() + LinesPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function LinesPipe:read() + local read_tx, read_rx = channel.oneshot() + + self.handle:read_start(function(err, data) + assert(not err, err) + self.handle:read_stop() + + read_tx(data) + if data == nil then + self.eof_tx() + end + end) + + return read_rx() +end + +function LinesPipe:iter(schedule) + if schedule == nil then + schedule = true + end + + local text = nil + local index = nil + + local get_next_text = function(previous) + index = nil + + local read = self:read() + if previous == nil and read == nil then + return + end + + return (previous or "") .. (read or "") + end + + local next_value = nil + next_value = function() + if schedule then + async.util.scheduler() + end + + if text == nil or (text == "" and index == nil) then + return nil + end + + local start = index + index = string.find(text, "\n", index, true) + + if index == nil then + text = get_next_text(string.sub(text, start or 1)) + return next_value() + end + + index = index + 1 + + return string.sub(text, start or 1, index - 2) + end + + text = get_next_text() + + return function() + return next_value() + end +end + +---@class NullPipe : BasePipe +local NullPipe = BasePipe:extend() + +function NullPipe:new() + NullPipe.super.new(self) + self.start = function() end + self.read_start = function() end + self.close = function() end + + -- This always has eof tx done, so can just call it now + self.eof_tx() +end + +---@class ChunkPipe : BasePipe +local ChunkPipe = BasePipe:extend() + +function ChunkPipe:new() + ChunkPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function ChunkPipe:read() + local read_tx, read_rx = channel.oneshot() + + self.handle:read_start(function(err, data) + assert(not err, err) + self.handle:read_stop() + + read_tx(data) + if data == nil then + self.eof_tx() + end + end) + + return read_rx() +end + +function ChunkPipe:iter() + return function() + if self._closed then + return nil + end + + return self:read() + end +end + +---@class ErrorPipe : BasePipe +local ErrorPipe = BasePipe:extend() + +function ErrorPipe:new() + ErrorPipe.super.new(self) + self.handle = uv.new_pipe(false) +end + +function ErrorPipe:start() + self.handle:read_start(function(err, data) + if not err and not data then + return + end + + self.handle:read_stop() + self.handle:close() + + error(string.format("Err: %s, Data: '%s'", err, data)) + end) +end + +M.NullPipe = NullPipe +M.LinesPipe = LinesPipe +M.ChunkPipe = ChunkPipe +M.ErrorPipe = ErrorPipe + +M.convert_opts = function(o) + if not o then + error(debug.traceback "Options are required for Job:new") + end + + local command = o.command + if not command then + if o[1] then + command = o[1] + else + error(debug.traceback "'command' is required for Job:new") + end + elseif o[1] then + error(debug.traceback "Cannot pass both 'command' and array args") + end + + local args = o.args + if not args then + if #o > 1 then + args = { select(2, unpack(o)) } + end + end + + local ok, is_exe = pcall(vim.fn.executable, command) + if not o.skip_validation and ok and 1 ~= is_exe then + error(debug.traceback(command .. ": Executable not found")) + end + + local obj = {} + + obj.args = args + + return command, obj +end + +return M diff --git a/lua/telescope/_compat.lua b/lua/telescope/_compat.lua deleted file mode 100644 index 42a3dfd..0000000 --- a/lua/telescope/_compat.lua +++ /dev/null @@ -1,56 +0,0 @@ -vim.deepcopy = (function() - local function _id(v) - return v - end - - local deepcopy_funcs = { - table = function(orig) - local copy = {} - - if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then - copy = vim.empty_dict() - end - - for k, v in pairs(orig) do - copy[vim.deepcopy(k)] = vim.deepcopy(v) - end - - if getmetatable(orig) then - setmetatable(copy, getmetatable(orig)) - end - - return copy - end, - ["function"] = _id or function(orig) - local ok, dumped = pcall(string.dump, orig) - if not ok then - error(debug.traceback(dumped)) - end - - local cloned = loadstring(dumped) - local i = 1 - while true do - local name = debug.getupvalue(orig, i) - if not name then - break - end - debug.upvaluejoin(cloned, i, orig, i) - i = i + 1 - end - return cloned - end, - number = _id, - string = _id, - ["nil"] = _id, - boolean = _id, - } - - return function(orig) - local f = deepcopy_funcs[type(orig)] - if f then - return f(orig) - else - error("Cannot deepcopy object of type " .. type(orig)) - end - end -end)() diff --git a/lua/telescope/actions/init.lua b/lua/telescope/actions/init.lua index 6dc022e..43315cc 100644 --- a/lua/telescope/actions/init.lua +++ b/lua/telescope/actions/init.lua @@ -582,12 +582,22 @@ actions.git_staging_toggle = function(prompt_bufnr) end local entry_to_qf = function(entry) + local text = entry.text + + if not text then + if type(entry.value) == "table" then + text = entry.value.text + else + text = entry.value + end + end + return { bufnr = entry.bufnr, filename = from_entry.path(entry, false), lnum = entry.lnum, col = entry.col, - text = entry.text or entry.value.text or entry.value, + text = text, } end diff --git a/lua/telescope/actions/state.lua b/lua/telescope/actions/state.lua index 0be2c3f..80fef56 100644 --- a/lua/telescope/actions/state.lua +++ b/lua/telescope/actions/state.lua @@ -18,7 +18,7 @@ end --- Gets the current line function action_state.get_current_line() - return global_state.get_global_key "current_line" + return global_state.get_global_key "current_line" or "" end --- Gets the current picker diff --git a/lua/telescope/builtin/files.lua b/lua/telescope/builtin/files.lua index 605a874..b0cf342 100644 --- a/lua/telescope/builtin/files.lua +++ b/lua/telescope/builtin/files.lua @@ -5,6 +5,7 @@ local finders = require "telescope.finders" local make_entry = require "telescope.make_entry" local pickers = require "telescope.pickers" local previewers = require "telescope.previewers" +local sorters = require "telescope.sorters" local utils = require "telescope.utils" local conf = require("telescope.config").values local log = require "telescope.log" @@ -80,8 +81,6 @@ files.live_grep = function(opts) return nil end - prompt = escape_chars(prompt) - local search_list = {} if search_dirs then @@ -103,7 +102,9 @@ files.live_grep = function(opts) prompt_title = "Live Grep", finder = live_grepper, previewer = conf.grep_previewer(opts), - sorter = conf.generic_sorter(opts), + -- TODO: It would be cool to use `--json` output for this + -- and then we could get the highlight positions directly. + sorter = sorters.highlighter_only(opts), }):find() end diff --git a/lua/telescope/builtin/lsp.lua b/lua/telescope/builtin/lsp.lua index bcbb833..e0550fc 100644 --- a/lua/telescope/builtin/lsp.lua +++ b/lua/telescope/builtin/lsp.lua @@ -1,16 +1,14 @@ -local actions = require "telescope.actions" +local channel = require("plenary.async.control").channel + local action_state = require "telescope.actions.state" +local actions = require "telescope.actions" +local conf = require("telescope.config").values +local entry_display = require "telescope.pickers.entry_display" local finders = require "telescope.finders" local make_entry = require "telescope.make_entry" local pickers = require "telescope.pickers" -local entry_display = require "telescope.pickers.entry_display" -local utils = require "telescope.utils" local strings = require "plenary.strings" -local a = require "plenary.async_lib" -local async, await = a.async, a.await -local channel = a.util.channel - -local conf = require("telescope.config").values +local utils = require "telescope.utils" local lsp = {} @@ -309,20 +307,21 @@ lsp.workspace_symbols = function(opts) }):find() end +-- TODO(MERGE) local function get_workspace_symbols_requester(bufnr) local cancel = function() end - return async(function(prompt) + return function(prompt) local tx, rx = channel.oneshot() cancel() _, cancel = vim.lsp.buf_request(bufnr, "workspace/symbol", { query = prompt }, tx) - local err, _, results_lsp = await(rx()) + local err, _, results_lsp = rx() assert(not err, err) local locations = vim.lsp.util.symbols_to_items(results_lsp or {}, bufnr) or {} return locations - end) + end end lsp.dynamic_workspace_symbols = function(opts) @@ -335,7 +334,7 @@ lsp.dynamic_workspace_symbols = function(opts) fn = get_workspace_symbols_requester(curr_bufnr), }, previewer = conf.qflist_previewer(opts), - sorter = conf.generic_sorter(), + sorter = conf.generic_sorter(opts), }):find() end diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua index 8db9cbb..1b0ce86 100644 --- a/lua/telescope/config.lua +++ b/lua/telescope/config.lua @@ -219,13 +219,20 @@ local telescope_defaults = { borderchars = { { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } }, get_status_text = { - function(self) + function(self, opts) local xx = (self.stats.processed or 0) - (self.stats.filtered or 0) local yy = self.stats.processed or 0 if xx == 0 and yy == 0 then return "" end + -- local status_icon + -- if opts.completed then + -- status_icon = "✔️" + -- else + -- status_icon = "*" + -- end + return string.format("%s / %s", xx, yy) end, }, diff --git a/lua/telescope/entry_manager.lua b/lua/telescope/entry_manager.lua index ff8b9ba..5055171 100644 --- a/lua/telescope/entry_manager.lua +++ b/lua/telescope/entry_manager.lua @@ -155,7 +155,10 @@ function EntryManager:add_entry(picker, score, entry) 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 + + if score < 1 and container[2] == score and #entry.ordinal < #container[1].ordinal then return self:_insert_container_before(picker, index, node, new_container) end @@ -174,11 +177,13 @@ function EntryManager:add_entry(picker, score, entry) end function EntryManager:iter() - return coroutine.wrap(function() - for val in self.linked_states:iter() do - coroutine.yield(val[1]) + local iterator = self.linked_states:iter() + return function() + local val = iterator() + if val then + return val[1] end - end) + end end return EntryManager diff --git a/lua/telescope/finders.lua b/lua/telescope/finders.lua index 7fecd67..ea4b88b 100644 --- a/lua/telescope/finders.lua +++ b/lua/telescope/finders.lua @@ -2,12 +2,10 @@ local Job = require "plenary.job" local make_entry = require "telescope.make_entry" local log = require "telescope.log" -local a = require "plenary.async_lib" -local await = a.await 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 async_job_finder = require "telescope.finders.async_job_finder" local finders = {} @@ -103,7 +101,7 @@ function JobFinder:_find(prompt, process_result, process_complete) enable_recording = false, on_stdout = on_output, - on_stderr = on_output, + -- on_stderr = on_output, on_exit = function() process_complete() @@ -131,17 +129,15 @@ function DynamicFinder:new(opts) end function DynamicFinder:_find(prompt, process_result, process_complete) - a.scope(function() - local results = await(self.fn(prompt)) + local results = self.fn(prompt) - for _, result in ipairs(results) do - if process_result(self.entry_maker(result)) then - return - end + for _, result in ipairs(results) do + if process_result(self.entry_maker(result)) then + return end + end - process_complete() - end) + process_complete() end --- Return a new Finder @@ -154,31 +150,18 @@ finders._new = function(opts) 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) - if command_list == nil then - return nil - end - - local command = table.remove(command_list, 1) +finders.new_async_job = function(opts) + if opts.writer then + return finders._new(opts) + end - return { - command = command, - args = command_list, - } - end, + return async_job_finder(opts) +end +finders.new_job = function(command_generator, entry_maker, _, cwd) + return async_job_finder { + command_generator = command_generator, entry_maker = entry_maker, - maximum_results = maximum_results, cwd = cwd, } end @@ -186,8 +169,8 @@ end --- 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 +-- @key entry_maker function Optional: function(line: string) => table +-- @key cwd string finders.new_oneshot_job = function(command_list, opts) opts = opts or {} diff --git a/lua/telescope/finders/async_job_finder.lua b/lua/telescope/finders/async_job_finder.lua index dfabf98..17799e8 100644 --- a/lua/telescope/finders/async_job_finder.lua +++ b/lua/telescope/finders/async_job_finder.lua @@ -1,14 +1,11 @@ -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 async_job = require "telescope._" +local LinesPipe = require("telescope._").LinesPipe local make_entry = require "telescope.make_entry" +local log = require "telescope.log" return function(opts) + log.trace("Creating async_job:", 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) @@ -18,58 +15,61 @@ return function(opts) local command = table.remove(command_list, 1) - return { + local res = { command = command, args = command_list, } + + return res 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 callable = function(_, prompt, process_result, process_complete) + if job then + job:close(true) + 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 + local job_opts = fn_command(prompt) + if not job_opts then + return + 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, + local writer = nil + -- if job_opts.writer and Job.is_job(job_opts.writer) then + -- writer = job_opts.writer + if opts.writer then + error "async_job_finder.writer is not yet implemented" + writer = async_job.writer(opts.writer) + end + + local stdout = LinesPipe() - on_stdout = vim.schedule_wrap(function(_, line) - if not line or line == "" then - return - end + job = async_job.spawn { + command = job_opts.command, + args = job_opts.args, + cwd = job_opts.cwd or opts.cwd, + writer = writer, - -- TODO: shutdown job here. - process_result(entry_maker(line)) - end), + stdout = stdout, + } - on_exit = function() - process_complete() - end, - } + for line in stdout:iter(true) do + if process_result(entry_maker(line)) then + return + end + end - job:start() - end)), + process_complete() + end + + return setmetatable({ + close = function() + if job then + job:close(true) + end + end, + }, { + __call = callable, }) end diff --git a/lua/telescope/finders/async_oneshot_finder.lua b/lua/telescope/finders/async_oneshot_finder.lua index 20e5403..19c2195 100644 --- a/lua/telescope/finders/async_oneshot_finder.lua +++ b/lua/telescope/finders/async_oneshot_finder.lua @@ -1,14 +1,9 @@ -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 async = require "plenary.async" +local async_job = require "telescope._" +local LinesPipe = require("telescope._").LinesPipe local make_entry = require "telescope.make_entry" -local Job = require "plenary.job" - return function(opts) opts = opts or {} @@ -21,64 +16,65 @@ return function(opts) local job_started = false local job_completed = false + local stdout = nil + return setmetatable({ - close = function() - results = {} - job_started = false - end, + -- close = function() results = {}; job_started = false end, + close = function() end, results = results, }, { - __call = void(async(function(_, prompt, process_result, process_complete) + __call = 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 + -- TODO: Handle writers. + -- 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 { + stdout = LinesPipe() + local _ = async_job.spawn { 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, + cwd = cwd, + + stdout = stdout, } - job:start() job_started = true end + if not job_completed then + for line in stdout:iter(true) do + num_results = num_results + 1 + + local v = entry_maker(line) + results[num_results] = v + process_result(v) + end + + process_complete() + job_completed = true + + return + end + local current_count = num_results for index = 1, current_count do + -- TODO: Figure out scheduling... + async.util.scheduler() + 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, }) end diff --git a/lua/telescope/finders/async_static_finder.lua b/lua/telescope/finders/async_static_finder.lua index 065b687..941d858 100644 --- a/lua/telescope/finders/async_static_finder.lua +++ b/lua/telescope/finders/async_static_finder.lua @@ -1,7 +1,4 @@ -local async_lib = require "plenary.async_lib" -local async = async_lib.async -local await = async_lib.await -local void = async_lib.void +local scheduler = require("plenary.async").util.scheduler local make_entry = require "telescope.make_entry" @@ -29,18 +26,18 @@ return function(opts) results = results, close = function() end, }, { - __call = void(async(function(_, _, process_result, process_complete) + __call = 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()) + scheduler() end end process_complete() - end)), + end, }) end diff --git a/lua/telescope/init.lua b/lua/telescope/init.lua index 17cd910..7a740a3 100644 --- a/lua/telescope/init.lua +++ b/lua/telescope/init.lua @@ -1,5 +1,3 @@ -require "telescope._compat" - local _extensions = require "telescope._extensions" local telescope = {} diff --git a/lua/telescope/path.lua b/lua/telescope/path.lua deleted file mode 100644 index 744c2fa..0000000 --- a/lua/telescope/path.lua +++ /dev/null @@ -1,103 +0,0 @@ -local log = require "telescope.log" - -local path = {} - -path.separator = package.config:sub(1, 1) -path.home = vim.fn.expand "~" - -path.make_relative = function(filepath, cwd) - if not cwd or not filepath then - return filepath - end - - if filepath:sub(1, #cwd) == cwd then - local offset = 0 - -- if cwd does ends in the os separator, we need to take it off - if cwd:sub(#cwd, #cwd) ~= path.separator then - offset = 1 - end - - filepath = filepath:sub(#cwd + 1 + offset, #filepath) - end - - return filepath -end - -path.shorten = (function() - if jit then - local ffi = require "ffi" - ffi.cdef [[ - typedef unsigned char char_u; - char_u *shorten_dir(char_u *str); - ]] - - return function(filepath) - if not filepath then - return filepath - end - - local c_str = ffi.new("char[?]", #filepath + 1) - ffi.copy(c_str, filepath) - return ffi.string(ffi.C.shorten_dir(c_str)) - end - else - return function(filepath) - return filepath - end - end -end)() - -path.normalize = function(filepath, cwd) - filepath = path.make_relative(filepath, cwd) - - -- Substitute home directory w/ "~" - filepath = filepath:gsub("^" .. path.home, "~", 1) - - -- Remove double path separators, it's annoying - filepath = filepath:gsub(path.separator .. path.separator, path.separator) - - return filepath -end - -path.read_file = function(filepath) - local fd = vim.loop.fs_open(filepath, "r", 438) - if fd == nil then - return "" - end - local stat = assert(vim.loop.fs_fstat(fd)) - if stat.type ~= "file" then - return "" - end - local data = assert(vim.loop.fs_read(fd, stat.size, 0)) - assert(vim.loop.fs_close(fd)) - return data -end - -path.read_file_async = function(filepath, callback) - vim.loop.fs_open(filepath, "r", 438, function(err_open, fd) - if err_open then - print("We tried to open this file but couldn't. We failed with following error message: " .. err_open) - return - end - vim.loop.fs_fstat(fd, function(err_fstat, stat) - assert(not err_fstat, err_fstat) - if stat.type ~= "file" then - return callback "" - end - vim.loop.fs_read(fd, stat.size, 0, function(err_read, data) - assert(not err_read, err_read) - vim.loop.fs_close(fd, function(err_close) - assert(not err_close, err_close) - return callback(data) - end) - end) - end) - end) -end - -return setmetatable({}, { - __index = function(_, k) - log.error "telescope.path is deprecated. please use plenary.path instead" - return path[k] - end, -}) diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index 646af03..f8e48f1 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -1,15 +1,12 @@ +require "telescope" + local a = vim.api -local async_lib = require "plenary.async_lib" -local async_util = async_lib.util +local async = require "plenary.async" +local await_schedule = async.util.scheduler +local channel = require("plenary.async.control").channel local popup = require "plenary.popup" -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" @@ -70,12 +67,13 @@ function Picker:new(opts) selection_caret = get_default(opts.selection_caret, config.values.selection_caret), entry_prefix = get_default(opts.entry_prefix, config.values.entry_prefix), initial_mode = get_default(opts.initial_mode, config.values.initial_mode), + debounce = get_default(tonumber(opts.debounce), nil), default_text = opts.default_text, get_status_text = get_default(opts.get_status_text, config.values.get_status_text), _on_input_filter_cb = opts.on_input_filter_cb or function() end, - finder = opts.finder, + finder = assert(opts.finder, "Finder is required."), sorter = opts.sorter or require("telescope.sorters").empty(), all_previewers = opts.previewer, @@ -228,7 +226,7 @@ function Picker:highlight_displayed_rows(results_bufnr, prompt) end function Picker:highlight_one_row(results_bufnr, prompt, display, row) - local highlights = self:_track("_highlight_time", self.sorter.highlighter, self.sorter, prompt, display) + local highlights = self.sorter:highlighter(prompt, display) if highlights then for _, hl in ipairs(highlights) do @@ -274,8 +272,6 @@ function Picker:find() self:close_existing_pickers() self:reset_selection() - assert(self.finder, "Finder is required to do picking") - self.original_win_id = a.nvim_get_current_win() -- User autocmd run it before create Telescope window @@ -346,26 +342,50 @@ function Picker:find() self.prompt_prefix = prompt_prefix self:_reset_prefix_color() - -- Temporarily disabled: Draw the screen ASAP. This makes things feel speedier. - -- vim.cmd [[redraw]] - -- First thing we want to do is set all the lines to blank. self.max_results = popup_opts.results.height + -- TODO(scrolling): This may be a hack when we get a little further into implementing scrolling. vim.api.nvim_buf_set_lines(results_bufnr, 0, self.max_results, false, utils.repeated_table(self.max_results, "")) + -- TODO(status): I would love to get the status text not moving back and forth. Perhaps it is just a problem with + -- virtual text & prompt buffers or something though. I can't figure out why it would redraw the way it does. + -- + -- A "hacked" version of this would be to calculate where the area I want the status to go and put a new window there. + -- With this method, I do not need to worry about padding or antying, just make it take up X characters or something. 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 local tx, rx = channel.mpsc() self.__on_lines = tx.send - local main_loop = async(function() + local find_id = self:_next_find_id() + + local main_loop = async.void(function() + self.sorter:_init() + + -- Do filetype last, so that users can register at the last second. + pcall(a.nvim_buf_set_option, prompt_bufnr, "filetype", "TelescopePrompt") + pcall(a.nvim_buf_set_option, results_bufnr, "filetype", "TelescopeResults") + + -- TODO(async): I wonder if this should actually happen _before_ we nvim_buf_attach. + -- This way the buffer would always start with what we think it should when we start the loop. + if self.default_text then + self:set_prompt(self.default_text) + end + + if self.initial_mode == "insert" then + vim.cmd [[startinsert!]] + elseif self.initial_mode ~= "normal" then + error("Invalid setting for initial_mode: " .. self.initial_mode) + end + + await_schedule() + while true do - await(async_lib.scheduler()) + -- Wait for the next input + rx.last() - local _, _, _, first_line, last_line = await(rx.last()) self:_reset_track() if not vim.api.nvim_buf_is_valid(prompt_bufnr) then @@ -373,74 +393,58 @@ 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, self:_get_prompt()) - return - end - - local original_prompt = self:_get_prompt() - local on_input_result = self._on_input_filter_cb(original_prompt) or {} + local start_time = vim.loop.hrtime() - local prompt = on_input_result.prompt or original_prompt - local finder = on_input_result.updated_finder + local prompt = self:_get_prompt() + local on_input_result = self._on_input_filter_cb(prompt) or {} - if finder then - self.finder:close() - self.finder = finder + local new_prompt = on_input_result.prompt + if new_prompt then + prompt = new_prompt end - if self.sorter then - self.sorter:_start(prompt) + local new_finder = on_input_result.updated_finder + if new_finder then + self.finder:close() + self.finder = new_finder end - -- TODO: Entry manager should have a "bulk" setter. This can prevent a lot of redraws from display + self.sorter:_start(prompt) 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)) + self.finder(prompt, process_result, process_complete) end) if not ok then - log.warn("Failed with msg: ", msg) + log.warn("Finder failed with msg: ", msg) + end + + local diff_time = (vim.loop.hrtime() - start_time) / 1e6 + if self.debounce and diff_time < self.debounce then + async.util.sleep(self.debounce - diff_time) end end end) -- Register attach vim.api.nvim_buf_attach(prompt_bufnr, false, { - on_lines = tx.send, - on_detach = function() - -- TODO: Can we add a "cleanup" / "teardown" function that completely removes these. - self.finder = nil - self.previewer = nil - self.sorter = nil - self.manager = nil + on_lines = function(...) + find_id = self:_next_find_id() - self.closed = true + self._result_completed = false + status_updater { completed = false } - -- TODO: Should we actually do this? - collectgarbage() - collectgarbage() + tx.send(...) + end, + on_detach = function() + self:_detach() end, }) - if self.sorter then - self.sorter:_init() - end - async_lib.run(main_loop()) - status_updater() - -- TODO: Use WinLeave as well? local on_buf_leave = string.format( [[ autocmd BufLeave <buffer> ++nested ++once :silent lua require('telescope.pickers').on_close_prompt(%s)]], @@ -480,19 +484,8 @@ function Picker:find() mappings.apply_keymap(prompt_bufnr, self.attach_mappings, config.values.mappings) - -- Do filetype last, so that users can register at the last second. - pcall(a.nvim_buf_set_option, prompt_bufnr, "filetype", "TelescopePrompt") - pcall(a.nvim_buf_set_option, results_bufnr, "filetype", "TelescopeResults") - - if self.default_text then - self:set_prompt(self.default_text) - end - - if self.initial_mode == "insert" then - vim.cmd [[startinsert!]] - elseif self.initial_mode ~= "normal" then - error("Invalid setting for initial_mode: " .. self.initial_mode) - end + tx.send() + main_loop() end function Picker:hide_preview() @@ -791,9 +784,7 @@ function Picker:set_selection(row) end local caret = self.selection_caret - -- local display = string.format('%s %s', caret, - -- (a.nvim_buf_get_lines(results_bufnr, row, row + 1, false)[1] or ''):sub(3) - -- ) + local display, display_highlights = entry_display.resolve(self, entry) display = caret .. display @@ -939,23 +930,6 @@ function Picker:_reset_track() self.stats.highlights = 0 end -function Picker:_track(key, func, ...) - local start, final - if self.track then - start = vim.loop.hrtime() - end - - -- Hack... we just do this so that we can track stuff that returns two values. - local res1, res2 = func(...) - - if self.track then - final = vim.loop.hrtime() - self.stats[key] = final - start + self.stats[key] - end - - return res1, res2 -end - function Picker:_increment(key) self.stats[key] = (self.stats[key] or 0) + 1 end @@ -987,12 +961,12 @@ function Picker:close_existing_pickers() end function Picker:get_status_updater(prompt_win, prompt_bufnr) - return function() - local text = self:get_status_text() + return function(opts) if self.closed or not vim.api.nvim_buf_is_valid(prompt_bufnr) then return end - local current_prompt = vim.api.nvim_buf_get_lines(prompt_bufnr, 0, 1, false)[1] + + local current_prompt = self:_get_prompt() if not current_prompt then return end @@ -1001,10 +975,11 @@ function Picker:get_status_updater(prompt_win, prompt_bufnr) return end - local prompt_len = #current_prompt + local text = self:get_status_text(opts) + local prompt_len = #self.prompt_prefix + #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) + local padding = string.rep(" ", vim.api.nvim_win_get_width(prompt_win) - prompt_len - #text) + vim.api.nvim_buf_clear_namespace(prompt_bufnr, ns_telescope_prompt, 0, -1) vim.api.nvim_buf_set_virtual_text(prompt_bufnr, ns_telescope_prompt, 0, { { padding .. text, "NonText" } }, {}) -- TODO: Wait for bfredl @@ -1022,7 +997,7 @@ end 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() + status_updater { completed = false } end local cb_filter = function(_) @@ -1030,7 +1005,7 @@ function Picker:get_result_processor(find_id, prompt, status_updater) end return function(entry) - if find_id ~= self._find_id or self.closed or self:is_done() then + if find_id ~= self._find_id then return true end @@ -1059,61 +1034,62 @@ function Picker:get_result_processor(find_id, prompt, status_updater) end function Picker:get_result_completor(results_bufnr, find_id, prompt, status_updater) - return function() + return vim.schedule_wrap(function() if self.closed == true or self:is_done() then return end - local selection_strategy = self.selection_strategy or "reset" + self:_do_selection(prompt) - -- TODO: Either: always leave one result or make sure we actually clean up the results when nothing matches - if selection_strategy == "row" then - if self._selection_row == nil and self.default_selection_index ~= nil then - self:set_selection(self:get_row(self.default_selection_index)) - else - self:set_selection(self:get_selection_row()) - end - elseif selection_strategy == "follow" then - if self._selection_row == nil and self.default_selection_index ~= nil then - self:set_selection(self:get_row(self.default_selection_index)) - else - local index = self.manager:find_entry(self:get_selection()) + state.set_global_key("current_line", self:_get_prompt()) + status_updater { completed = true } - if index then - local follow_row = self:get_row(index) - self:set_selection(follow_row) - else - self:set_selection(self:get_reset_row()) - end - end - elseif selection_strategy == "reset" then - if self.default_selection_index ~= nil then - self:set_selection(self:get_row(self.default_selection_index)) - else - self:set_selection(self:get_reset_row()) - end - elseif selection_strategy == "closest" then - if prompt == "" and self.default_selection_index ~= nil then - self:set_selection(self:get_row(self.default_selection_index)) + self:clear_extra_rows(results_bufnr) + self:highlight_displayed_rows(results_bufnr, prompt) + self.sorter:_finish(prompt) + + self:_on_complete() + + self._result_completed = true + end) +end + +function Picker:_do_selection(prompt) + local selection_strategy = self.selection_strategy or "reset" + -- TODO: Either: always leave one result or make sure we actually clean up the results when nothing matches + if selection_strategy == "row" then + if self._selection_row == nil and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + self:set_selection(self:get_selection_row()) + end + elseif selection_strategy == "follow" then + if self._selection_row == nil and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + local index = self.manager:find_entry(self:get_selection()) + + if index then + local follow_row = self:get_row(index) + self:set_selection(follow_row) else self:set_selection(self:get_reset_row()) end + end + elseif selection_strategy == "reset" then + if self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) else - error("Unknown selection strategy: " .. selection_strategy) + self:set_selection(self:get_reset_row()) end - - local current_line = vim.api.nvim_get_current_line():sub(self.prompt_prefix:len() + 1) - state.set_global_key("current_line", current_line) - - status_updater() - - self:clear_extra_rows(results_bufnr) - self:highlight_displayed_rows(results_bufnr, prompt) - if self.sorter then - self.sorter:_finish(prompt) + elseif selection_strategy == "closest" then + if prompt == "" and self.default_selection_index ~= nil then + self:set_selection(self:get_row(self.default_selection_index)) + else + self:set_selection(self:get_reset_row()) end - - self:_on_complete() + else + error("Unknown selection strategy: " .. selection_strategy) end end @@ -1169,6 +1145,18 @@ function Picker:_reset_highlights() self.highlighter:clear_display() end +function Picker:_detach() + self.finder:close() + + -- TODO: Can we add a "cleanup" / "teardown" function that completely removes these. + -- self.finder = nil + -- self.previewer = nil + -- self.sorter = nil + -- self.manager = nil + + self.closed = true +end + pickers._Picker = Picker return pickers diff --git a/lua/telescope/pickers/highlights.lua b/lua/telescope/pickers/highlights.lua index 1be289e..e7b99e1 100644 --- a/lua/telescope/pickers/highlights.lua +++ b/lua/telescope/pickers/highlights.lua @@ -89,7 +89,7 @@ function Highlighter:hi_multiselect(row, is_selected) -- This is still kind of weird to me, since it seems like I'm erasing stuff -- when i shouldn't... perhaps it's a bout the gravity of the extmark? if #existing_marks > 0 then - log.trace("Clearning row: ", row) + log.trace("Clearning highlight multi select row: ", row) vim.api.nvim_buf_clear_namespace(results_bufnr, ns_telescope_multiselection, row, row + 1) end diff --git a/lua/telescope/sorters.lua b/lua/telescope/sorters.lua index 8874287..ba38e2d 100644 --- a/lua/telescope/sorters.lua +++ b/lua/telescope/sorters.lua @@ -96,10 +96,10 @@ function Sorter:_start(prompt) local len_previous = #previous if #prompt < len_previous then - log.debug "Reset discard because shorter prompt" + log.trace "Reset discard because shorter prompt" self._discard_state.filtered = {} elseif string.sub(prompt, 1, len_previous) ~= previous then - log.debug "Reset discard no match" + log.trace "Reset discard no match" self._discard_state.filtered = {} end @@ -167,11 +167,10 @@ end sorters.Sorter = Sorter -TelescopeCachedTails = TelescopeCachedTails or nil -if not TelescopeCachedTails then +local make_cached_tail = function() local os_sep = util.get_separator() local match_string = "[^" .. os_sep .. "]*$" - TelescopeCachedTails = setmetatable({}, { + return setmetatable({}, { __index = function(t, k) local tail = string.match(k, match_string) @@ -181,8 +180,8 @@ if not TelescopeCachedTails then }) end -TelescopeCachedUppers = TelescopeCachedUppers - or setmetatable({}, { +local make_cached_uppers = function() + return setmetatable({}, { __index = function(t, k) local obj = {} for i = 1, #k do @@ -196,8 +195,7 @@ TelescopeCachedUppers = TelescopeCachedUppers return obj end, }) - -TelescopeCachedNgrams = TelescopeCachedNgrams or {} +end -- TODO: Match on upper case words -- TODO: Match on last match @@ -206,9 +204,11 @@ sorters.get_fuzzy_file = function(opts) local ngram_len = opts.ngram_len or 2 + local cached_ngrams = {} + local function overlapping_ngrams(s, n) - if TelescopeCachedNgrams[s] and TelescopeCachedNgrams[s][n] then - return TelescopeCachedNgrams[s][n] + if cached_ngrams[s] and cached_ngrams[s][n] then + return cached_ngrams[s][n] end local R = {} @@ -216,15 +216,18 @@ sorters.get_fuzzy_file = function(opts) R[#R + 1] = s:sub(i, i + n - 1) end - if not TelescopeCachedNgrams[s] then - TelescopeCachedNgrams[s] = {} + if not cached_ngrams[s] then + cached_ngrams[s] = {} end - TelescopeCachedNgrams[s][n] = R + cached_ngrams[s][n] = R return R end + local cached_tails = make_cached_tail() + local cached_uppers = make_cached_uppers() + return Sorter:new { scoring_function = function(_, prompt, line) local N = #prompt @@ -243,8 +246,8 @@ sorters.get_fuzzy_file = function(opts) -- Contains the original string local contains_string = line_lower:find(prompt_lower, 1, true) - local prompt_uppers = TelescopeCachedUppers[prompt] - local line_uppers = TelescopeCachedUppers[line] + local prompt_uppers = cached_uppers[prompt] + local line_uppers = cached_uppers[line] local uppers_matching = 0 for k, _ in pairs(prompt_uppers) do @@ -254,7 +257,7 @@ sorters.get_fuzzy_file = function(opts) end -- TODO: Consider case senstivity - local tail = TelescopeCachedTails[line_lower] + local tail = cached_tails[line_lower] local contains_tail = tail:find(prompt, 1, true) local consecutive_matches = 0 @@ -313,9 +316,10 @@ sorters.get_generic_fuzzy_sorter = function(opts) local ngram_len = opts.ngram_len or 2 + local cached_ngrams = {} local function overlapping_ngrams(s, n) - if TelescopeCachedNgrams[s] and TelescopeCachedNgrams[s][n] then - return TelescopeCachedNgrams[s][n] + if cached_ngrams[s] and cached_ngrams[s][n] then + return cached_ngrams[s][n] end local R = {} @@ -323,11 +327,11 @@ sorters.get_generic_fuzzy_sorter = function(opts) R[#R + 1] = s:sub(i, i + n - 1) end - if not TelescopeCachedNgrams[s] then - TelescopeCachedNgrams[s] = {} + if not cached_ngrams[s] then + cached_ngrams[s] = {} end - TelescopeCachedNgrams[s][n] = R + cached_ngrams[s][n] = R return R end @@ -462,6 +466,9 @@ sorters.get_fzy_sorter = function(opts) } end +-- TODO: Could probably do something nice where we check their conf +-- and choose their default for this. +-- But I think `fzy` is good default for now. sorters.highlighter_only = function(opts) opts = opts or {} local fzy = opts.fzy_mod or require "telescope.algos.fzy" |
