---@tag telescope.layout ---@brief [[ --- --- Layout strategies are different functions to position telescope. --- --- All layout strategies are functions with the following signature: --- ---
--- function(picker, columns, lines, layout_config)
--- -- Do some calculations here...
--- return {
--- preview = preview_configuration
--- results = results_configuration,
--- prompt = prompt_configuration,
--- }
--- end
---
--- Parameters: ~
--- - picker : A Picker object. (docs coming soon)
--- - columns : (number) Columns in the vim window
--- - lines : (number) Lines in the vim window
--- - layout_config : (table) The configuration values specific to the picker.
---
---
---
--- This means you can create your own layout strategy if you want! Just be aware
--- for now that we may change some APIs or interfaces, so they may break if you create
--- your own.
---
--- A good method for creating your own would be to copy one of the strategies that most
--- resembles what you want from "./lua/telescope/pickers/layout_strategies.lua" in the
--- telescope repo.
---
---@brief ]]
local resolve = require "telescope.config.resolve"
local p_window = require "telescope.pickers.window"
local if_nil = vim.F.if_nil
local get_border_size = function(opts)
if opts.window.border == false then
return 0
end
return 1
end
local calc_tabline = function(max_lines)
local tbln = (vim.o.showtabline == 2) or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
if tbln then
max_lines = max_lines - 1
end
return max_lines, tbln
end
local layout_strategies = {}
layout_strategies._configurations = {}
--@param strategy_config table: table with keys for each option for a strategy
--@return table: table with keys for each option (for this strategy) and with keys for each layout_strategy
local get_valid_configuration_keys = function(strategy_config)
local valid_configuration_keys = {
-- TEMP: There are a few keys we should say are valid to start with.
preview_cutoff = true,
prompt_position = true,
}
for key in pairs(strategy_config) do
valid_configuration_keys[key] = true
end
for name in pairs(layout_strategies) do
valid_configuration_keys[name] = true
end
return valid_configuration_keys
end
--@param strategy_name string: the name of the layout_strategy we are validating for
--@param configuration table: table with keys for each option available
--@param values table: table containing all of the non-default options we want to set
--@param default_layout_config table: table with the default values to configure layouts
--@return table: table containing the combined options (defaults and non-defaults)
local function validate_layout_config(strategy_name, configuration, values, default_layout_config)
assert(strategy_name, "It is required to have a strategy name for validation.")
local valid_configuration_keys = get_valid_configuration_keys(configuration)
-- If no default_layout_config provided, check Telescope's config values
default_layout_config = if_nil(default_layout_config, require("telescope.config").values.layout_config)
local result = {}
local get_value = function(k)
-- skip "private" items
if string.sub(k, 1, 1) == "_" then
return
end
local val
-- Prioritise options that are specific to this strategy
if values[strategy_name] ~= nil and values[strategy_name][k] ~= nil then
val = values[strategy_name][k]
end
-- Handle nested layout config values
if layout_strategies[k] and strategy_name ~= k and type(val) == "table" then
val = vim.tbl_deep_extend("force", default_layout_config[k], val)
end
if val == nil and values[k] ~= nil then
val = values[k]
end
if val == nil then
if default_layout_config[strategy_name] ~= nil and default_layout_config[strategy_name][k] ~= nil then
val = default_layout_config[strategy_name][k]
else
val = default_layout_config[k]
end
end
return val
end
-- Always set the values passed first.
for k in pairs(values) do
if not valid_configuration_keys[k] then
-- TODO: At some point we'll move to error here,
-- but it's a bit annoying to just straight up crash everyone's stuff.
vim.api.nvim_err_writeln(
string.format(
"Unsupported layout_config key for the %s strategy: %s\n%s",
strategy_name,
k,
vim.inspect(values)
)
)
end
result[k] = get_value(k)
end
-- And then set other valid keys via "inheritance" style extension
for k in pairs(valid_configuration_keys) do
if result[k] == nil then
result[k] = get_value(k)
end
end
return result
end
-- List of options that are shared by more than one layout.
local shared_options = {
width = { "How wide to make Telescope's entire layout", "See |resolver.resolve_width()|" },
height = { "How tall to make Telescope's entire layout", "See |resolver.resolve_height()|" },
mirror = "Flip the location of the results/prompt and preview windows",
scroll_speed = "The number of lines to scroll through the previewer",
}
-- Used for generating vim help documentation.
layout_strategies._format = function(name)
local strategy_config = layout_strategies._configurations[name]
if vim.tbl_isempty(strategy_config) then
return {}
end
local results = { "", "`picker.layout_config` shared options:" }
local strategy_keys = vim.tbl_keys(strategy_config)
table.sort(strategy_keys, function(a, b)
return a < b
end)
local add_value = function(k, val)
if type(val) == "string" then
table.insert(results, string.format(" - %s: %s", k, val))
elseif type(val) == "table" then
table.insert(results, string.format(" - %s:", k))
for _, line in ipairs(val) do
table.insert(results, string.format(" - %s", line))
end
else
error("Unknown type:" .. type(val))
end
end
for _, k in ipairs(strategy_keys) do
if shared_options[k] then
add_value(k, strategy_config[k])
end
end
table.insert(results, "")
table.insert(results, "`picker.layout_config` unique options:")
for _, k in ipairs(strategy_keys) do
if not shared_options[k] then
add_value(k, strategy_config[k])
end
end
table.insert(results, "")
return results
end
--@param name string: the name to be assigned to the layout
--@param layout_config table: table where keys are the available options for the layout
--@param layout function: function with signature
-- function(self, max_columns, max_lines, layout_config): table
-- the returned table is the sizing and location information for the parts of the picker
--@retun function: wrapped function that inputs a validated layout_config into the `layout` function
local function make_documented_layout(name, layout_config, layout)
-- Save configuration data to be used by documentation
layout_strategies._configurations[name] = layout_config
-- Return new function that always validates configuration
return function(self, max_columns, max_lines, override_layout)
return layout(
self,
max_columns,
max_lines,
validate_layout_config(
name,
layout_config,
vim.tbl_deep_extend("keep", if_nil(override_layout, {}), if_nil(self.layout_config, {}))
)
)
end
end
--- Horizontal layout has two columns, one for the preview
--- and one for the prompt and results.
---
--- --- ┌──────────────────────────────────────────────────┐ --- │ │ --- │ ┌───────────────────┐┌───────────────────┐ │ --- │ │ ││ │ │ --- │ │ ││ │ │ --- │ │ ││ │ │ --- │ │ Results ││ │ │ --- │ │ ││ Preview │ │ --- │ │ ││ │ │ --- │ │ ││ │ │ --- │ └───────────────────┘│ │ │ --- │ ┌───────────────────┐│ │ │ --- │ │ Prompt ││ │ │ --- │ └───────────────────┘└───────────────────┘ │ --- │ │ --- └──────────────────────────────────────────────────┘ ------@eval { ["description"] = require('telescope.pickers.layout_strategies')._format("horizontal") } --- layout_strategies.horizontal = make_documented_layout( "horizontal", vim.tbl_extend("error", shared_options, { preview_width = { "Change the width of Telescope's preview window", "See |resolver.resolve_width()|" }, preview_cutoff = "When columns are less than this value, the preview will be disabled", prompt_position = { "Where to place prompt window.", "Available Values: 'bottom', 'top'" }, }), function(self, max_columns, max_lines, layout_config) local initial_options = p_window.get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt local tbln max_lines, tbln = calc_tabline(max_lines) local width_opt = layout_config.width local picker_width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) local width_padding = math.floor((max_columns - picker_width) / 2) local height_opt = layout_config.height local picker_height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) local height_padding = math.floor((max_lines - picker_height) / 2) if self.previewer and max_columns >= layout_config.preview_cutoff then preview.width = resolve.resolve_width(if_nil(layout_config.preview_width, function(_, cols) if cols < 150 then return math.floor(cols * 0.4) elseif cols < 200 then return 80 else return 120 end end))(self, picker_width, max_lines) else preview.width = 0 end results.width = picker_width - preview.width prompt.width = picker_width - preview.width prompt.height = 1 results.height = picker_height - prompt.height - 2 if self.previewer then preview.height = picker_height else preview.height = 0 end -- Default value is false, to use the normal horizontal layout if not layout_config.mirror then results.col = width_padding prompt.col = width_padding preview.col = results.col + results.width + 2 else preview.col = width_padding prompt.col = preview.col + preview.width + 2 results.col = preview.col + preview.width + 2 end preview.line = height_padding if layout_config.prompt_position == "top" then prompt.line = height_padding results.line = prompt.line + prompt.height + 2 elseif layout_config.prompt_position == "bottom" then results.line = height_padding prompt.line = results.line + results.height + 2 else error("Unknown prompt_position: " .. tostring(self.window.prompt_position) .. "\n" .. vim.inspect(layout_config)) end if tbln then prompt.line = prompt.line + 1 results.line = results.line + 1 preview.line = preview.line + 1 end return { preview = self.previewer and preview.width > 0 and preview, results = results, prompt = prompt, } end ) --- Centered layout with a combined block of the prompt --- and results aligned to the middle of the screen. --- The preview window is then placed in the remaining space above. --- Particularly useful for creating dropdown menus --- (see |telescope.themes| and |themes.get_dropdown()|`). --- ---
--- ┌──────────────────────────────────────────────────┐ --- │ ┌────────────────────────────────────────┐ │ --- │ | Preview | │ --- │ | Preview | │ --- │ └────────────────────────────────────────┘ │ --- │ ┌────────────────────────────────────────┐ │ --- │ | Prompt | │ --- │ ├────────────────────────────────────────┤ │ --- │ | Result | │ --- │ | Result | │ --- │ └────────────────────────────────────────┘ │ --- │ │ --- │ │ --- │ │ --- │ │ --- └──────────────────────────────────────────────────┘ ------@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("center") } --- layout_strategies.center = make_documented_layout( "center", vim.tbl_extend("error", shared_options, { preview_cutoff = "When lines are less than this value, the preview will be disabled", }), function(self, max_columns, max_lines, layout_config) local initial_options = p_window.get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt local tbln max_lines, tbln = calc_tabline(max_lines) -- This sets the width for the whole layout local width_opt = layout_config.width local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) -- This sets the number of results displayed local res_height_opt = layout_config.height local res_height = resolve.resolve_height(res_height_opt)(self, max_columns, max_lines) local max_results = (res_height > max_lines and max_lines or res_height) local max_width = (width > max_columns and max_columns or width) local bs = get_border_size(self) prompt.height = 1 results.height = max_results prompt.width = max_width results.width = max_width preview.width = max_width -- Align the prompt and results so halfway up the screen is -- in the middle of this combined block prompt.line = (max_lines / 2) - ((max_results + (bs * 2)) / 2) results.line = prompt.line + 1 + bs preview.line = 1 if self.previewer and max_lines >= layout_config.preview_cutoff then preview.height = math.floor(prompt.line - (2 + bs)) else preview.height = 0 end results.col = math.ceil((max_columns / 2) - (width / 2) - bs) prompt.col = results.col preview.col = results.col if tbln then prompt.line = prompt.line + 1 results.line = results.line + 1 preview.line = preview.line + 1 end return { preview = self.previewer and preview.height > 0 and preview, results = results, prompt = prompt, } end ) --- Cursor layout dynamically positioned below the cursor if possible. --- If there is no place below the cursor it will be placed above. --- ---
--- ┌──────────────────────────────────────────────────┐ --- │ │ --- │ █ │ --- │ ┌──────────────┐┌─────────────────────┐ │ --- │ │ Prompt ││ Preview │ │ --- │ ├──────────────┤│ Preview │ │ --- │ │ Result ││ Preview │ │ --- │ │ Result ││ Preview │ │ --- │ └──────────────┘└─────────────────────┘ │ --- │ █ │ --- │ │ --- │ │ --- │ │ --- │ │ --- │ │ --- └──────────────────────────────────────────────────┘ ---layout_strategies.cursor = make_documented_layout( "cursor", vim.tbl_extend("error", shared_options, { preview_width = { "Change the width of Telescope's preview window", "See |resolver.resolve_width()|" }, preview_cutoff = "When columns are less than this value, the preview will be disabled", }), function(self, max_columns, max_lines, layout_config) local initial_options = p_window.get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt local height_opt = layout_config.height local height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) local width_opt = layout_config.width local width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) local max_width = (width > max_columns and max_columns or width) local bs = get_border_size(self) prompt.height = 1 results.height = height preview.height = results.height + prompt.height + bs if self.previewer and max_columns >= layout_config.preview_cutoff then preview.width = resolve.resolve_width(if_nil(layout_config.preview_width, function(_, cols) -- By default, previewer takes 2/3 of the layout return 2 * math.floor(max_width / 3) end))(self, max_width, max_lines) else preview.width = 0 end prompt.width = max_width - preview.width results.width = prompt.width local total_height = preview.height + (bs * 2) local total_width = prompt.width + (bs * 2) + preview.width + bs local position = vim.api.nvim_win_get_position(0) local top_left = { line = vim.fn.winline() + position[1] + bs, col = vim.fn.wincol() + position[2], } local bot_right = { line = top_left.line + total_height - 1, col = top_left.col + total_width - 1, } if bot_right.line > max_lines then -- position above current line top_left.line = top_left.line - total_height - 1 end if bot_right.col >= max_columns then -- cap to the right of the screen top_left.col = max_columns - total_width end prompt.line = top_left.line results.line = prompt.line + bs + 1 preview.line = prompt.line prompt.col = top_left.col results.col = prompt.col preview.col = results.col + (bs * 2) + results.width return { preview = self.previewer and preview.width > 0 and preview, results = results, prompt = prompt, } end ) --- Vertical layout stacks the items on top of each other. --- Particularly useful with thinner windows. --- ---
--- ┌──────────────────────────────────────────────────┐ --- │ │ --- │ ┌────────────────────────────────────────┐ │ --- │ | Preview | │ --- │ | Preview | │ --- │ | Preview | │ --- │ └────────────────────────────────────────┘ │ --- │ ┌────────────────────────────────────────┐ │ --- │ | Result | │ --- │ | Result | │ --- │ └────────────────────────────────────────┘ │ --- │ ┌────────────────────────────────────────┐ │ --- │ | Prompt | │ --- │ └────────────────────────────────────────┘ │ --- │ │ --- └──────────────────────────────────────────────────┘ ------@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("vertical") } --- layout_strategies.vertical = make_documented_layout( "vertical", vim.tbl_extend("error", shared_options, { preview_cutoff = "When lines are less than this value, the preview will be disabled", preview_height = { "Change the height of Telescope's preview window", "See |resolver.resolve_height()|" }, prompt_position = { "(unimplemented, but we plan on supporting)" }, }), function(self, max_columns, max_lines, layout_config) local initial_options = p_window.get_initial_window_options(self) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt local tbln max_lines, tbln = calc_tabline(max_lines) local width_opt = layout_config.width local picker_width = resolve.resolve_width(width_opt)(self, max_columns, max_lines) local width_padding = math.floor((max_columns - picker_width) / 2) local height_opt = layout_config.height local picker_height = resolve.resolve_height(height_opt)(self, max_columns, max_lines) local height_padding = math.floor((max_lines - picker_height) / 2) if self.previewer and max_lines >= layout_config.preview_cutoff then preview.width = picker_width else preview.width = 0 end results.width = picker_width prompt.width = picker_width local preview_total = 0 preview.height = 0 if self.previewer and max_lines >= layout_config.preview_cutoff then preview.height = resolve.resolve_height(if_nil(layout_config.preview_height, 0.5))( self, max_columns, picker_height ) preview_total = preview.height + 2 end prompt.height = 1 results.height = picker_height - preview_total - prompt.height - 2 results.col, preview.col, prompt.col = width_padding, width_padding, width_padding if not layout_config.mirror then preview.line = height_padding results.line = (preview.height == 0) and preview.line or preview.line + preview.height + 2 prompt.line = results.line + results.height + 2 else prompt.line = height_padding results.line = prompt.line + prompt.height + 2 preview.line = results.line + results.height + 2 end if tbln then prompt.line = prompt.line + 1 results.line = results.line + 1 preview.line = preview.line + 1 end return { preview = self.previewer and preview.height > 0 and preview, results = results, prompt = prompt, } end ) --- Flex layout swaps between `horizontal` and `vertical` strategies based on the window width --- - Supports |layout_strategies.vertical| or |layout_strategies.horizontal| features --- ---@eval { ["description"] = require("telescope.pickers.layout_strategies")._format("flex") } --- layout_strategies.flex = make_documented_layout( "flex", vim.tbl_extend("error", shared_options, { flip_columns = "The number of columns required to move to horizontal mode", flip_lines = "The number of lines required to move to horizontal mode", vertical = "Options to pass when switching to vertical layout", horizontal = "Options to pass when switching to horizontal layout", }), function(self, max_columns, max_lines, layout_config) local flip_columns = if_nil(layout_config.flip_columns, 100) local flip_lines = if_nil(layout_config.flip_lines, 20) if max_columns < flip_columns and max_lines > flip_lines then return layout_strategies.vertical(self, max_columns, max_lines, layout_config.vertical) else return layout_strategies.horizontal(self, max_columns, max_lines, layout_config.horizontal) end end ) layout_strategies.current_buffer = make_documented_layout("current_buffer", { -- No custom options. -- height, width ignored }, function(self, _, _, _) local initial_options = p_window.get_initial_window_options(self) local window_width = vim.api.nvim_win_get_width(0) local window_height = vim.api.nvim_win_get_height(0) local preview = initial_options.preview local results = initial_options.results local prompt = initial_options.prompt local width_padding = 2 local width = window_width - width_padding * 2 if not self.previewer then preview.width = 0 else preview.width = width end results.width = width prompt.width = width -- Height local height_padding = 3 results.height = 10 prompt.height = 1 -- The last 2 * 2 is for the extra borders if self.previewer then preview.height = window_height - results.height - prompt.height - 2 * 2 - height_padding * 2 else results.height = window_height - prompt.height - 2 - height_padding * 2 end local win_position = vim.api.nvim_win_get_position(0) local line = win_position[1] if self.previewer then preview.line = height_padding + line results.line = preview.line + preview.height + 2 prompt.line = results.line + results.height + 2 else results.line = height_padding + line prompt.line = results.line + results.height + 2 end local col = win_position[2] + width_padding preview.col, results.col, prompt.col = col, col, col return { preview = preview.width > 0 and preview, results = results, prompt = prompt, } end) --- Bottom pane can be used to create layouts similar to "ivy". --- --- For an easy ivy configuration, see |themes.get_ivy()| layout_strategies.bottom_pane = make_documented_layout( "bottom_pane", vim.tbl_extend("error", shared_options, { -- No custom options... }), function(self, max_columns, max_lines, layout_config) local initial_options = p_window.get_initial_window_options(self) local results = initial_options.results local prompt = initial_options.prompt local preview = initial_options.preview local result_height = if_nil(resolve.resolve_height(layout_config.height)(self, max_columns, max_lines), 25) local prompt_width = max_columns local col = 0 local has_border = not not self.window.border if has_border then col = 1 prompt_width = prompt_width - 2 end local left_side_width if self.previewer then left_side_width = math.floor(prompt_width / 2) local base_col if layout_config.mirror then base_col = 0 else base_col = left_side_width + 1 end if has_border then preview = vim.tbl_deep_extend("force", { col = base_col + 2, line = max_lines - result_height + 1, width = prompt_width - left_side_width - 2, height = result_height - 1, }, preview) else preview = vim.tbl_deep_extend("force", { col = base_col, line = max_lines - result_height, width = prompt_width - left_side_width, height = result_height, }, preview) end else preview = nil left_side_width = prompt_width end local result_col if layout_config.mirror and self.previewer then result_col = left_side_width + 2 if has_border then left_side_width = left_side_width - 2 end else result_col = col end return { preview = preview, prompt = vim.tbl_deep_extend("force", prompt, { line = max_lines - result_height - 1, col = col, height = 1, width = prompt_width, }), results = vim.tbl_deep_extend("force", results, { line = max_lines - result_height, col = result_col, height = result_height, width = left_side_width, }), } end ) layout_strategies._validate_layout_config = validate_layout_config return layout_strategies