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')
end
function actions.select_default(prompt_bufnr)
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
end
}
function actions.select_horizontal(prompt_bufnr)
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
end
}
function actions.select_vertical(prompt_bufnr)
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
end
}
function actions.select_tab(prompt_bufnr)
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
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]]

View File

@@ -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

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))
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 = {

View File

@@ -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

View File

@@ -2,6 +2,7 @@ local strings = require "plenary.strings"
local deprecated = require "telescope.deprecated"
local sorters = require "telescope.sorters"
local if_nil = vim.F.if_nil
local os_sep = require("plenary.path").path.sep
-- Keep the values around between reloads
_TelescopeConfigurationValues = _TelescopeConfigurationValues or {}
@@ -206,15 +207,48 @@ local telescope_defaults = {
end,
},
dynamic_preview_title = {
false,
[[
dynamic_preview_title = { false, [[
Will change the title of the preview window dynamically, where it
is supported. Means the preview window will for example show the
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
@@ -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, {}))
)
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)
end

View File

@@ -473,7 +473,7 @@ function Picker:find()
pcall(a.nvim_buf_set_option, prompt_bufnr, 'filetype', 'TelescopePrompt')
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
if self.initial_mode == "insert" then
@@ -544,6 +544,11 @@ function Picker:delete_selection(delete_cb)
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)
local prompt_win = status.prompt_win

View File

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