From 5d37c3ea08f40d8c9d3a9ebcc72bd641d366c110 Mon Sep 17 00:00:00 2001 From: fdschmidt93 <39233597+fdschmidt93@users.noreply.github.com> Date: Wed, 1 Sep 2021 18:17:18 +0200 Subject: feat: allow caching and resuming picker (#1051) * expose `cache_picker` in telescope.setup to configure caching, see `:h telescope.defaults.cache_picker` * add builtin.resume and builtin.pickers picker --- lua/telescope/actions/init.lua | 15 +++++ lua/telescope/algos/linked_list.lua | 37 ++++++++++ lua/telescope/builtin/init.lua | 23 +++++-- lua/telescope/builtin/internal.lua | 76 +++++++++++++++++++++ lua/telescope/config.lua | 40 ++++++++++- lua/telescope/make_entry.lua | 26 +++++++ lua/telescope/pickers.lua | 97 ++++++++++++++++++++++----- lua/telescope/previewers/buffer_previewer.lua | 79 ++++++++++++++++++++++ lua/telescope/previewers/init.lua | 1 + 9 files changed, 372 insertions(+), 22 deletions(-) (limited to 'lua') diff --git a/lua/telescope/actions/init.lua b/lua/telescope/actions/init.lua index cda5f1d..aa930dc 100644 --- a/lua/telescope/actions/init.lua +++ b/lua/telescope/actions/init.lua @@ -832,6 +832,21 @@ actions.cycle_previewers_prev = function(prompt_bufnr) actions.get_current_picker(prompt_bufnr):cycle_previewers(-1) end +--- Removes the selected picker in |builtin.pickers|.
+--- This action is not mapped by default and only intended for |builtin.pickers|. +---@param prompt_bufnr number: The prompt bufnr +actions.remove_selected_picker = function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + local selection_index = current_picker:get_index(current_picker:get_selection_row()) + local cached_pickers = state.get_global_key "cached_pickers" + current_picker:delete_selection(function() + table.remove(cached_pickers, selection_index) + end) + if #cached_pickers == 0 then + actions.close(prompt_bufnr) + end +end + -- ================================================== -- Transforms modules and sets the corect metatables. -- ================================================== diff --git a/lua/telescope/algos/linked_list.lua b/lua/telescope/algos/linked_list.lua index 6015e1e..2da6a6e 100644 --- a/lua/telescope/algos/linked_list.lua +++ b/lua/telescope/algos/linked_list.lua @@ -215,4 +215,41 @@ function LinkedList:ipairs() end end +function LinkedList:truncate(max_results) + if max_results >= self.size then + return + end + + local current_node + if max_results < self.size - max_results then + local index = 1 + current_node = self.head + while index < max_results do + local node = current_node + if not node.next then + break + end + current_node = current_node.next + index = index + 1 + end + self.size = max_results + else + current_node = self.tail + while self.size > max_results do + if current_node.prev == nil then + break + end + current_node = current_node.prev + self.size = self.size - 1 + end + end + self.tail = current_node + self.tail.next = nil + if max_results < self.track_at then + self.track_at = max_results + self.tracked = current_node.item + self._tracked_node = current_node + end +end + return LinkedList diff --git a/lua/telescope/builtin/init.lua b/lua/telescope/builtin/init.lua index ffc217e..6cc24b4 100644 --- a/lua/telescope/builtin/init.lua +++ b/lua/telescope/builtin/init.lua @@ -68,7 +68,7 @@ local builtin = {} --- Search for a string and get results live as you type (respecting .gitignore) ---@param opts table: options to pass to the picker ----@field cwd string: directory path to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) +---@field cwd string: root dir to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) ---@field grep_open_files boolean: if true, restrict search to open files only, mutually exclusive with `search_dirs` ---@field search_dirs table: directory/directories to search in, mutually exclusive with `grep_open_files` ---@field additional_args function: function(opts) which returns a table of additional arguments to be passed on @@ -76,7 +76,7 @@ builtin.live_grep = require("telescope.builtin.files").live_grep --- Searches for the string under your cursor in your current working directory ---@param opts table: options to pass to the picker ----@field cwd string: directory path to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) +---@field cwd string: root dir to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) ---@field search string: the query to search ---@field search_dirs table: directory/directories to search in ---@field use_regex boolean: if true, special characters won't be escaped, allows for using regex (default is false) @@ -85,7 +85,7 @@ builtin.grep_string = require("telescope.builtin.files").grep_string --- Search for files (respecting .gitignore) ---@param opts table: options to pass to the picker ----@field cwd string: directory path to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) +---@field cwd string: root dir to search from (default is cwd, use utils.buffer_dir() to search relative to open buffer) ---@field find_command table: command line arguments for `find_files` to use for the search, overrides default config ---@field follow boolean: if true, follows symlinks (i.e. uses `-L` flag for the `find` command) ---@field hidden boolean: determines whether to show hidden files or not (default is false) @@ -105,7 +105,7 @@ builtin.fd = builtin.find_files --- create the file `init.lua` inside of `lua/telescope` and will create the necessary folders (similar to how --- `mkdir -p` would work) if they do not already exist ---@param opts table: options to pass to the picker ----@field cwd string: directory path to browse (default is cwd, use utils.buffer_dir() to browse relative to open buffer) +---@field cwd string: root dir to browse from (default is cwd, use utils.buffer_dir() to search relative to open buffer) ---@field depth number: file tree depth to display (default is 1) ---@field dir_icon string: change the icon for a directory. default:  ---@field hidden boolean: determines whether to show hidden files or not (default is false) @@ -243,6 +243,21 @@ builtin.command_history = require("telescope.builtin.internal").command_history ---@param opts table: options to pass to the picker builtin.search_history = require("telescope.builtin.internal").search_history +--- Opens the previous picker in the identical state (incl. multi selections) +--- - Notes: +--- - Requires `cache_picker` in setup or when having invoked pickers, see |telescope.defaults.cache_picker| +---@param opts table: options to pass to the picker +---@field cache_index number: what picker to resume, where 1 denotes most recent (default 1) +builtin.resume = require("telescope.builtin.internal").resume + +--- Opens a picker over previously cached pickers in there preserved states (incl. multi selections) +--- - Default keymaps: +--- - ``: delete the selected cached picker +--- - Notes: +--- - Requires `cache_picker` in setup or when having invoked pickers, see |telescope.defaults.cache_picker| +---@param opts table: options to pass to the picker +builtin.pickers = require("telescope.builtin.internal").pickers + --- Lists vim options, allows you to edit the current value on `` ---@param opts table: options to pass to the picker builtin.vim_options = require("telescope.builtin.internal").vim_options diff --git a/lua/telescope/builtin/internal.lua b/lua/telescope/builtin/internal.lua index b71811e..cbacf97 100644 --- a/lua/telescope/builtin/internal.lua +++ b/lua/telescope/builtin/internal.lua @@ -6,7 +6,9 @@ local make_entry = require "telescope.make_entry" local Path = require "plenary.path" local pickers = require "telescope.pickers" local previewers = require "telescope.previewers" +local p_window = require "telescope.pickers.window" local sorters = require "telescope.sorters" +local state = require "telescope.state" local utils = require "telescope.utils" local conf = require("telescope.config").values @@ -71,6 +73,80 @@ internal.builtin = function(opts) }):find() end +internal.resume = function(opts) + opts = opts or {} + opts.cache_index = vim.F.if_nil(opts.cache_index, 1) + + local cached_pickers = state.get_global_key "cached_pickers" + if cached_pickers == nil or vim.tbl_isempty(cached_pickers) then + print "No picker(s) cached." + return + end + local picker = cached_pickers[opts.cache_index] + if picker == nil then + print("Index too large as there are only %s pickers cached", #cached_pickers) + return + end + -- reset layout strategy and get_window_options if default as only one is valid + -- and otherwise unclear which was actually set + if picker.layout_strategy == conf.layout_strategy then + picker.layout_strategy = nil + end + if picker.get_window_options == p_window.get_window_options then + picker.get_window_options = nil + end + picker.cache_picker.index = opts.cache_index + + -- avoid partial `opts.cache_picker` at picker creation + if opts.cache_picker ~= false then + picker.cache_picker = vim.tbl_extend("keep", opts.cache_picker or {}, picker.cache_picker) + else + picker.cache_picker.disabled = true + end + opts.cache_picker = nil + pickers.new(opts, picker):find() +end + +internal.pickers = function(opts) + local cached_pickers = state.get_global_key "cached_pickers" + if cached_pickers == nil or vim.tbl_isempty(cached_pickers) then + print "No picker(s) cached." + return + end + + opts = opts or {} + + -- clear cache picker for immediate pickers.new and pass option to resumed picker + if opts.cache_picker ~= nil then + opts._cache_picker = opts.cache_picker + opts.cache_picker = nil + end + + pickers.new(opts, { + prompt_title = "Pickers", + finder = finders.new_table { + results = cached_pickers, + entry_maker = make_entry.gen_from_picker(opts), + }, + previewer = previewers.pickers.new(opts), + sorter = conf.generic_sorter(opts), + cache_picker = false, + attach_mappings = function(_, map) + actions.select_default:replace(function(prompt_bufnr) + local current_picker = action_state.get_current_picker(prompt_bufnr) + local selection_index = current_picker:get_index(current_picker:get_selection_row()) + actions._close(prompt_bufnr, cached_pickers[selection_index].initial_mode == "insert") + opts.cache_picker = opts._cache_picker + opts["cache_index"] = selection_index + internal.resume(opts) + end) + map("i", "", actions.remove_selected_picker) + map("n", "", actions.remove_selected_picker) + return true + end, + }):find() +end + internal.planets = function(opts) local show_pluto = opts.show_pluto or false diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua index a4899af..d58b94e 100644 --- a/lua/telescope/config.lua +++ b/lua/telescope/config.lua @@ -46,11 +46,22 @@ local smarter_depth_2_extend = function(priority, base) return result end +local resolve_table_opts = function(priority, base) + if priority == false or (priority == nil and base == false) then + return false + end + if priority == nil and type(base) == "table" then + return base + end + return smarter_depth_2_extend(priority, base) +end + -- TODO: Add other major configuration points here. -- selection_strategy local config = {} config.smarter_depth_2_extend = smarter_depth_2_extend +config.resolve_table_opts = resolve_table_opts config.values = _TelescopeConfigurationValues config.descriptions = {} @@ -286,6 +297,33 @@ local telescope_defaults = { ]], }, + cache_picker = { + { + num_pickers = 1, + limit_entries = 1000, + }, + [[ + This field handles the configuration for picker caching. + By default it is a table, with default values (more below). + To disable caching, set it to false. + + Caching preserves all previous multi selections and results and + therefore may result in slowdown or increased RAM occupation + if too many pickers (`cache_picker.num_pickers`) or entries + ('cache_picker.limit_entries`) are cached. + + Fields: + - num_pickers: The number of pickers to be cached. + Set to -1 to preserve all pickers of your session. + If passed to a picker, the cached pickers with + indices larger than `cache_picker.num_pickers` will + be cleared. + Default: 1 + - limit_entries: The amount of entries that will be written in the + Default: 1000 + ]], + }, + -- Builtin configuration -- List that will be executed. @@ -416,7 +454,7 @@ function config.set_defaults(user_defaults, tele_defaults) vim.tbl_deep_extend("keep", if_nil(config.values[name], {}), if_nil(default_val, {})) ) end - if name == "history" then + if name == "history" or name == "cache_picker" then if user_defaults[name] == false or config.values[name] == false then return false end diff --git a/lua/telescope/make_entry.lua b/lua/telescope/make_entry.lua index e2c6d45..14d5ecb 100644 --- a/lua/telescope/make_entry.lua +++ b/lua/telescope/make_entry.lua @@ -662,6 +662,32 @@ function make_entry.gen_from_highlights() end end +function make_entry.gen_from_picker(opts) + local displayer = entry_display.create { + separator = " ", + items = { + { width = 30 }, + { remaining = true }, + }, + } + + local make_display = function(entry) + return displayer { + entry.value.prompt_title, + entry.value.default_text, + } + end + + return function(entry) + return { + value = entry, + text = entry.prompt_title, + ordinal = string.format("%s %s", entry.prompt_title, utils.get_default(entry.default_text, "")), + display = make_display, + } + end +end + function make_entry.gen_from_buffer_lines(opts) local displayer = entry_display.create { separator = " │ ", diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index 9e5f251..42e37d9 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -85,7 +85,11 @@ function Picker:new(opts) _find_id = 0, _completion_callbacks = {}, - _multi = MultiSelect:new(), + manager = (type(opts.manager) == "table" and getmetatable(opts.manger) == getmetatable(EntryManager)) + and opts.manager, + _multi = (type(opts._multi) == "table" and getmetatable(opts._multi) == getmetatable(MultiSelect:new())) + and opts._multi + or MultiSelect:new(), track = get_default(opts.track, false), stats = {}, @@ -104,6 +108,8 @@ function Picker:new(opts) border = get_default(opts.border, config.values.border), borderchars = get_default(opts.borderchars, config.values.borderchars), }, + + cache_picker = config.resolve_table_opts(opts.cache_picker, vim.deepcopy(config.values.cache_picker)), }, self) obj.get_window_options = opts.get_window_options or p_window.get_window_options @@ -332,6 +338,7 @@ function Picker:find() if prompt_border_win then vim.api.nvim_win_set_option(prompt_border_win, "winhl", "Normal:TelescopePromptBorder") end + self.prompt_bufnr = prompt_bufnr -- Prompt prefix local prompt_prefix = self.prompt_prefix @@ -416,23 +423,47 @@ function Picker:find() self.finder = new_finder end - self.sorter:_start(prompt) - self.manager = EntryManager:new(self.max_results, self.entry_adder, self.stats) + -- TODO: Entry manager should have a "bulk" setter. This can prevent a lot of redraws from display + if self.cache_picker == false or not (self.cache_picker.is_cached == true) then + self.sorter:_start(prompt) + self.manager = EntryManager:new(self.max_results, self.entry_adder, self.stats) - 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 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, process_complete) - end) + local ok, msg = pcall(function() + self.finder(prompt, process_result, process_complete) + end) - if not ok then - log.warn("Finder failed with msg: ", msg) - end + if not ok then + 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) + 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 + else + -- resume previous picker + local index = 1 + for entry in self.manager:iter() do + self:entry_adder(index, entry, _, true) + index = index + 1 + end + self.cache_picker.is_cached = false + -- if text changed, required to set anew to restart finder; otherwise hl and selection + if self.cache_picker.cached_prompt ~= self.default_text then + self:reset_prompt() + self:set_prompt(self.default_text) + else + -- scheduling required to apply highlighting and selection appropriately + await_schedule(function() + self:highlight_displayed_rows(self.results_bufnr, self.cache_picker.cached_prompt) + if self.cache_picker.selection_row ~= nil then + self:set_selection(self.cache_picker.selection_row) + end + end) + end end end end) @@ -444,7 +475,6 @@ function Picker:find() self._result_completed = false status_updater { completed = false } - tx.send(...) end, on_detach = function() @@ -463,8 +493,6 @@ function Picker:find() vim.cmd(on_buf_leave) vim.cmd [[augroup END]] - self.prompt_bufnr = prompt_bufnr - local preview_border = preview_opts and preview_opts.border self.preview_border = preview_border local preview_border_win = (preview_border and preview_border.win_id) and preview_border.win_id @@ -1124,6 +1152,41 @@ function pickers.on_close_prompt(prompt_bufnr) local status = state.get_status(prompt_bufnr) local picker = status.picker + if type(picker.cache_picker) == "table" then + local cached_pickers = state.get_global_key "cached_pickers" or {} + + if type(picker.cache_picker.index) == "number" then + if not vim.tbl_isempty(cached_pickers) then + table.remove(cached_pickers, picker.cache_picker.index) + end + end + + -- if picker was disabled post-hoc (e.g. `cache_picker = false` conclude after deletion) + if picker.cache_picker.disabled ~= true then + if picker.cache_picker.limit_entries > 0 then + -- edge case: starting in normal mode and not having run a search means having no manager instantiated + if picker.manager then + picker.manager.linked_states:truncate(picker.cache_picker.limit_entries) + else + picker.manager = EntryManager:new(picker.max_results, picker.entry_adder, picker.stats) + end + end + picker.default_text = picker:_get_prompt() + picker.cache_picker.selection_row = picker._selection_row + picker.cache_picker.cached_prompt = picker:_get_prompt() + picker.cache_picker.is_cached = true + table.insert(cached_pickers, 1, picker) + + -- release pickers + if picker.cache_picker.num_pickers > 0 then + while #cached_pickers > picker.cache_picker.num_pickers do + table.remove(cached_pickers, #cached_pickers) + end + end + state.set_global_key("cached_pickers", cached_pickers) + end + end + if picker.sorter then picker.sorter:_destroy() end diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index f6c4a78..59630b8 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -247,6 +247,8 @@ previewers.new_buffer_previewer = function(opts) buf_delete(bufnr) end end + -- enable resuming picker with existing previewer to avoid lookup of deleted bufs + bufname_table = {} end function opts.preview_fn(self, entry, status) @@ -853,6 +855,83 @@ previewers.highlights = defaulter(function(_) } end, {}) +previewers.pickers = defaulter(function(_) + local ns_telescope_multiselection = vim.api.nvim_create_namespace "telescope_mulitselection" + local get_row = function(picker, preview_height, index) + if picker.sorting_strategy == "ascending" then + return index - 1 + else + return preview_height - index + end + end + return previewers.new_buffer_previewer { + + dyn_title = function(_, entry) + if entry.value.default_text and entry.value.default_text ~= "" then + return string.format("%s ─ %s", entry.value.prompt_title, entry.value.default_text) + end + return entry.value.prompt_title + end, + + get_buffer_by_name = function(_, entry) + return tostring(entry.value.prompt_bufnr) + end, + + teardown = function(self) + if self.state and self.state.last_set_bufnr and vim.api.nvim_buf_is_valid(self.state.last_set_bufnr) then + vim.api.nvim_buf_clear_namespace(self.state.last_set_bufnr, ns_telescope_multiselection, 0, -1) + end + end, + + define_preview = function(self, entry, status) + putils.with_preview_window(status, nil, function() + local ns_telescope_entry = vim.api.nvim_create_namespace "telescope_entry" + local preview_height = vim.api.nvim_win_get_height(status.preview_win) + + if self.state.bufname then + return + end + + local picker = entry.value + -- prefill buffer to be able to set lines individually + local placeholder = utils.repeated_table(preview_height, "") + vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, placeholder) + + for index = 1, math.min(preview_height, picker.manager:num_results()) do + local row = get_row(picker, preview_height, index) + local e = picker.manager:get_entry(index) + local display, display_highlight = e:display() + + vim.api.nvim_buf_set_lines(self.state.bufnr, row, row + 1, false, { display }) + + if display_highlight ~= nil then + for _, hl_block in ipairs(display_highlight) do + vim.api.nvim_buf_add_highlight( + self.state.bufnr, + ns_telescope_entry, + hl_block[2], + row, + hl_block[1][1], + hl_block[1][2] + ) + end + end + if picker._multi:is_selected(e) then + vim.api.nvim_buf_add_highlight( + self.state.bufnr, + ns_telescope_multiselection, + "TelescopeMultiSelection", + row, + 0, + -1 + ) + end + end + end) + end, + } +end, {}) + previewers.display_content = defaulter(function(_) return previewers.new_buffer_previewer { define_preview = function(self, entry, status) diff --git a/lua/telescope/previewers/init.lua b/lua/telescope/previewers/init.lua index 1d7d63e..b749056 100644 --- a/lua/telescope/previewers/init.lua +++ b/lua/telescope/previewers/init.lua @@ -306,6 +306,7 @@ previewers.man = buffer_previewer.man previewers.autocommands = buffer_previewer.autocommands previewers.highlights = buffer_previewer.highlights previewers.buffers = buffer_previewer.buffers +previewers.pickers = buffer_previewer.pickers --- A deprecated way of displaying content more easily. Was written at a time, --- where the buffer_previewer interface wasn't present. Nowadays it's easier -- cgit v1.2.3