From 5f37fbfa837dfee7ecd30f388b271f4a71c0a9e0 Mon Sep 17 00:00:00 2001
From: Luke Kershaw <35707277+l-kershaw@users.noreply.github.com>
Date: Fri, 10 Dec 2021 19:08:24 +0000
Subject: [PATCH] feat: layout `anchor` (#1582)
* feat: add `anchor` option to some `layout_strategies`
* tests: improve tests for `resolve_height/width`
---
doc/telescope.txt | 29 ++++
lua/telescope/config/resolve.lua | 29 ++++
lua/telescope/pickers/layout_strategies.lua | 32 ++++-
lua/tests/automated/resolver_spec.lua | 140 +++++++++++++++++++-
4 files changed, 223 insertions(+), 7 deletions(-)
diff --git a/doc/telescope.txt b/doc/telescope.txt
index eae3ebe..1991e45 100644
--- a/doc/telescope.txt
+++ b/doc/telescope.txt
@@ -1609,6 +1609,9 @@ layout_strategies.horizontal() *layout_strategies.horizontal()*
└──────────────────────────────────────────────────┘
`picker.layout_config` shared options:
+ - anchor:
+ - Which edge/corner to pin the picker to
+ - See |resolver.resolve_anchor_pos()|
- height:
- How tall to make Telescope's entire layout
- See |resolver.resolve_height()|
@@ -1634,6 +1637,9 @@ layout_strategies.center() *layout_strategies.center()*
remaining space above. Particularly useful for creating dropdown menus (see
|telescope.themes| and |themes.get_dropdown()|`).
+ Note that the `anchor` option can only pin this layout to the left or right
+ edges.
+
┌──────────────────────────────────────────────────┐
│ ┌────────────────────────────────────────┐ │
│ | Preview | │
@@ -1652,6 +1658,9 @@ layout_strategies.center() *layout_strategies.center()*
└──────────────────────────────────────────────────┘
`picker.layout_config` shared options:
+ - anchor:
+ - Which edge/corner to pin the picker to
+ - See |resolver.resolve_anchor_pos()|
- height:
- How tall to make Telescope's entire layout
- See |resolver.resolve_height()|
@@ -1713,6 +1722,9 @@ layout_strategies.vertical() *layout_strategies.vertical()*
└──────────────────────────────────────────────────┘
`picker.layout_config` shared options:
+ - anchor:
+ - Which edge/corner to pin the picker to
+ - See |resolver.resolve_anchor_pos()|
- height:
- How tall to make Telescope's entire layout
- See |resolver.resolve_height()|
@@ -1740,6 +1752,9 @@ layout_strategies.flex() *layout_strategies.flex()*
`picker.layout_config` shared options:
+ - anchor:
+ - Which edge/corner to pin the picker to
+ - See |resolver.resolve_anchor_pos()|
- height:
- How tall to make Telescope's entire layout
- See |resolver.resolve_height()|
@@ -1812,6 +1827,20 @@ resolver.resolve_width() *resolver.resolve_width()*
+resolver.resolve_anchor_pos() *resolver.resolve_anchor_pos()*
+ Calculates the adjustment required to move the picker from the middle of
+ the screen to an edge or corner.
+ The `anchor` can be any of the following strings:
+ - "", "CENTER", "NW", "N", "NE", "E", "SE", "S", "SW", "W" The anchors
+ have the following meanings:
+ - "" or "CENTER":
+ the picker will remain in the middle of the screen.
+ - Compass directions:
+ the picker will move to the corresponding edge/corner e.g. "NW" -> "top
+ left corner", "E" -> "right edge", "S" -> "bottom edge"
+
+
+
================================================================================
*telescope.actions*
diff --git a/lua/telescope/config/resolve.lua b/lua/telescope/config/resolve.lua
index 9875565..3b47025 100644
--- a/lua/telescope/config/resolve.lua
+++ b/lua/telescope/config/resolve.lua
@@ -213,6 +213,35 @@ resolver.resolve_width = function(val)
error("invalid configuration option for width:" .. tostring(val))
end
+--- Calculates the adjustment required to move the picker from the middle of the screen to
+--- an edge or corner.
+--- The `anchor` can be any of the following strings:
+--- - "", "CENTER", "NW", "N", "NE", "E", "SE", "S", "SW", "W"
+--- The anchors have the following meanings:
+--- - "" or "CENTER":
+--- the picker will remain in the middle of the screen.
+--- - Compass directions:
+--- the picker will move to the corresponding edge/corner
+--- e.g. "NW" -> "top left corner", "E" -> "right edge", "S" -> "bottom edge"
+resolver.resolve_anchor_pos = function(anchor, p_width, p_height, max_columns, max_lines)
+ anchor = anchor:upper()
+ local pos = { 0, 0 }
+ if anchor == "CENTER" then
+ return pos
+ end
+ if anchor:find "W" then
+ pos[1] = math.ceil((p_width - max_columns) / 2) + 1
+ elseif anchor:find "E" then
+ pos[1] = math.ceil((max_columns - p_width) / 2) - 1
+ end
+ if anchor:find "N" then
+ pos[2] = math.ceil((p_height - max_lines) / 2) + 1
+ elseif anchor:find "S" then
+ pos[2] = math.ceil((max_lines - p_height) / 2) - 1
+ end
+ return pos
+end
+
-- Win option always returns a table with preview, results, and prompt.
-- It handles many different ways. Some examples are as follows:
--
diff --git a/lua/telescope/pickers/layout_strategies.lua b/lua/telescope/pickers/layout_strategies.lua
index 9b1b2cd..7159797 100644
--- a/lua/telescope/pickers/layout_strategies.lua
+++ b/lua/telescope/pickers/layout_strategies.lua
@@ -107,6 +107,13 @@ local get_valid_configuration_keys = function(strategy_config)
return valid_configuration_keys
end
+local adjust_pos = function(pos, ...)
+ for _, opts in ipairs { ... } do
+ opts.col = opts.col and opts.col + pos[1]
+ opts.line = opts.line and opts.line + pos[2]
+ end
+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
@@ -187,6 +194,7 @@ local shared_options = {
mirror = "Flip the location of the results/prompt and preview windows",
scroll_speed = "The number of lines to scroll through the previewer",
prompt_position = { "Where to place prompt window.", "Available Values: 'bottom', 'top'" },
+ anchor = { "Which edge/corner to pin the picker to", "See |resolver.resolve_anchor_pos()|" },
}
-- Used for generating vim help documentation.
@@ -368,6 +376,9 @@ layout_strategies.horizontal = make_documented_layout(
error(string.format("Unknown prompt_position: %s\n%s", self.window.prompt_position, vim.inspect(layout_config)))
end
+ local anchor_pos = resolve.resolve_anchor_pos(layout_config.anchor or "", width, height, max_columns, max_lines)
+ adjust_pos(anchor_pos, prompt, results, preview)
+
if tbln then
prompt.line = prompt.line + 1
results.line = results.line + 1
@@ -388,6 +399,8 @@ layout_strategies.horizontal = make_documented_layout(
--- Particularly useful for creating dropdown menus
--- (see |telescope.themes| and |themes.get_dropdown()|`).
---
+--- Note that the `anchor` option can only pin this layout to the left or right edges.
+---
---
--- ┌──────────────────────────────────────────────────┐
--- │ ┌────────────────────────────────────────┐ │
@@ -475,7 +488,12 @@ layout_strategies.center = make_documented_layout(
preview.height = 0
end
- results.col, preview.col, prompt.col = 0, 0, 0 -- all centered
+ local width_padding = math.floor((max_columns - width) / 2) + bs + 1
+ results.col, preview.col, prompt.col = width_padding, width_padding, width_padding
+
+ local anchor_pos = resolve.resolve_anchor_pos(layout_config.anchor or "", width, height, max_columns, max_lines)
+ anchor_pos[2] = 0 -- only use horizontal anchoring
+ adjust_pos(anchor_pos, prompt, results, preview)
if tbln then
prompt.line = prompt.line + 1
@@ -514,7 +532,11 @@ layout_strategies.center = make_documented_layout(
---
layout_strategies.cursor = make_documented_layout(
"cursor",
- vim.tbl_extend("error", shared_options, {
+ vim.tbl_extend("error", {
+ width = shared_options.width,
+ height = shared_options.height,
+ scroll_speed = shared_options.scroll_speed,
+ }, {
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",
}),
@@ -661,7 +683,8 @@ layout_strategies.vertical = make_documented_layout(
prompt.height = 1
results.height = height - preview.height - prompt.height - h_space
- results.col, preview.col, prompt.col = 0, 0, 0 -- all centered
+ local width_padding = math.floor((max_columns - width) / 2) + bs + 1
+ results.col, preview.col, prompt.col = width_padding, width_padding, width_padding
local height_padding = math.floor((max_lines - height) / 2)
if not layout_config.mirror then
@@ -689,6 +712,9 @@ layout_strategies.vertical = make_documented_layout(
end
end
+ local anchor_pos = resolve.resolve_anchor_pos(layout_config.anchor or "", width, height, max_columns, max_lines)
+ adjust_pos(anchor_pos, prompt, results, preview)
+
if tbln then
prompt.line = prompt.line + 1
results.line = results.line + 1
diff --git a/lua/tests/automated/resolver_spec.lua b/lua/tests/automated/resolver_spec.lua
index ac141bd..0251ec0 100644
--- a/lua/tests/automated/resolver_spec.lua
+++ b/lua/tests/automated/resolver_spec.lua
@@ -59,10 +59,142 @@ describe("telescope.config.resolve", function()
end)
describe("resolve_height/width", function()
- eq(10, resolve.resolve_height(0.1)(nil, 24, 100))
- eq(2, resolve.resolve_width(0.1)(nil, 24, 100))
+ local test_sizes = {
+ { 24, 100 },
+ { 35, 125 },
+ { 60, 59 },
+ { 100, 40 },
+ }
+ it("should handle percentages", function()
+ local percentages = { 0.1, 0.33333, 0.5, 0.99 }
+ for _, s in ipairs(test_sizes) do
+ for _, p in ipairs(percentages) do
+ eq(math.floor(s[1] * p), resolve.resolve_width(p)(nil, unpack(s)))
+ eq(math.floor(s[2] * p), resolve.resolve_height(p)(nil, unpack(s)))
+ end
+ end
+ end)
- eq(10, resolve.resolve_width(10)(nil, 24, 100))
- eq(24, resolve.resolve_width(50)(nil, 24, 100))
+ it("should handle fixed size", function()
+ local fixed = { 5, 8, 13, 21, 34 }
+ for _, s in ipairs(test_sizes) do
+ for _, f in ipairs(fixed) do
+ eq(math.min(f, s[1]), resolve.resolve_width(f)(nil, unpack(s)))
+ eq(math.min(f, s[2]), resolve.resolve_height(f)(nil, unpack(s)))
+ end
+ end
+ end)
+
+ it("should handle functions", function()
+ local func = function(_, max_columns, max_lines)
+ if max_columns < 45 then
+ return math.min(max_columns, max_lines)
+ elseif max_columns < max_lines then
+ return max_columns * 0.8
+ else
+ return math.min(max_columns, max_lines) * 0.5
+ end
+ end
+ for _, s in ipairs(test_sizes) do
+ eq(func(nil, unpack(s)), resolve.resolve_height(func)(nil, unpack(s)))
+ end
+ end)
+
+ it("should handle padding", function()
+ local func = function(_, max_columns, max_lines)
+ return math.floor(math.min(max_columns * 0.6, max_lines * 0.8))
+ end
+ local pads = { 0.1, 5, func }
+ for _, s in ipairs(test_sizes) do
+ for _, p in ipairs(pads) do
+ eq(s[1] - 2 * resolve.resolve_width(p)(nil, unpack(s)), resolve.resolve_width { padding = p }(nil, unpack(s)))
+ eq(
+ s[2] - 2 * resolve.resolve_height(p)(nil, unpack(s)),
+ resolve.resolve_height { padding = p }(nil, unpack(s))
+ )
+ end
+ end
+ end)
+ end)
+
+ describe("resolve_anchor_pos", function()
+ local test_sizes = {
+ { 6, 7, 8, 9 },
+ { 10, 20, 30, 40 },
+ { 15, 15, 16, 16 },
+ { 17, 19, 23, 31 },
+ { 21, 18, 26, 24 },
+ { 50, 100, 150, 200 },
+ }
+
+ it([[should not adjust when "CENTER" or "" is the anchor]], function()
+ for _, s in ipairs(test_sizes) do
+ eq({ 0, 0 }, resolve.resolve_anchor_pos("", unpack(s)))
+ eq({ 0, 0 }, resolve.resolve_anchor_pos("center", unpack(s)))
+ eq({ 0, 0 }, resolve.resolve_anchor_pos("CENTER", unpack(s)))
+ end
+ end)
+
+ it([[should end up at top when "N" in the anchor]], function()
+ local top_test = function(anchor, p_width, p_height, max_columns, max_lines)
+ local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines)
+ eq(1, pos[2] + math.floor((max_lines - p_height) / 2))
+ end
+ for _, s in ipairs(test_sizes) do
+ top_test("NW", unpack(s))
+ top_test("N", unpack(s))
+ top_test("NE", unpack(s))
+ end
+ end)
+
+ it([[should end up at left when "W" in the anchor]], function()
+ local left_test = function(anchor, p_width, p_height, max_columns, max_lines)
+ local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines)
+ eq(1, pos[1] + math.floor((max_columns - p_width) / 2))
+ end
+ for _, s in ipairs(test_sizes) do
+ left_test("NW", unpack(s))
+ left_test("W", unpack(s))
+ left_test("SW", unpack(s))
+ end
+ end)
+
+ it([[should end up at bottom when "S" in the anchor]], function()
+ local bot_test = function(anchor, p_width, p_height, max_columns, max_lines)
+ local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines)
+ eq(max_lines - 1, pos[2] + p_height + math.floor((max_lines - p_height) / 2))
+ end
+ for _, s in ipairs(test_sizes) do
+ bot_test("SW", unpack(s))
+ bot_test("S", unpack(s))
+ bot_test("SE", unpack(s))
+ end
+ end)
+
+ it([[should end up at right when "E" in the anchor]], function()
+ local right_test = function(anchor, p_width, p_height, max_columns, max_lines)
+ local pos = resolve.resolve_anchor_pos(anchor, p_width, p_height, max_columns, max_lines)
+ eq(max_columns - 1, pos[1] + p_width + math.floor((max_columns - p_width) / 2))
+ end
+ for _, s in ipairs(test_sizes) do
+ right_test("NE", unpack(s))
+ right_test("E", unpack(s))
+ right_test("SE", unpack(s))
+ end
+ end)
+
+ it([[should ignore casing of the anchor]], function()
+ local case_test = function(a1, a2, p_width, p_height, max_columns, max_lines)
+ local pos1 = resolve.resolve_anchor_pos(a1, p_width, p_height, max_columns, max_lines)
+ local pos2 = resolve.resolve_anchor_pos(a2, p_width, p_height, max_columns, max_lines)
+ eq(pos1, pos2)
+ end
+ for _, s in ipairs(test_sizes) do
+ case_test("ne", "NE", unpack(s))
+ case_test("w", "W", unpack(s))
+ case_test("sW", "sw", unpack(s))
+ case_test("cEnTeR", "CeNtEr", unpack(s))
+ end
+ end)
end)
end)