feat: cycle prompt history (#521)

history is enabled on default but cycle_history_next and cycle_history_prev is not mapped yet

Example:
require('telescope').setup {
  defaults = {
    mappings = {
      i = {
        ["<C-Down>"] = require('telescope.actions').cycle_history_next,
        ["<C-Up>"] = require('telescope.actions').cycle_history_prev,
      }
    }
  }
}

For more information :help telescope.defaults.history

big thanks to clason and all other testers :)
This commit is contained in:
Simon Hauser
2021-07-09 20:45:29 +02:00
committed by GitHub
parent 385020eb23
commit 3699605627
9 changed files with 1098 additions and 625 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,186 @@
local conf = require('telescope.config').values
local Path = require('plenary.path')
local uv = vim.loop
---@brief [[
--- A base implementation of a prompt history that provides a simple history
--- and can be replaced with a custom implementation.
---
--- For example: We provide a extension for a smart history that uses sql.nvim
--- to map histories to metadata, like the calling picker or cwd.
---
--- So you have a history for:
--- - find_files project_1
--- - grep_string project_1
--- - live_grep project_1
--- - find_files project_2
--- - grep_string project_2
--- - live_grep project_2
--- - etc
---
--- See github.com/nvim-telescope/telescope-smart-history.nvim
---@brief ]]
---@tag telescope.actions.history
-- TODO(conni2461): currently not present in plenary path only sync.
-- But sync is just unnecessary here
local write_async = function(path, txt, flag)
uv.fs_open(path, flag, 438, function(open_err, fd)
assert(not open_err, open_err)
uv.fs_write(fd, txt, -1, function(write_err)
assert(not write_err, write_err)
uv.fs_close(fd, function(close_err)
assert(not close_err, close_err)
end)
end)
end)
end
local append_async = function(path, txt) write_async(path, txt, "a") end
local histories = {}
--- Manages prompt history
---@class History @Manages prompt history
---@field enabled boolean: Will indicate if History is enabled or disabled
---@field path string: Will point to the location of the history file
---@field limit string: Will have the limit of the history. Can be nil, if limit is disabled.
---@field content table: History table. Needs to be filled by your own History implementation
---@field index number: Used to keep track of the next or previous index. Default is #content + 1
histories.History = {}
histories.History.__index = histories.History
--- Create a new History
---@param opts table: Defines the behavior of History
---@field init function: Will be called after handling configuration (required)
---@field append function: How to append a new prompt item (required)
---@field reset function: What happens on reset. Will be called when telescope closes (required)
---@field pre_get function: Will be called before a next or previous item will be returned (optional)
function histories.History:new(opts)
local obj = {}
if not conf.history or type(conf.history) ~= "table" then
obj.enabled = false
return setmetatable(obj, self)
end
obj.enabled = true
if conf.history.limit then
obj.limit = conf.history.limit
end
obj.path = vim.fn.expand(conf.history.path)
obj.content = {}
obj.index = 1
opts.init(obj)
obj._reset = opts.reset
obj._append = opts.append
obj._pre_get = opts.pre_get
return setmetatable(obj, self)
end
--- Shorthand to create a new history
function histories.new(...)
return histories.History:new(...)
end
--- Will reset the history index to the default initial state. Will happen after the picker closed
function histories.History:reset()
if not self.enabled then return end
self._reset(self)
end
--- Append a new line to the history
---@param line string: current line that will be appended
---@param picker table: the current picker object
---@param no_reset boolean: On default it will reset the state at the end. If you don't want to do this set to true
function histories.History:append(line, picker, no_reset)
if not self.enabled then return end
self._append(self, line, picker, no_reset)
end
--- Will return the next history item. Can be nil if there are no next items
---@param line string: the current line
---@param picker table: the current picker object
---@return string: the next history item
function histories.History:get_next(line, picker)
if not self.enabled then
print("You are cycling to next the history item but history is disabled.",
"Read ':help telescope.defaults.history'")
return false
end
if self._pre_get then self._pre_get(self, line, picker) end
local next_idx = self.index + 1
if next_idx <= #self.content then
self.index = next_idx
return self.content[next_idx]
end
self.index = #self.content + 1
return nil
end
--- Will return the previous history item. Can be nil if there are no previous items
---@param line string: the current line
---@param picker table: the current picker object
---@return string: the previous history item
function histories.History:get_prev(line, picker)
if not self.enabled then
print("You are cycling to previous the history item but history is disabled.",
"Read ':help telescope.defaults.history'")
return false
end
if self._pre_get then self._pre_get(self, line, picker) end
local next_idx = self.index - 1
if self.index == #self.content + 1 then
if line ~= '' then self:append(line, picker, true) end
end
if next_idx >= 1 then
self.index = next_idx
return self.content[next_idx]
end
return nil
end
--- A simple implementation of history.
---
--- It will keep one unified history across all pickers.
histories.get_simple_history = function()
return histories.new({
init = function(obj)
local p = Path:new(obj.path)
if not p:exists() then p:touch({ parents = true }) end
obj.content = Path:new(obj.path):readlines()
obj.index = #obj.content
table.remove(obj.content, obj.index)
end,
reset = function(self)
self.index = #self.content + 1
end,
append = function(self, line, _, no_reset)
if line ~= '' then
if self.content[#self.content] ~= line then
table.insert(self.content, line)
local len = #self.content
if self.limit and len > self.limit then
local diff = len - self.limit
for i = diff, 1, -1 do
table.remove(self.content, i)
end
write_async(self.path, table.concat(self.content, '\n') .. '\n', 'w')
else
append_async(self.path, line .. '\n')
end
end
end
if not no_reset then
self:reset()
end
end,
})
end
return histories

View File

@@ -196,21 +196,53 @@ function actions.center(_)
vim.cmd(':normal! zz') vim.cmd(':normal! zz')
end end
function actions.select_default(prompt_bufnr) actions.select_default = {
return action_set.select(prompt_bufnr, "default") pre = function(prompt_bufnr)
end action_state.get_current_history():append(
action_state.get_current_line(),
action_state.get_current_picker(prompt_bufnr)
)
end,
action = function(prompt_bufnr)
return action_set.select(prompt_bufnr, "default")
end
}
function actions.select_horizontal(prompt_bufnr) actions.select_horizontal = {
return action_set.select(prompt_bufnr, "horizontal") pre = function(prompt_bufnr)
end action_state.get_current_history():append(
action_state.get_current_line(),
action_state.get_current_picker(prompt_bufnr)
)
end,
action = function(prompt_bufnr)
return action_set.select(prompt_bufnr, "horizontal")
end
}
function actions.select_vertical(prompt_bufnr) actions.select_vertical = {
return action_set.select(prompt_bufnr, "vertical") pre = function(prompt_bufnr)
end action_state.get_current_history():append(
action_state.get_current_line(),
action_state.get_current_picker(prompt_bufnr)
)
end,
action = function(prompt_bufnr)
return action_set.select(prompt_bufnr, "vertical")
end
}
function actions.select_tab(prompt_bufnr) actions.select_tab = {
return action_set.select(prompt_bufnr, "tab") pre = function(prompt_bufnr)
end action_state.get_current_history():append(
action_state.get_current_line(),
action_state.get_current_picker(prompt_bufnr)
)
end,
action = function(prompt_bufnr)
return action_set.select(prompt_bufnr, "tab")
end
}
-- TODO: consider adding float! -- TODO: consider adding float!
-- https://github.com/nvim-telescope/telescope.nvim/issues/365 -- https://github.com/nvim-telescope/telescope.nvim/issues/365
@@ -238,6 +270,7 @@ function actions.close_pum(_)
end end
actions._close = function(prompt_bufnr, keepinsert) actions._close = function(prompt_bufnr, keepinsert)
action_state.get_current_history():reset()
local picker = action_state.get_current_picker(prompt_bufnr) local picker = action_state.get_current_picker(prompt_bufnr)
local prompt_win = state.get_status(prompt_bufnr).prompt_win local prompt_win = state.get_status(prompt_bufnr).prompt_win
local original_win_id = picker.original_win_id local original_win_id = picker.original_win_id
@@ -695,6 +728,33 @@ actions.complete_tag = function(prompt_bufnr)
end end
actions.cycle_history_next = function(prompt_bufnr)
local history = action_state.get_current_history()
local current_picker = actions.get_current_picker(prompt_bufnr)
local line = action_state.get_current_line()
local entry = history:get_next(line, current_picker)
if entry == false then return end
current_picker:reset_prompt()
if entry ~= nil then
current_picker:set_prompt(entry)
end
end
actions.cycle_history_prev = function(prompt_bufnr)
local history = action_state.get_current_history()
local current_picker = actions.get_current_picker(prompt_bufnr)
local line = action_state.get_current_line()
local entry = history:get_prev(line, current_picker)
if entry == false then return end
if entry ~= nil then
current_picker:reset_prompt()
current_picker:set_prompt(entry)
end
end
--- Open the quickfix list --- Open the quickfix list
actions.open_qflist = function(_) actions.open_qflist = function(_)
vim.cmd [[copen]] vim.cmd [[copen]]

View File

@@ -19,6 +19,9 @@ action_mt.create = function(mod)
__call = function(t, ...) __call = function(t, ...)
local values = {} local values = {}
for _, action_name in ipairs(t) do for _, action_name in ipairs(t) do
if t._static_pre[action_name] then
t._static_pre[action_name](...)
end
if t._pre[action_name] then if t._pre[action_name] then
t._pre[action_name](...) t._pre[action_name](...)
end end
@@ -34,6 +37,9 @@ action_mt.create = function(mod)
table.insert(values, res) table.insert(values, res)
end end
if t._static_post[action_name] then
t._static_post[action_name](...)
end
if t._post[action_name] then if t._post[action_name] then
t._post[action_name](...) t._post[action_name](...)
end end
@@ -55,8 +61,10 @@ action_mt.create = function(mod)
return setmetatable(new_actions, getmetatable(lhs)) return setmetatable(new_actions, getmetatable(lhs))
end, end,
_static_pre = {},
_pre = {}, _pre = {},
_replacements = {}, _replacements = {},
_static_post = {},
_post = {}, _post = {},
} }
@@ -119,8 +127,14 @@ action_mt.create = function(mod)
return mt return mt
end end
action_mt.transform = function(k, mt) action_mt.transform = function(k, mt, mod, v)
return setmetatable({k}, mt) local res = setmetatable({k}, mt)
if type(v) == "table" then
res._static_pre[k] = v.pre
res._static_post[k] = v.post
mod[k] = v.action
end
return res
end end
action_mt.transform_mod = function(mod) action_mt.transform_mod = function(mod)
@@ -130,8 +144,8 @@ action_mt.transform_mod = function(mod)
-- This allows for custom errors, lookups, etc. -- This allows for custom errors, lookups, etc.
local redirect = setmetatable({}, getmetatable(mod) or {}) local redirect = setmetatable({}, getmetatable(mod) or {})
for k, _ in pairs(mod) do for k, v in pairs(mod) do
redirect[k] = action_mt.transform(k, mt) redirect[k] = action_mt.transform(k, mt, mod, v)
end end
redirect._clear = mt.clear redirect._clear = mt.clear

