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:
186
lua/telescope/actions/history.lua
Normal file
186
lua/telescope/actions/history.lua
Normal 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
|
||||
@@ -196,21 +196,53 @@ function actions.center(_)
|
||||
vim.cmd(':normal! zz')
|
||||
end
|
||||
|
||||
function actions.select_default(prompt_bufnr)
|
||||
return action_set.select(prompt_bufnr, "default")
|
||||
end
|
||||
actions.select_default = {
|
||||
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)
|
||||
return action_set.select(prompt_bufnr, "default")
|
||||
end
|
||||
}
|
||||
|
||||
function actions.select_horizontal(prompt_bufnr)
|
||||
return action_set.select(prompt_bufnr, "horizontal")
|
||||
end
|
||||
actions.select_horizontal = {
|
||||
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)
|
||||
return action_set.select(prompt_bufnr, "horizontal")
|
||||
end
|
||||
}
|
||||
|
||||
function actions.select_vertical(prompt_bufnr)
|
||||
return action_set.select(prompt_bufnr, "vertical")
|
||||
end
|
||||
actions.select_vertical = {
|
||||
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)
|
||||
return action_set.select(prompt_bufnr, "vertical")
|
||||
end
|
||||
}
|
||||
|
||||
function actions.select_tab(prompt_bufnr)
|
||||
return action_set.select(prompt_bufnr, "tab")
|
||||
end
|
||||
actions.select_tab = {
|
||||
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)
|
||||
return action_set.select(prompt_bufnr, "tab")
|
||||
end
|
||||
}
|
||||
|
||||
-- TODO: consider adding float!
|
||||
-- https://github.com/nvim-telescope/telescope.nvim/issues/365
|
||||
@@ -238,6 +270,7 @@ function actions.close_pum(_)
|
||||
end
|
||||
|
||||
actions._close = function(prompt_bufnr, keepinsert)
|
||||
action_state.get_current_history():reset()
|
||||
local picker = action_state.get_current_picker(prompt_bufnr)
|
||||
local prompt_win = state.get_status(prompt_bufnr).prompt_win
|
||||
local original_win_id = picker.original_win_id
|
||||
@@ -695,6 +728,33 @@ actions.complete_tag = function(prompt_bufnr)
|
||||
|
||||
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
|
||||
actions.open_qflist = function(_)
|
||||
vim.cmd [[copen]]
|
||||
|
||||
@@ -19,6 +19,9 @@ action_mt.create = function(mod)
|
||||
__call = function(t, ...)
|
||||
local values = {}
|
||||
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
|
||||
t._pre[action_name](...)
|
||||
end
|
||||
@@ -34,6 +37,9 @@ action_mt.create = function(mod)
|
||||
table.insert(values, res)
|
||||
end
|
||||
|
||||
if t._static_post[action_name] then
|
||||
t._static_post[action_name](...)
|
||||
end
|
||||
if t._post[action_name] then
|
||||
t._post[action_name](...)
|
||||
end
|
||||
@@ -55,8 +61,10 @@ action_mt.create = function(mod)
|
||||
return setmetatable(new_actions, getmetatable(lhs))
|
||||
end,
|
||||
|
||||
_static_pre = {},
|
||||
_pre = {},
|
||||
_replacements = {},
|
||||
_static_post = {},
|
||||
_post = {},
|
||||
}
|
||||
|
||||
@@ -119,8 +127,14 @@ action_mt.create = function(mod)
|
||||
return mt
|
||||
end
|
||||
|
||||
action_mt.transform = function(k, mt)
|
||||
return setmetatable({k}, mt)
|
||||
action_mt.transform = function(k, mt, mod, v)
|
||||
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
|
||||
|
||||
action_mt.transform_mod = function(mod)
|
||||
@@ -130,8 +144,8 @@ action_mt.transform_mod = function(mod)
|
||||
-- This allows for custom errors, lookups, etc.
|
||||
local redirect = setmetatable({}, getmetatable(mod) or {})
|
||||
|
||||
for k, _ in pairs(mod) do
|
||||
redirect[k] = action_mt.transform(k, mt)
|
||||
for k, v in pairs(mod) do
|
||||
redirect[k] = action_mt.transform(k, mt, mod, v)
|
||||
end
|
||||
|
||||
redirect._clear = mt.clear
|
||||
|
||||
@@ -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))
|
||||
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
|
||||
do
|
||||
local map = {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
---@brief ]]
|
||||
|
||||
local global_state = require('telescope.state')
|
||||
local conf = require('telescope.config').values
|
||||
|
||||
local action_state = {}
|
||||
|
||||
@@ -36,4 +37,19 @@ function action_state.select_key_to_edit_key(type)
|
||||
return select_to_edit_map[type]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user