feat: layout anchor (#1582)

* feat: add `anchor` option to some `layout_strategies`

* tests: improve tests for `resolve_height/width`
This commit is contained in:
Luke Kershaw
2021-12-10 19:08:24 +00:00
committed by GitHub
parent 5e5351ef13
commit 5f37fbfa83
4 changed files with 223 additions and 7 deletions

View File

@@ -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. <br>
--- 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":<br>
--- the picker will remain in the middle of the screen.
--- - Compass directions:<br>
--- 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:
--

View File

@@ -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.
---
--- <pre>
--- ┌──────────────────────────────────────────────────┐
--- │ ┌────────────────────────────────────────┐ │
@@ -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(
--- </pre>
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

View File

@@ -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)