diff --git a/doc/telescope.txt b/doc/telescope.txt index c5a3f7154a..a1872a0329 100644 --- a/doc/telescope.txt +++ b/doc/telescope.txt @@ -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* diff --git a/lua/telescope/actions/generate.lua b/lua/telescope/actions/generate.lua index d17e890dc0..a4a3ca7f83 100644 --- a/lua/telescope/actions/generate.lua +++ b/lua/telescope/actions/generate.lua @@ -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 = {} diff --git a/lua/telescope/actions/layout.lua b/lua/telescope/actions/layout.lua index 0e8b27a968..7b0c258245 100644 --- a/lua/telescope/actions/layout.lua +++ b/lua/telescope/actions/layout.lua @@ -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 diff --git a/lua/telescope/actions/set.lua b/lua/telescope/actions/set.lua index d535df5b36..886c47d9f9 100644 --- a/lua/telescope/actions/set.lua +++ b/lua/telescope/actions/set.lua @@ -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) diff --git a/lua/telescope/builtin/__internal.lua b/lua/telescope/builtin/__internal.lua index 885d352c42..d7e342c00f 100644 --- a/lua/telescope/builtin/__internal.lua +++ b/lua/telescope/builtin/__internal.lua @@ -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, diff --git a/lua/telescope/config.lua b/lua/telescope/config.lua index f69f30dd8b..bb217de22f 100644 --- a/lua/telescope/config.lua +++ b/lua/telescope/config.lua @@ -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( diff --git a/lua/telescope/pickers.lua b/lua/telescope/pickers.lua index 1c2dd0015e..c89ee5fb0c 100644 --- a/lua/telescope/pickers.lua +++ b/lua/telescope/pickers.lua @@ -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 line_count = vim.o.lines - vim.o.cmdheight - if vim.o.laststatus ~= 0 then - line_count = line_count - 1 + local layout = self:create_layout() + layout:mount() + + 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 - - 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 + status.layout:update() - -- 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 diff --git a/lua/telescope/pickers/entry_display.lua b/lua/telescope/pickers/entry_display.lua index d201bdd3e2..1353cff13f 100644 --- a/lua/telescope/pickers/entry_display.lua +++ b/lua/telescope/pickers/entry_display.lua @@ -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 diff --git a/lua/telescope/pickers/layout.lua b/lua/telescope/pickers/layout.lua new file mode 100644 index 0000000000..1dde1c444f --- /dev/null +++ b/lua/telescope/pickers/layout.lua @@ -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: ~ +--- +--- 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, +--- } +--- +---@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 }]] diff --git a/lua/telescope/previewers/buffer_previewer.lua b/lua/telescope/previewers/buffer_previewer.lua index ad3779ea37..2c9f829297 100644 --- a/lua/telescope/previewers/buffer_previewer.lua +++ b/lua/telescope/previewers/buffer_previewer.lua @@ -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 diff --git a/lua/telescope/previewers/previewer.lua b/lua/telescope/previewers/previewer.lua index b217a56213..a02d5bf383 100644 --- a/lua/telescope/previewers/previewer.lua +++ b/lua/telescope/previewers/previewer.lua @@ -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 diff --git a/lua/telescope/previewers/term_previewer.lua b/lua/telescope/previewers/term_previewer.lua index 368f6a9794..aa83374bfa 100644 --- a/lua/telescope/previewers/term_previewer.lua +++ b/lua/telescope/previewers/term_previewer.lua @@ -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) diff --git a/lua/telescope/testharness/helpers.lua b/lua/telescope/testharness/helpers.lua index 5296c455ee..1f0b0b1d78 100644 --- a/lua/telescope/testharness/helpers.lua +++ b/lua/telescope/testharness/helpers.lua @@ -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() diff --git a/lua/telescope/utils.lua b/lua/telescope/utils.lua index c194cc6f71..cb87c33830 100644 --- a/lua/telescope/utils.lua +++ b/lua/telescope/utils.lua @@ -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 diff --git a/scripts/gendocs.lua b/scripts/gendocs.lua index cfec1601ef..ec0047c93a 100644 --- a/scripts/gendocs.lua +++ b/scripts/gendocs.lua @@ -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",