View File

@@ -48,6 +48,20 @@ action_set.select = function(prompt_bufnr, type)
return action_set.edit(prompt_bufnr, action_state.select_key_to_edit_key(type)) return action_set.edit(prompt_bufnr, action_state.select_key_to_edit_key(type))
end end
-- goal: currently we have a workaround in actions/init.lua where we do this for all files
-- action_set.select = {
-- -- Will not be called if `select_default` is replaced rather than `action_set.select` because we never get here
-- pre = function(prompt_bufnr)
-- action_state.get_current_history():append(
-- action_state.get_current_line(),
-- action_state.get_current_picker(prompt_bufnr)
-- )
-- end,
-- action = function(prompt_bufnr, type)
-- return action_set.edit(prompt_bufnr, action_state.select_key_to_edit_key(type))
-- end
-- }
local edit_buffer local edit_buffer
do do
local map = { local map = {

View File

@@ -7,6 +7,7 @@
---@brief ]] ---@brief ]]
local global_state = require('telescope.state') local global_state = require('telescope.state')
local conf = require('telescope.config').values
local action_state = {} local action_state = {}
@@ -36,4 +37,19 @@ function action_state.select_key_to_edit_key(type)
return select_to_edit_map[type] return select_to_edit_map[type]
end end
function action_state.get_current_history()
local history = global_state.get_global_key("history")
if not history then
if not conf.history or type(conf.history) ~= "table" then
history = require('telescope.actions.history').get_simple_history()
global_state.set_global_key("history", history)
else
history = conf.history.handler()
global_state.set_global_key("history", history)
end
end
return history
end
return action_state return action_state

