feat(pickers): fully customizable layout (#2572)

This commit is contained in:
Munif Tanjim 2023-09-27 12:34:22 +06:00 committed by GitHub
parent 5c91b855b8
commit 84d53dfdbe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 648 additions and 201 deletions

View file

@ -146,6 +146,13 @@ telescope.setup({opts}) *telescope.setup()*
Default: 'horizontal'
*telescope.defaults.create_layout*
create_layout: ~
Configure the layout of Telescope pickers.
See |telescope.pickers.layout| for details.
Default: 'nil'
*telescope.defaults.layout_config*
layout_config: ~
Determines the default configuration values for layout strategies.
@ -1947,6 +1954,155 @@ ordered from the lowest priority to the highest priority.
<
================================================================================
LAYOUT *telescope.pickers.layout*
The telescope pickers layout can be configured using the
|telescope.defaults.create_layout| option.
Parameters: ~
- picker : A Picker object.
Return: ~
- layout : instance of `TelescopeLayout` class.
Example: ~
>
local Layout = require "telescope.pickers.layout"
require("telescope").setup {
create_layout = function(picker)
local function create_window(enter, width, height, row, col, title)
local bufnr = vim.api.nvim_create_buf(false, true)
local winid = vim.api.nvim_open_win(bufnr, enter, {
style = "minimal",
relative = "editor",
width = width,
height = height,
row = row,
col = col,
border = "single",
title = title,
})
vim.wo[winid].winhighlight = "Normal:Normal"
return Layout.Window {
bufnr = bufnr,
winid = winid,
}
end
local function destory_window(window)
if window then
if vim.api.nvim_win_is_valid(window.winid) then
vim.api.nvim_win_close(window.winid, true)
end
if vim.api.nvim_buf_is_valid(window.bufnr) then
vim.api.nvim_buf_delete(window.bufnr, { force = true })
end
end
end
local layout = Layout {
picker = picker,
mount = function(self)
self.results = create_window(false, 40, 20, 0, 0, "Results")
self.preview = create_window(false, 40, 23, 0, 42, "Preview")
self.prompt = create_window(true, 40, 1, 22, 0, "Prompt")
end,
unmount = function(self)
destory_window(self.results)
destory_window(self.preview)
destory_window(self.prompt)
end,
update = function(self) end,
}
return layout
end,
}
<
TelescopeWindowBorder.config *TelescopeWindowBorder.config*
Fields: ~
{bufnr} (integer)
{winid} (integer|nil)
{change_title} (nil|function) (self: TelescopeWindowBorder, title:
string, pos?:
"NW"|"N"|"NE"|"SW"|"S"|"SE"):nil
TelescopeWindowBorder *TelescopeWindowBorder*
Fields: ~
{bufnr} (integer|nil)
{winid} (integer|nil)
TelescopeWindow.config *TelescopeWindow.config*
Fields: ~
{bufnr} (integer)
{winid} (integer|nil)
{border} (TelescopeWindowBorder.config|nil)
TelescopeWindow *TelescopeWindow*
Fields: ~
{border} (TelescopeWindowBorder)
{bufnr} (integer)
{winid} (integer)
TelescopeLayout.config *TelescopeLayout.config*
Fields: ~
{mount} (function) (self: TelescopeLayout):nil
{unmount} (function) (self: TelescopeLayout):nil
{update} (function) (self: TelescopeLayout):nil
{prompt} (TelescopeWindow|nil)
{results} (TelescopeWindow|nil)
{preview} (TelescopeWindow|nil)
TelescopeLayout *TelescopeLayout*
Fields: ~
{prompt} (TelescopeWindow)
{results} (TelescopeWindow)
{preview} (TelescopeWindow|nil)
Layout:mount() *telescope.pickers.layout:mount()*
Create the layout. This needs to ensure the required properties are
populated.
Layout:unmount() *telescope.pickers.layout:unmount()*
Destroy the layout. This is responsible for performing clean-up, for
example:
- deleting buffers
- closing windows
- clearing autocmds
Layout:update() *telescope.pickers.layout:update()*
Refresh the layout. This is called when, for example, vim is resized.
================================================================================
LAYOUT *telescope.layout*

View file

@ -75,12 +75,12 @@ action_generate.refine = function(prompt_bufnr, opts)
end
-- title
if opts.prompt_title and current_picker.prompt_border then
current_picker.prompt_border:change_title(opts.prompt_title)
if opts.prompt_title and current_picker.layout.prompt.border then
current_picker.layout.prompt.border:change_title(opts.prompt_title)
end
if opts.results_title and current_picker.results_border then
current_picker.results_border:change_title(opts.results_title)
if opts.results_title and current_picker.layout.results.border then
current_picker.layout.results.border:change_title(opts.results_title)
end
local results = {}

View file

@ -26,10 +26,11 @@ action_layout.toggle_preview = function(prompt_bufnr)
local picker = action_state.get_current_picker(prompt_bufnr)
local status = state.get_status(picker.prompt_bufnr)
if picker.previewer and status.preview_win then
local preview_winid = status.layout.preview and status.layout.preview.winid
if picker.previewer and preview_winid then
picker.hidden_previewer = picker.previewer
picker.previewer = nil
elseif picker.hidden_previewer and not status.preview_win then
elseif picker.hidden_previewer and not preview_winid then
picker.previewer = picker.hidden_previewer
picker.hidden_previewer = nil
else

View file

@ -230,12 +230,13 @@ action_set.scroll_previewer = function(prompt_bufnr, direction)
local previewer = action_state.get_current_picker(prompt_bufnr).previewer
local status = state.get_status(prompt_bufnr)
local preview_winid = status.layout.preview and status.layout.preview.winid
-- Check if we actually have a previewer and a preview window
if type(previewer) ~= "table" or previewer.scroll_fn == nil or status.preview_win == nil then
if type(previewer) ~= "table" or previewer.scroll_fn == nil or preview_winid == nil then
return
end
local default_speed = vim.api.nvim_win_get_height(status.preview_win) / 2
local default_speed = vim.api.nvim_win_get_height(status.layout.preview.winid) / 2
local speed = status.picker.layout_config.scroll_speed or default_speed
previewer:scroll_fn(math.floor(speed * direction))
@ -270,12 +271,12 @@ end
-- Valid directions include: "1", "-1"
action_set.scroll_results = function(prompt_bufnr, direction)
local status = state.get_status(prompt_bufnr)
local default_speed = vim.api.nvim_win_get_height(status.results_win) / 2
local default_speed = vim.api.nvim_win_get_height(status.layout.results.winid) / 2
local speed = status.picker.layout_config.scroll_speed or default_speed
local input = direction > 0 and [[]] or [[]]
vim.api.nvim_win_call(status.results_win, function()
vim.api.nvim_win_call(status.layout.results.winid, function()
vim.cmd([[normal! ]] .. math.floor(speed) .. input)
end)

View file

@ -991,8 +991,10 @@ internal.colorscheme = function(opts)
preview_fn = function(_, entry, status)
if not deleted then
deleted = true
del_win(status.preview_win)
del_win(status.preview_border_win)
if status.layout.preview then
del_win(status.layout.preview.winid)
del_win(status.layout.preview.border.winid)
end
end
vim.cmd("colorscheme " .. entry.value)
end,

View file

@ -188,6 +188,16 @@ append(
Default: 'horizontal']]
)
append(
"create_layout",
nil,
[[
Configure the layout of Telescope pickers.
See |telescope.pickers.layout| for details.
Default: 'nil']]
)
append("layout_config", layout_config_defaults, layout_config_description)
append(

View file

@ -20,6 +20,7 @@ local entry_display = require "telescope.pickers.entry_display"
local p_highlighter = require "telescope.pickers.highlights"
local p_scroller = require "telescope.pickers.scroller"
local p_window = require "telescope.pickers.window"
local Layout = require "telescope.pickers.layout"
local EntryManager = require "telescope.entry_manager"
local MultiSelect = require "telescope.pickers.multi"
@ -31,6 +32,183 @@ local ns_telescope_matching = a.nvim_create_namespace "telescope_matching"
local ns_telescope_prompt = a.nvim_create_namespace "telescope_prompt"
local ns_telescope_prompt_prefix = a.nvim_create_namespace "telescope_prompt_prefix"
---@class telescope_popup_options
---@field border table<1|2|3|4, integer>
---@field borderchars table<1|2|3|4|5|6|7|8, string>
---@field borderhighlight string
---@field col integer
---@field enter boolean
---@field height integer
---@field highlight string
---@field line integer
---@field minheight integer
---@field title integer
---@field titlehighlight integer
---@field width integer
-- Create three windows:
-- 1. Prompt window
-- 2. Options window
-- 3. Preview window
--
---@param picker Picker
local function default_create_layout(picker)
local function make_border(border)
if not border then
return nil
end
border.winid = border.win_id
return border
end
local layout = Layout {
picker = picker,
---@param self TelescopeLayout
mount = function(self)
local line_count = vim.o.lines - vim.o.cmdheight
if vim.o.laststatus ~= 0 then
line_count = line_count - 1
end
local popup_opts = picker:get_window_options(vim.o.columns, line_count)
-- `popup.nvim` massaging so people don't have to remember minheight shenanigans
popup_opts.results.minheight = popup_opts.results.height
popup_opts.results.highlight = "TelescopeResultsNormal"
popup_opts.results.borderhighlight = "TelescopeResultsBorder"
popup_opts.results.titlehighlight = "TelescopeResultsTitle"
popup_opts.prompt.minheight = popup_opts.prompt.height
popup_opts.prompt.highlight = "TelescopePromptNormal"
popup_opts.prompt.borderhighlight = "TelescopePromptBorder"
popup_opts.prompt.titlehighlight = "TelescopePromptTitle"
if popup_opts.preview then
popup_opts.preview.minheight = popup_opts.preview.height
popup_opts.preview.highlight = "TelescopePreviewNormal"
popup_opts.preview.borderhighlight = "TelescopePreviewBorder"
popup_opts.preview.titlehighlight = "TelescopePreviewTitle"
end
local results_win, results_opts = picker:_create_window("", popup_opts.results)
local results_bufnr = a.nvim_win_get_buf(results_win)
self.results = Layout.Window {
winid = results_win,
bufnr = results_bufnr,
border = make_border(results_opts.border),
}
if popup_opts.preview then
local preview_win, preview_opts = picker:_create_window("", popup_opts.preview)
local preview_bufnr = a.nvim_win_get_buf(preview_win)
self.preview = Layout.Window {
winid = preview_win,
bufnr = preview_bufnr,
border = make_border(preview_opts.border),
}
end
local prompt_win, prompt_opts = picker:_create_window("", popup_opts.prompt)
local prompt_bufnr = a.nvim_win_get_buf(prompt_win)
self.prompt = Layout.Window {
winid = prompt_win,
bufnr = prompt_bufnr,
border = make_border(prompt_opts.border),
}
end,
---@param self TelescopeLayout
unmount = function(self)
utils.win_delete("results_win", self.results.winid, true, true)
if self.preview then
utils.win_delete("preview_win", self.preview.winid, true, true)
end
utils.win_delete("prompt_border_win", self.prompt.border.winid, true, true)
utils.win_delete("results_border_win", self.results.border.winid, true, true)
if self.preview then
utils.win_delete("preview_border_win", self.preview.border.winid, true, true)
end
-- we cant use win_delete. We first need to close and then delete the buffer
if vim.api.nvim_win_is_valid(self.prompt.winid) then
vim.api.nvim_win_close(self.prompt.winid, true)
end
utils.buf_delete(self.prompt.bufnr)
end,
---@param self TelescopeLayout
update = function(self)
local line_count = vim.o.lines - vim.o.cmdheight
if vim.o.laststatus ~= 0 then
line_count = line_count - 1
end
local popup_opts = picker:get_window_options(vim.o.columns, line_count)
-- `popup.nvim` massaging so people don't have to remember minheight shenanigans
popup_opts.results.minheight = popup_opts.results.height
popup_opts.prompt.minheight = popup_opts.prompt.height
if popup_opts.preview then
popup_opts.preview.minheight = popup_opts.preview.height
end
local prompt_win = self.prompt.winid
local results_win = self.results.winid
local preview_win = self.preview and self.preview.winid
local preview_opts
if popup_opts.preview then
if preview_win ~= nil then
-- Move all popups at the same time
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
popup.move(preview_win, popup_opts.preview)
else
popup_opts.preview.highlight = "TelescopePreviewNormal"
popup_opts.preview.borderhighlight = "TelescopePreviewBorder"
popup_opts.preview.titlehighlight = "TelescopePreviewTitle"
local preview_bufnr = (self.preview and self.preview.bufnr ~= nil)
and vim.api.nvim_buf_is_valid(self.preview.bufnr)
and self.preview.bufnr
or ""
preview_win, preview_opts = picker:_create_window(preview_bufnr, popup_opts.preview)
if preview_bufnr == "" then
preview_bufnr = a.nvim_win_get_buf(preview_win)
end
self.preview = Layout.Window {
winid = preview_win,
bufnr = preview_bufnr,
border = make_border(preview_opts.border),
}
if picker.previewer and picker.previewer.state and picker.previewer.state.winid then
picker.previewer.state.winid = preview_win
end
-- Move prompt and results after preview created
vim.defer_fn(function()
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
end, 0)
end
elseif preview_win ~= nil then
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
-- Remove preview after the prompt and results are moved
vim.defer_fn(function()
utils.win_delete("preview_win", preview_win, true)
utils.win_delete("preview_win", self.preview.border.winid, true)
self.preview = nil
end, 0)
else
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
end
end,
}
return layout
end
local pickers = {}
-- TODO: Add overscroll option for results buffer
@ -141,6 +319,7 @@ function Picker:new(opts)
__scrolling_limit = tonumber(vim.F.if_nil(opts.temp__scrolling_limit, 250)),
}, self)
obj.create_layout = opts.create_layout or config.values.create_layout or default_create_layout
obj.get_window_options = opts.get_window_options or p_window.get_window_options
if obj.all_previewers ~= nil and obj.all_previewers ~= false then
@ -325,13 +504,11 @@ end
--- A helper function for creating each of the windows in a picker
---@param bufnr number: the buffer number to be used in the window
---@param popup_opts table: options to pass to `popup.create`
---@param nowrap boolean: is |'wrap'| disabled in the created window
function Picker:_create_window(bufnr, popup_opts, nowrap)
function Picker:_create_window(bufnr, popup_opts)
local what = bufnr or ""
local win, opts = popup.create(what, popup_opts)
a.nvim_win_set_option(win, "winblend", self.window.winblend)
a.nvim_win_set_option(win, "wrap", not nowrap)
local border_win = opts and opts.border and opts.border.win_id
if border_win then
a.nvim_win_set_option(border_win, "winblend", self.window.winblend)
@ -350,67 +527,35 @@ function Picker:find()
-- User autocmd run it before create Telescope window
vim.api.nvim_exec_autocmds("User", { pattern = "TelescopeFindPre" })
-- Create three windows:
-- 1. Prompt window
-- 2. Options window
-- 3. Preview window
local layout = self:create_layout()
layout:mount()
local line_count = vim.o.lines - vim.o.cmdheight
if vim.o.laststatus ~= 0 then
line_count = line_count - 1
self.layout = layout
self.prompt_win, self.prompt_bufnr, self.prompt_border =
layout.prompt.winid, layout.prompt.bufnr, layout.prompt.border
self.results_win, self.results_bufnr, self.results_border =
layout.results.winid, layout.results.bufnr, layout.results.border
if layout.preview then
self.preview_win, self.preview_bufnr, self.preview_border =
layout.preview.winid, layout.preview.bufnr, layout.preview.border
else
self.preview_win, self.preview_bufnr, self.preview_border = nil, nil, nil
end
local popup_opts = self:get_window_options(vim.o.columns, line_count)
-- `popup.nvim` massaging so people don't have to remember minheight shenanigans
popup_opts.results.minheight = popup_opts.results.height
popup_opts.results.highlight = "TelescopeResultsNormal"
popup_opts.results.borderhighlight = "TelescopeResultsBorder"
popup_opts.results.titlehighlight = "TelescopeResultsTitle"
popup_opts.prompt.minheight = popup_opts.prompt.height
popup_opts.prompt.highlight = "TelescopePromptNormal"
popup_opts.prompt.borderhighlight = "TelescopePromptBorder"
popup_opts.prompt.titlehighlight = "TelescopePromptTitle"
if popup_opts.preview then
popup_opts.preview.minheight = popup_opts.preview.height
popup_opts.preview.highlight = "TelescopePreviewNormal"
popup_opts.preview.borderhighlight = "TelescopePreviewBorder"
popup_opts.preview.titlehighlight = "TelescopePreviewTitle"
pcall(a.nvim_buf_set_option, self.results_bufnr, "tabstop", 1) -- #1834
pcall(a.nvim_buf_set_option, self.prompt_bufnr, "tabstop", 1) -- #1834
a.nvim_buf_set_option(self.prompt_bufnr, "buftype", "prompt")
if not self.wrap_results then
a.nvim_win_set_option(self.results_win, "wrap", false)
end
local results_win, results_opts, results_border_win =
self:_create_window("", popup_opts.results, not self.wrap_results)
local results_bufnr = a.nvim_win_get_buf(results_win)
pcall(a.nvim_buf_set_option, results_bufnr, "tabstop", 1) -- #1834
self.results_bufnr = results_bufnr
self.results_win = results_win
self.results_border = results_opts and results_opts.border
local preview_win, preview_opts, preview_bufnr, preview_border_win
if popup_opts.preview then
preview_win, preview_opts, preview_border_win = self:_create_window("", popup_opts.preview)
preview_bufnr = a.nvim_win_get_buf(preview_win)
a.nvim_win_set_option(self.prompt_win, "wrap", true)
if self.preview_win then
a.nvim_win_set_option(self.preview_win, "wrap", true)
end
-- This is needed for updating the title
local preview_border = preview_opts and preview_opts.border
self.preview_win = preview_win
self.preview_border = preview_border
local prompt_win, prompt_opts, prompt_border_win = self:_create_window("", popup_opts.prompt)
local prompt_bufnr = a.nvim_win_get_buf(prompt_win)
pcall(a.nvim_buf_set_option, prompt_bufnr, "tabstop", 1) -- #1834
self.prompt_bufnr = prompt_bufnr
self.prompt_win = prompt_win
self.prompt_border = prompt_opts and prompt_opts.border
-- Prompt prefix
local prompt_prefix = self.prompt_prefix
a.nvim_buf_set_option(prompt_bufnr, "buftype", "prompt")
vim.fn.prompt_setprompt(prompt_bufnr, prompt_prefix)
self.prompt_prefix = prompt_prefix
vim.fn.prompt_setprompt(self.prompt_bufnr, prompt_prefix)
self:_reset_prefix_color()
-- TODO: This could be configurable in the future, but I don't know why you would
@ -419,9 +564,9 @@ function Picker:find()
-- This just lets us stop doing stuff after tons of things.
self.max_results = self.__scrolling_limit
vim.api.nvim_buf_set_lines(results_bufnr, 0, self.max_results, false, utils.repeated_table(self.max_results, ""))
vim.api.nvim_buf_set_lines(self.results_bufnr, 0, self.max_results, false, utils.repeated_table(self.max_results, ""))
local status_updater = self:get_status_updater(prompt_win, prompt_bufnr)
local status_updater = self:get_status_updater(self.prompt_win, self.prompt_bufnr)
local debounced_status = debounce.throttle_leading(status_updater, 50)
local tx, rx = channel.mpsc()
@ -455,8 +600,8 @@ function Picker:find()
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")
pcall(a.nvim_buf_set_option, self.prompt_bufnr, "filetype", "TelescopePrompt")
pcall(a.nvim_buf_set_option, self.results_bufnr, "filetype", "TelescopeResults")
await_schedule()
@ -471,8 +616,8 @@ function Picker:find()
self:_reset_track()
if not vim.api.nvim_buf_is_valid(prompt_bufnr) then
log.debug("ON_LINES: Invalid prompt_bufnr", prompt_bufnr)
if not vim.api.nvim_buf_is_valid(self.prompt_bufnr) then
log.debug("ON_LINES: Invalid prompt_bufnr", self.prompt_bufnr)
return
end
@ -509,7 +654,7 @@ function Picker:find()
end)
-- Register attach
vim.api.nvim_buf_attach(prompt_bufnr, false, {
vim.api.nvim_buf_attach(self.prompt_bufnr, false, {
on_lines = function(...)
if self._finder_attached then
find_id = self:_next_find_id()
@ -527,46 +672,45 @@ function Picker:find()
vim.api.nvim_create_augroup("PickerInsert", {})
-- TODO: Use WinLeave as well?
vim.api.nvim_create_autocmd("BufLeave", {
buffer = prompt_bufnr,
buffer = self.prompt_bufnr,
group = "PickerInsert",
nested = true,
once = true,
callback = function()
require("telescope.pickers").on_close_prompt(prompt_bufnr)
require("telescope.pickers").on_close_prompt(self.prompt_bufnr)
end,
})
vim.api.nvim_create_autocmd("VimResized", {
buffer = prompt_bufnr,
buffer = self.prompt_bufnr,
group = "PickerInsert",
nested = true,
callback = function()
require("telescope.pickers").on_resize_window(prompt_bufnr)
require("telescope.pickers").on_resize_window(self.prompt_bufnr)
end,
})
self.prompt_bufnr = prompt_bufnr
state.set_status(
prompt_bufnr,
self.prompt_bufnr,
setmetatable({
prompt_bufnr = prompt_bufnr,
prompt_win = prompt_win,
prompt_border_win = prompt_border_win,
results_bufnr = results_bufnr,
results_win = results_win,
results_border_win = results_border_win,
preview_bufnr = preview_bufnr,
preview_win = preview_win,
preview_border_win = preview_border_win,
layout = layout,
picker = self,
-- compatibility
prompt_bufnr = self.prompt_bufnr,
prompt_win = self.prompt_win,
prompt_border_win = self.prompt_border.winid,
results_bufnr = self.results_bufnr,
results_win = self.results_win,
results_border_win = self.results_border.winid,
preview_bufnr = self.preview_bufnr,
preview_win = self.preview_win,
preview_border_win = self.preview_border and self.preview_border.winid,
}, {
__mode = "kv",
})
)
mappings.apply_keymap(prompt_bufnr, self.attach_mappings, config.values.mappings)
mappings.apply_keymap(self.prompt_bufnr, self.attach_mappings, config.values.mappings)
tx.send()
main_loop()
@ -574,79 +718,20 @@ end
--- A helper function to update picker windows when layout options are changed
function Picker:recalculate_layout()
local line_count = vim.o.lines - vim.o.cmdheight
if vim.o.laststatus ~= 0 then
line_count = line_count - 1
end
local popup_opts = self:get_window_options(vim.o.columns, line_count)
-- `popup.nvim` massaging so people don't have to remember minheight shenanigans
popup_opts.results.minheight = popup_opts.results.height
popup_opts.prompt.minheight = popup_opts.prompt.height
if popup_opts.preview then
popup_opts.preview.minheight = popup_opts.preview.height
end
local status = state.get_status(self.prompt_bufnr)
local prompt_win = status.prompt_win
local results_win = status.results_win
local preview_win = status.preview_win
status.layout:update()
local preview_opts, preview_border_win
if popup_opts.preview then
if preview_win ~= nil then
-- Move all popups at the same time
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
popup.move(preview_win, popup_opts.preview)
else
popup_opts.preview.highlight = "TelescopePreviewNormal"
popup_opts.preview.borderhighlight = "TelescopePreviewBorder"
popup_opts.preview.titlehighlight = "TelescopePreviewTitle"
local preview_bufnr = status.preview_bufnr ~= nil
and vim.api.nvim_buf_is_valid(status.preview_bufnr)
and status.preview_bufnr
or ""
preview_win, preview_opts, preview_border_win = self:_create_window(preview_bufnr, popup_opts.preview)
if preview_bufnr == "" then
preview_bufnr = a.nvim_win_get_buf(preview_win)
end
status.preview_win = preview_win
status.preview_bufnr = preview_bufnr
status.preview_border_win = preview_border_win
state.set_status(prompt_win, status)
self.preview_win = preview_win
self.preview_border_win = preview_border_win
self.preview_border = preview_opts and preview_opts.border
if self.previewer and self.previewer.state and self.previewer.state.winid then
self.previewer.state.winid = preview_win
end
-- Move prompt and results after preview created
vim.defer_fn(function()
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
end, 0)
end
elseif preview_win ~= nil then
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
-- Remove preview after the prompt and results are moved
vim.defer_fn(function()
utils.win_delete("preview_win", preview_win, true)
utils.win_delete("preview_win", status.preview_border_win, true)
status.preview_win = nil
status.preview_border_win = nil
state.set_status(prompt_win, status)
self.preview_win = nil
self.preview_border_win = nil
self.preview_border = nil
end, 0)
local layout = status.layout
self.prompt_win, self.prompt_bufnr, self.prompt_border =
layout.prompt.winid, layout.prompt.bufnr, layout.prompt.border
self.results_win, self.results_bufnr, self.results_border =
layout.results.winid, layout.results.bufnr, layout.results.border
if layout.preview then
self.preview_win, self.preview_bufnr, self.preview_border =
layout.preview.winid, layout.preview.bufnr, layout.preview.border
else
popup.move(prompt_win, popup_opts.prompt)
popup.move(results_win, popup_opts.results)
self.preview_win, self.preview_bufnr, self.preview_border = nil, nil, nil
end
-- Temporarily disabled: Draw the screen ASAP. This makes things feel speedier.
@ -749,20 +834,9 @@ end
---@param status table: table containing information on the picker
--- and associated windows. Generally obtained from `state.get_status`
function Picker.close_windows(status)
utils.win_delete("results_win", status.results_win, true, true)
utils.win_delete("preview_win", status.preview_win, true, true)
utils.win_delete("prompt_border_win", status.prompt_border_win, true, true)
utils.win_delete("results_border_win", status.results_border_win, true, true)
utils.win_delete("preview_border_win", status.preview_border_win, true, true)
-- we cant use win_delete. We first need to close and then delete the buffer
if vim.api.nvim_win_is_valid(status.prompt_win) then
vim.api.nvim_win_close(status.prompt_win, true)
end
utils.buf_delete(status.prompt_bufnr)
state.clear_status(status.prompt_bufnr)
local prompt_bufnr = status.layout.prompt.bufnr
status.layout:unmount()
state.clear_status(prompt_bufnr)
end
--- Get the entry table of the current selection
@ -1079,7 +1153,12 @@ end
--- Refresh the previewer based on the current `status` of the picker
function Picker:refresh_previewer()
local status = state.get_status(self.prompt_bufnr)
if self.previewer and status.preview_win and a.nvim_win_is_valid(status.preview_win) then
if
self.previewer
and status.layout.preview
and status.layout.preview.winid
and a.nvim_win_is_valid(status.layout.preview.winid)
then
self:_increment "previewed"
self.previewer:preview(self._selection_entry, status)
@ -1091,7 +1170,7 @@ function Picker:refresh_previewer()
local new_title = self.previewer:title(self._selection_entry, config.values.dynamic_preview_title)
if new_title ~= nil and new_title ~= self.preview_title then
self.preview_title = new_title
self.preview_border:change_title(new_title)
self.layout.preview.border:change_title(new_title)
end
end
end

View file

@ -75,8 +75,8 @@ entry_display.create = function(configuration)
if width == nil then
local status = state.get_status(vim.F.if_nil(configuration.prompt_bufnr, vim.api.nvim_get_current_buf()))
local s = {}
s[1] = vim.api.nvim_win_get_width(status.results_win) - #status.picker.selection_caret
s[2] = vim.api.nvim_win_get_height(status.results_win)
s[1] = vim.api.nvim_win_get_width(status.layout.results.winid) - #status.picker.selection_caret
s[2] = vim.api.nvim_win_get_height(status.layout.results.winid)
width = resolve.resolve_width(v.width)(nil, s[1], s[2])
end
if type(item) == "table" then

View file

@ -0,0 +1,193 @@
---@tag telescope.pickers.layout
---@config { ["module"] = "telescope.pickers.layout" }
---@brief [[
--- The telescope pickers layout can be configured using the
--- |telescope.defaults.create_layout| option.
---
--- Parameters: ~
--- - picker : A Picker object.
---
--- Return: ~
--- - layout : instance of `TelescopeLayout` class.
---
--- Example: ~
--- <code>
--- local Layout = require "telescope.pickers.layout"
---
--- require("telescope").setup {
--- create_layout = function(picker)
--- local function create_window(enter, width, height, row, col, title)
--- local bufnr = vim.api.nvim_create_buf(false, true)
--- local winid = vim.api.nvim_open_win(bufnr, enter, {
--- style = "minimal",
--- relative = "editor",
--- width = width,
--- height = height,
--- row = row,
--- col = col,
--- border = "single",
--- title = title,
--- })
---
--- vim.wo[winid].winhighlight = "Normal:Normal"
---
--- return Layout.Window {
--- bufnr = bufnr,
--- winid = winid,
--- }
--- end
---
--- local function destory_window(window)
--- if window then
--- if vim.api.nvim_win_is_valid(window.winid) then
--- vim.api.nvim_win_close(window.winid, true)
--- end
--- if vim.api.nvim_buf_is_valid(window.bufnr) then
--- vim.api.nvim_buf_delete(window.bufnr, { force = true })
--- end
--- end
--- end
---
--- local layout = Layout {
--- picker = picker,
--- mount = function(self)
--- self.results = create_window(false, 40, 20, 0, 0, "Results")
--- self.preview = create_window(false, 40, 23, 0, 42, "Preview")
--- self.prompt = create_window(true, 40, 1, 22, 0, "Prompt")
--- end,
--- unmount = function(self)
--- destory_window(self.results)
--- destory_window(self.preview)
--- destory_window(self.prompt)
--- end,
--- update = function(self) end,
--- }
---
--- return layout
--- end,
--- }
--- </code>
---@brief ]]
local function wrap_instance(class, instance)
local self = instance
if not getmetatable(instance) then
self = setmetatable(instance, { __index = class })
end
return self
end
---@class TelescopeWindowBorder.config
---@field bufnr integer
---@field winid integer|nil
---@field change_title nil|function: (self: TelescopeWindowBorder, title: string, pos?: "NW"|"N"|"NE"|"SW"|"S"|"SE"):nil
---@param class TelescopeWindowBorder
---@param config TelescopeWindowBorder.config
---@return TelescopeWindowBorder
local function init_border(class, config)
config = config or {}
---@type TelescopeWindowBorder
local self = wrap_instance(class, config)
if not self.change_title then
self.change_title = class.change_title
end
return self
end
---@class TelescopeWindowBorder
---@field bufnr integer|nil
---@field winid integer|nil
local Border = setmetatable({}, {
__call = init_border,
__name = "TelescopeWindowBorder",
})
---@param title string
---@param pos "NW"|"N"|"NE"|"SW"|"S"|"SE"|nil
function Border:change_title(title, pos) end
---@class TelescopeWindow.config
---@field bufnr integer
---@field winid integer|nil
---@field border TelescopeWindowBorder.config|nil
---@param class TelescopeWindow
---@param config TelescopeWindow.config
---@return TelescopeWindow
local function init_window(class, config)
config = config or {}
---@type TelescopeWindow
local self = wrap_instance(class, config)
self.border = Border(config.border)
return self
end
---@class TelescopeWindow
---@field border TelescopeWindowBorder
---@field bufnr integer
---@field winid integer
local Window = setmetatable({}, {
__call = init_window,
__name = "TelescopeWindow",
})
---@class TelescopeLayout.config
---@field mount function: (self: TelescopeLayout):nil
---@field unmount function: (self: TelescopeLayout):nil
---@field update function: (self: TelescopeLayout):nil
---@field prompt TelescopeWindow|nil
---@field results TelescopeWindow|nil
---@field preview TelescopeWindow|nil
---@param class TelescopeLayout
---@param config TelescopeLayout.config
---@return TelescopeLayout
local function init_layout(class, config)
config = config or {}
---@type TelescopeLayout
local self = wrap_instance(class, config)
assert(config.mount, "missing layout:mount")
assert(config.unmount, "missing layout:unmount")
assert(config.update, "missing layout:update")
return self
end
---@class TelescopeLayout
---@field prompt TelescopeWindow
---@field results TelescopeWindow
---@field preview TelescopeWindow|nil
local Layout = setmetatable({
Window = Window,
}, {
__call = init_layout,
__name = "TelescopeLayout",
})
--- Create the layout.
--- This needs to ensure the required properties are populated.
function Layout:mount() end
--- Destroy the layout.
--- This is responsible for performing clean-up, for example:
--- - deleting buffers
--- - closing windows
--- - clearing autocmds
function Layout:unmount() end
--- Refresh the layout.
--- This is called when, for example, vim is resized.
function Layout:update() end
---@alias TelescopeWindow.constructor fun(config: TelescopeWindow.config): TelescopeWindow
---@alias TelescopeLayout.constructor fun(config: TelescopeLayout.config): TelescopeLayout
return Layout --[[@as TelescopeLayout.constructor|{ Window: TelescopeWindow.constructor }]]

View file

@ -409,32 +409,33 @@ previewers.new_buffer_previewer = function(opts)
end
function opts.preview_fn(self, entry, status)
local preview_winid = status.layout.preview and status.layout.preview.winid
if get_bufnr(self) == nil then
set_bufnr(self, vim.api.nvim_win_get_buf(status.preview_win))
preview_window_id = status.preview_win
set_bufnr(self, vim.api.nvim_win_get_buf(preview_winid))
preview_window_id = preview_winid
end
if opts.get_buffer_by_name and get_bufnr_by_bufname(self, opts.get_buffer_by_name(self, entry)) then
self.state.bufname = opts.get_buffer_by_name(self, entry)
self.state.bufnr = get_bufnr_by_bufname(self, self.state.bufname)
utils.win_set_buf_noautocmd(status.preview_win, self.state.bufnr)
utils.win_set_buf_noautocmd(preview_winid, self.state.bufnr)
else
local bufnr = vim.api.nvim_create_buf(false, true)
set_bufnr(self, bufnr)
vim.schedule(function()
if vim.api.nvim_buf_is_valid(bufnr) then
utils.win_set_buf_noautocmd(status.preview_win, bufnr)
utils.win_set_buf_noautocmd(preview_winid, bufnr)
end
end)
vim.api.nvim_win_set_option(status.preview_win, "winhl", "Normal:TelescopePreviewNormal")
vim.api.nvim_win_set_option(status.preview_win, "signcolumn", "no")
vim.api.nvim_win_set_option(status.preview_win, "foldlevel", 100)
vim.api.nvim_win_set_option(status.preview_win, "wrap", false)
vim.api.nvim_win_set_option(status.preview_win, "scrollbind", false)
vim.api.nvim_win_set_option(preview_winid, "winhl", "Normal:TelescopePreviewNormal")
vim.api.nvim_win_set_option(preview_winid, "signcolumn", "no")
vim.api.nvim_win_set_option(preview_winid, "foldlevel", 100)
vim.api.nvim_win_set_option(preview_winid, "wrap", false)
vim.api.nvim_win_set_option(preview_winid, "scrollbind", false)
self.state.winid = status.preview_win
self.state.winid = preview_winid
self.state.bufname = nil
end
@ -1012,13 +1013,15 @@ previewers.autocommands = defaulter(function(_)
pcall(vim.api.nvim_buf_clear_namespace, self.state.last_set_bufnr, ns_previewer, 0, -1)
end
local preview_winid = status.layout.preview and status.layout.preview.winid
local selected_row = 0
if self.state.bufname ~= entry.value.group_name then
local display = {}
table.insert(display, string.format(" augroup: %s - [ %d entries ]", entry.value.group_name, #results))
-- TODO: calculate banner width/string in setup()
-- TODO: get column characters to be the same HL group as border
table.insert(display, string.rep("", vim.fn.getwininfo(status.preview_win)[1].width))
table.insert(display, string.rep("", vim.fn.getwininfo(preview_winid)[1].width))
for idx, item in ipairs(results) do
if item == entry then
@ -1046,7 +1049,7 @@ previewers.autocommands = defaulter(function(_)
-- set the cursor position after self.state.bufnr is connected to the
-- preview window (which is scheduled in new_buffer_previewer)
vim.schedule(function()
pcall(vim.api.nvim_win_set_cursor, status.preview_win, { selected_row, 0 })
pcall(vim.api.nvim_win_set_cursor, preview_winid, { selected_row, 0 })
end)
self.state.last_set_bufnr = self.state.bufnr

View file

@ -39,7 +39,7 @@ function Previewer:preview(entry, status)
end
if vim.api.nvim_buf_is_valid(self._empty_bufnr) then
vim.api.nvim_win_set_buf(status.preview_win, self._empty_bufnr)
vim.api.nvim_win_set_buf(status.layout.preview.winid, self._empty_bufnr)
end
return
end

View file

@ -185,19 +185,20 @@ previewers.new_termopen_previewer = function(opts)
end
function opts.preview_fn(self, entry, status)
local preview_winid = status.layout.preview and status.layout.preview.winid
if get_bufnr(self) == nil then
set_bufnr(self, vim.api.nvim_win_get_buf(status.preview_win))
set_bufnr(self, vim.api.nvim_win_get_buf(preview_winid))
end
local prev_bufnr = get_bufnr_by_bufentry(self, entry)
if prev_bufnr then
self.state.termopen_bufnr = prev_bufnr
utils.win_set_buf_noautocmd(status.preview_win, self.state.termopen_bufnr)
utils.win_set_buf_noautocmd(preview_winid, self.state.termopen_bufnr)
self.state.termopen_id = term_ids[self.state.termopen_bufnr]
else
local bufnr = vim.api.nvim_create_buf(false, true)
set_bufnr(self, bufnr)
utils.win_set_buf_noautocmd(status.preview_win, bufnr)
utils.win_set_buf_noautocmd(preview_winid, bufnr)
local term_opts = {
cwd = opts.cwd or vim.loop.cwd(),
@ -277,7 +278,7 @@ previewers.vimgrep = defaulter(function(opts)
end,
get_command = function(entry, status)
local win_id = status.preview_win
local win_id = status.layout.preview and status.layout.preview.winid
local height = vim.api.nvim_win_get_height(win_id)
local p = from_entry.path(entry, true, false)
@ -312,7 +313,7 @@ previewers.qflist = defaulter(function(opts)
end,
get_command = function(entry, status)
local win_id = status.preview_win
local win_id = status.layout.preview and status.layout.preview.winid
local height = vim.api.nvim_win_get_height(win_id)
local p = from_entry.path(entry, true, false)

View file

@ -7,7 +7,7 @@ end
test_helpers.get_results_bufnr = function()
local state = require "telescope.state"
return state.get_status(vim.api.nvim_get_current_buf()).results_bufnr
return state.get_status(vim.api.nvim_get_current_buf()).layout.results.bufnr
end
test_helpers.get_file = function()

View file

@ -203,7 +203,7 @@ end
local calc_result_length = function(truncate_len)
local status = get_status(vim.api.nvim_get_current_buf())
local len = vim.api.nvim_win_get_width(status.results_win) - status.picker.selection_caret:len() - 2
local len = vim.api.nvim_win_get_width(status.layout.results.winid) - status.picker.selection_caret:len() - 2
return type(truncate_len) == "number" and len - truncate_len or len
end

View file

@ -16,6 +16,7 @@ docs.test = function()
"./lua/telescope/builtin/init.lua",
"./lua/telescope/themes.lua",
"./lua/telescope/mappings.lua",
"./lua/telescope/pickers/layout.lua",
"./lua/telescope/pickers/layout_strategies.lua",
"./lua/telescope/config/resolve.lua",
"./lua/telescope/make_entry.lua",