diff options
| author | TJ DeVries <devries.timothyj@gmail.com> | 2021-07-01 02:41:58 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-07-01 05:41:58 -0400 |
| commit | 5a53ec5c2fdab10ca8775d3979b1a85e63d57953 (patch) | |
| tree | 40c62f0a260a3328c3b5578c06a729b76d36d5cb /lua/telescope/pickers/layout_strategies.lua | |
| parent | e5bd4963da81b5d044749ee4507061801aeb0f78 (diff) | |
feat: Consistent and sensible layout_config (#922)
* feat: Consistent and sensible layout_config
* [docgen] Update doc/telescope.txt
skip-checks: true
* [WIP]: Thu 17 Jun 2021 03:36:44 PM EDT
* [WIP]: Thu 17 Jun 2021 03:38:11 PM EDT
* layout_default -> layout_defaults
* remove options from bug repot
* Conni2461 suggestions: part 1
* [docgen] Update doc/telescope.txt
skip-checks: true
* Conni2461 suggestions: part 2
* [docgen] Update doc/telescope.txt
skip-checks: true
* Linting
* Improve deprecation checks
- Move `layout_defaults` handling to `deprecated.lua`
- Check for "layout keys" outside of `layout_config` on `setup`
* fixup: Just add a few more words
Co-authored-by: Luke Kershaw <35707277+l-kershaw@users.noreply.github.com>
Co-authored-by: Github Actions <actions@github>
Diffstat (limited to 'lua/telescope/pickers/layout_strategies.lua')
| -rw-r--r-- | lua/telescope/pickers/layout_strategies.lua | 532 |
1 files changed, 338 insertions, 194 deletions
diff --git a/lua/telescope/pickers/layout_strategies.lua b/lua/telescope/pickers/layout_strategies.lua index 21994b3..56100c3 100644 --- a/lua/telescope/pickers/layout_strategies.lua +++ b/lua/telescope/pickers/layout_strategies.lua @@ -7,7 +7,7 @@ --- All layout strategies are functions with the following signature: --- --- <pre> ---- function(picker, columns, lines) +--- function(picker, columns, lines, layout_config) --- -- Do some calculations here... --- return { --- preview = preview_configuration @@ -17,136 +17,257 @@ --- end --- --- Parameters: ~ ---- - picker : A Picker object. (docs coming soon) ---- - columns : number Columns in the vim window ---- - lines : number Lines in the vim window +--- - 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. --- --- </pre> --- ---- TODO: I would like to make these link to `telescope.layout_strategies.*`, ---- but it's not yet possible. +--- 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. --- ---- Available layout strategies include: ---- - horizontal: ---- - See |layout_strategies.horizontal| +--- 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. --- ---- - vertical: ---- - See |layout_strategies.vertical| ---- ---- - flex: ---- - See |layout_strategies.flex| ---- ---- Available tweaks to the settings in layout defaults include ---- (can be applied to horizontal and vertical layouts): ---- - mirror (default is `false`): ---- - Flip the view of the current layout: ---- - If using horizontal: if `true`, swaps the location of the ---- results/prompt window and preview window ---- - If using vertical: if `true`, swaps the location of the results and ---- prompt windows ---- ---- - width_padding: ---- - How many cells to pad the width of Telescope's layout window ---- ---- - height_padding: ---- - How many cells to pad the height of Telescope's layout window ---- ---- - preview_width: ---- - Change the width of Telescope's preview window ---- ---- - scroll_speed: ---- - Change the scrolling speed of the previewer ---@brief ]] -local config = require('telescope.config') -local resolve = require("telescope.config.resolve") - +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 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 --- Check if there are any borders. Right now it's a little raw as --- there are a few things that contribute to the border -local is_borderless = function(opts) - return opts.window.border == false + 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 -local function validate_layout_config(options, values) - for k, _ in pairs(options) do - if not values[k] then - error(string.format( - "Unsupported layout_config key: %s\n%s", - k, - vim.inspect(values) + -- 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 options + return result end -local layout_strategies = {} +-- 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 ---- Horizontal previewer + local results = {"<pre>", "`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, "</pre>") + 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. --- --- <pre> ---- +-------------+--------------+ ---- | | | ---- | Results | | ---- | | Preview | ---- | | | ---- +-------------| | ---- | Prompt | | ---- +-------------+--------------+ +--- ┌──────────────────────────────────────────────────┐ +--- │ │ +--- │ ┌───────────────────┐┌───────────────────┐ │ +--- │ │ ││ │ │ +--- │ │ ││ │ │ +--- │ │ ││ │ │ +--- │ │ Results ││ │ │ +--- │ │ ││ Preview │ │ +--- │ │ ││ │ │ +--- │ │ ││ │ │ +--- │ └───────────────────┘│ │ │ +--- │ ┌───────────────────┐│ │ │ +--- │ │ Prompt ││ │ │ +--- │ └───────────────────┘└───────────────────┘ │ +--- │ │ +--- └──────────────────────────────────────────────────┘ --- </pre> -layout_strategies.horizontal = function(self, max_columns, max_lines) - local layout_config = validate_layout_config(self.layout_config or {}, { - width_padding = "How many cells to pad the width", - height_padding = "How many cells to pad the height", - preview_width = "(Resolvable): Determine preview width", - mirror = "Flip the location of the results/prompt and preview windows", - scroll_speed = "The speed when scrolling through the previewer", - }) - +---@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 - -- TODO: Test with 120 width terminal - -- TODO: Test with self.width - local width_padding = resolve.resolve_width(layout_config.width_padding or function(_, cols) - if cols < self.preview_cutoff then - return 2 - elseif cols < 150 then - return 5 - else - return 10 - end - end)(self, max_columns, max_lines) - local picker_width = max_columns - 2 * width_padding + 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_padding = resolve.resolve_height(layout_config.height_padding or function(_, _, lines) - if lines < 40 then - return 4 - else - return math.floor(0.1 * lines) - end - end)(self, max_columns, max_lines) - local picker_height = max_lines - 2 * height_padding + 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 then - preview.width = resolve.resolve_width(layout_config.preview_width or function(_, cols) - if not self.previewer or cols < self.preview_cutoff then - return 0 - elseif cols < 150 then + 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) + end))(self, picker_width, max_lines) else preview.width = 0 end @@ -175,14 +296,14 @@ layout_strategies.horizontal = function(self, max_columns, max_lines) end preview.line = height_padding - if self.window.prompt_position == "top" then + if layout_config.prompt_position == "top" then prompt.line = height_padding results.line = prompt.line + prompt.height + 2 - elseif self.window.prompt_position == "bottom" then + elseif layout_config.prompt_position == "bottom" then results.line = height_padding prompt.line = results.line + results.height + 2 else - error("Unknown prompt_position: " .. self.window.prompt_position) + error("Unknown prompt_position: " .. tostring(self.window.prompt_position) .. "\n" .. vim.inspect(layout_config)) end return { @@ -190,33 +311,54 @@ layout_strategies.horizontal = function(self, max_columns, max_lines) results = results, prompt = prompt } -end +end) ---- Centered layout wih smaller default sizes (I think) +--- 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()|`). --- --- <pre> ---- +--------------+ ---- | Preview | ---- +--------------+ ---- | Prompt | ---- +--------------+ ---- | Result | ---- | Result | ---- | Result | ---- +--------------+ +--- ┌──────────────────────────────────────────────────┐ +--- │ ┌────────────────────────────────────────┐ │ +--- │ | Preview | │ +--- │ | Preview | │ +--- │ └────────────────────────────────────────┘ │ +--- │ ┌────────────────────────────────────────┐ │ +--- │ | Prompt | │ +--- │ ├────────────────────────────────────────┤ │ +--- │ | Result | │ +--- │ | Result | │ +--- │ └────────────────────────────────────────┘ │ +--- │ │ +--- │ │ +--- │ │ +--- │ │ +--- └──────────────────────────────────────────────────┘ --- </pre> -layout_strategies.center = function(self, columns, lines) +---@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 - -- This sets the height/width for the whole layout - local height = resolve.resolve_height(self.window.results_height)(self, columns, lines) - local width = resolve.resolve_width(self.window.width)(self, columns, 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 = (height > lines and lines or height) - local max_width = (width > columns and columns or width) + 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 @@ -225,87 +367,84 @@ layout_strategies.center = function(self, columns, lines) results.width = max_width preview.width = max_width - -- border size - local bs = 1 - if is_borderless(self) then - bs = 0 - end - - prompt.line = (lines / 2) - ((max_results + (bs * 2)) / 2) + -- 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 - preview.height = math.floor(prompt.line - (2 + bs)) - if not self.previewer or columns < self.preview_cutoff then + 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((columns / 2) - (width / 2) - bs) + results.col = math.ceil((max_columns / 2) - (width / 2) - bs) prompt.col = results.col preview.col = results.col return { - preview = self.previewer and preview.width > 0 and preview, + preview = self.previewer and preview.height > 0 and preview, results = results, prompt = prompt } -end +end) ---- Vertical perviewer stacks the items on top of each other. +--- Vertical layout stacks the items on top of each other. +--- Particularly useful with thinner windows. --- --- <pre> ---- +-----------------+ ---- | Previewer | ---- | Previewer | ---- | Previewer | ---- +-----------------+ ---- | Result | ---- | Result | ---- | Result | ---- +-----------------+ ---- | Prompt | ---- +-----------------+ +--- ┌──────────────────────────────────────────────────┐ +--- │ │ +--- │ ┌────────────────────────────────────────┐ │ +--- │ | Preview | │ +--- │ | Preview | │ +--- │ | Preview | │ +--- │ └────────────────────────────────────────┘ │ +--- │ ┌────────────────────────────────────────┐ │ +--- │ | Result | │ +--- │ | Result | │ +--- │ └────────────────────────────────────────┘ │ +--- │ ┌────────────────────────────────────────┐ │ +--- │ | Prompt | │ +--- │ └────────────────────────────────────────┘ │ +--- │ │ +--- └──────────────────────────────────────────────────┘ --- </pre> -layout_strategies.vertical = function(self, max_columns, max_lines) - local layout_config = validate_layout_config(self.layout_config or {}, { - width_padding = "How many cells to pad the width", - height_padding = "How many cells to pad the height", - preview_height = "(Resolvable): Determine preview height", - mirror = "Flip the locations of the results and prompt windows", - scroll_speed = "The speed when scrolling through the previewer", - }) - +---@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 width_padding = resolve.resolve_width( - layout_config.width_padding or math.ceil((1 - self.window.width) * 0.5 * max_columns) - )(self, max_columns, 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 width = max_columns - width_padding * 2 - if not self.previewer then - preview.width = 0 + 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 = width + preview.width = 0 end - results.width = width - prompt.width = width - - -- Height - local height_padding = math.max( - 1, - resolve.resolve_height(layout_config.height_padding or 3)(self, max_columns, max_lines) - ) - local picker_height = max_lines - 2 * height_padding + results.width = picker_width + prompt.width = picker_width local preview_total = 0 preview.height = 0 - if self.previewer then + if self.previewer and max_lines >= layout_config.preview_cutoff then preview.height = resolve.resolve_height( - layout_config.preview_height or (max_lines - 15) + if_nil(layout_config.preview_height, 0.5) )(self, max_columns, picker_height) preview_total = preview.height + 2 @@ -332,36 +471,38 @@ layout_strategies.vertical = function(self, max_columns, max_lines) end return { - preview = self.previewer and preview.width > 0 and preview, + preview = self.previewer and preview.height > 0 and preview, results = results, prompt = prompt } -end +end) ---- Swap between `horizontal` and `vertical` strategies based on the window width ---- - Supports `vertical` or `horizontal` features +--- Flex layout swaps between `horizontal` and `vertical` strategies based on the window width +--- - Supports |layout_strategies.vertical| or |layout_strategies.horizontal| features --- ---- Uses: ---- - flip_columns ---- - flip_lines -layout_strategies.flex = function(self, max_columns, max_lines) - local layout_config = self.layout_config or {} - - local flip_columns = layout_config.flip_columns or 100 - local flip_lines = layout_config.flip_lines or 20 +---@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 - -- TODO: This feels a bit like a hack.... cause you wouldn't be able to pass this to flex easily. - self.layout_config = (config.values.layout_defaults or {})['vertical'] - return layout_strategies.vertical(self, max_columns, max_lines) + return layout_strategies.vertical(self, max_columns, max_lines, layout_config.vertical) else - self.layout_config = (config.values.layout_defaults or {})['horizontal'] - return layout_strategies.horizontal(self, max_columns, max_lines) + return layout_strategies.horizontal(self, max_columns, max_lines, layout_config.horizontal) end -end +end) -layout_strategies.current_buffer = function(self, _, _) - local initial_options = self:_get_initial_window_options() +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) @@ -414,19 +555,20 @@ layout_strategies.current_buffer = function(self, _, _) results = results, prompt = prompt, } -end - -layout_strategies.bottom_pane = function(self, max_columns, max_lines) - local layout_config = validate_layout_config(self.layout_config or {}, { - height = "The height of the layout", - }) +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 = layout_config.height or 25 + 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 @@ -477,6 +619,8 @@ layout_strategies.bottom_pane = function(self, max_columns, max_lines) width = result_width, }), } -end +end) + +layout_strategies._validate_layout_config = validate_layout_config return layout_strategies |