View File

@@ -2,6 +2,7 @@ local strings = require "plenary.strings"
local deprecated = require "telescope.deprecated" local deprecated = require "telescope.deprecated"
local sorters = require "telescope.sorters" local sorters = require "telescope.sorters"
local if_nil = vim.F.if_nil local if_nil = vim.F.if_nil
local os_sep = require("plenary.path").path.sep
-- Keep the values around between reloads -- Keep the values around between reloads
_TelescopeConfigurationValues = _TelescopeConfigurationValues or {} _TelescopeConfigurationValues = _TelescopeConfigurationValues or {}
@@ -206,15 +207,48 @@ local telescope_defaults = {
end, end,
}, },
dynamic_preview_title = { dynamic_preview_title = { false, [[
false,
[[
Will change the title of the preview window dynamically, where it Will change the title of the preview window dynamically, where it
is supported. Means the preview window will for example show the is supported. Means the preview window will for example show the
full filename. full filename.
Default: false Default: false]],
]], },
history = { {
path = vim.fn.stdpath("data") .. os_sep .. "telescope_history",
limit = 100,
handler = function(...) return require('telescope.actions.history').get_simple_history(...) end,
}, [[
This field handles the configuration for prompt history.
By default it is a table, with default values (more below).
To disable history, set it to either false or nil.
Currently mappings still need to be added, Example:
mappings = {
i = {
["<C-Down>"] = require('telescope.actions').cycle_history_next,
["<C-Up>"] = require('telescope.actions').cycle_history_prev,
},
},
Fields:
- path: The path to the telescope history as string.
default: stdpath("data")/telescope_history
- limit: The amount of entries that will be written in the
history.
Warning: If limit is set to nil it will grown unbound.
default: 100
- handler: A lua function that implements the history.
This is meant as a developer setting for extensions to
override the history handling, e.g.,
https://github.com/nvim-telescope/telescope-smart-history.nvim,
which allows context sensitive (cwd + picker) history.
Default:
require('telescope.actions.history').get_simple_history
]],
}, },
-- Builtin configuration -- Builtin configuration
@@ -346,6 +380,16 @@ function config.set_defaults(user_defaults, tele_defaults)
vim.tbl_deep_extend("keep", if_nil(config.values[name], {}), if_nil(default_val, {})) vim.tbl_deep_extend("keep", if_nil(config.values[name], {}), if_nil(default_val, {}))
) )
end end
if name == "history" then
if not user_defaults[name] or not config.values[name] then
return false
end
return smarter_depth_2_extend(
if_nil(user_defaults[name], {}),
vim.tbl_deep_extend("keep", if_nil(config.values[name], {}), if_nil(default_val, {}))
)
end
return first_non_null(user_defaults[name], config.values[name], default_val) return first_non_null(user_defaults[name], config.values[name], default_val)
end end

View File

@@ -473,7 +473,7 @@ function Picker:find()
pcall(a.nvim_buf_set_option, prompt_bufnr, 'filetype', 'TelescopePrompt') pcall(a.nvim_buf_set_option, prompt_bufnr, 'filetype', 'TelescopePrompt')
if self.default_text then if self.default_text then
vim.api.nvim_buf_set_lines(prompt_bufnr, 0, 1, false, {self.default_text}) self:set_prompt(self.default_text)
end end
if self.initial_mode == "insert" then if self.initial_mode == "insert" then
@@ -544,6 +544,11 @@ function Picker:delete_selection(delete_cb)
end) end)
end end
function Picker:set_prompt(str)
-- TODO(conni2461): As soon as prompt_buffers are fix use this:
-- vim.api.nvim_buf_set_lines(self.prompt_bufnr, 0, 1, false, { str })
vim.api.nvim_feedkeys(str, 'n', false)
end
function Picker.close_windows(status) function Picker.close_windows(status)
local prompt_win = status.prompt_win local prompt_win = status.prompt_win

View File

@@ -11,20 +11,17 @@ docs.test = function()
local input_files = { local input_files = {
"./lua/telescope/init.lua", "./lua/telescope/init.lua",
"./lua/telescope/builtin/init.lua", "./lua/telescope/builtin/init.lua",
"./lua/telescope/themes.lua",
"./lua/telescope/pickers/layout_strategies.lua", "./lua/telescope/pickers/layout_strategies.lua",
"./lua/telescope/config/resolve.lua",
"./lua/telescope/actions/init.lua", "./lua/telescope/actions/init.lua",
"./lua/telescope/actions/state.lua", "./lua/telescope/actions/state.lua",
"./lua/telescope/actions/set.lua", "./lua/telescope/actions/set.lua",
"./lua/telescope/actions/utils.lua", "./lua/telescope/actions/utils.lua",
"./lua/telescope/previewers/init.lua", "./lua/telescope/previewers/init.lua",
"./lua/telescope/config/resolve.lua", "./lua/telescope/actions/history.lua",
"./lua/telescope/themes.lua",
} }
table.sort(input_files, function(a, b)
return #a < #b
end)
local output_file = "./doc/telescope.txt" local output_file = "./doc/telescope.txt"
local output_file_handle = io.open(output_file, "w") local output_file_handle = io.open(output_file, "w")