diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index d2d7e9faac..1e57b85153 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1580,7 +1580,8 @@ hover({_}, {result}, {ctx}, {config}) *vim.lsp.handlers.hover()* • {config} (table) Configuration table. • border: (default=nil) • Add borders to the floating window - • See |nvim_open_win()| + • See |vim.lsp.util.open_floating_preview()| for more + options. *vim.lsp.handlers.signature_help()* signature_help({_}, {result}, {ctx}, {config}) @@ -1599,7 +1600,8 @@ signature_help({_}, {result}, {ctx}, {config}) • {config} (table) Configuration table. • border: (default=nil) • Add borders to the floating window - • See |nvim_open_win()| + • See |vim.lsp.util.open_floating_preview()| for more + options ============================================================================== @@ -1791,6 +1793,13 @@ make_floating_popup_options({width}, {height}, {opts}) • focusable (string or table) override `focusable` • zindex (string or table) override `zindex`, defaults to 50 • relative ("mouse"|"cursor") defaults to "cursor" + • anchor_bias ("auto"|"above"|"below") defaults to "auto" + • "auto": place window based on which side of the cursor + has more lines + • "above": place the window above the cursor unless there + are not enough lines to display the full window height. + • "below": place the window below the cursor unless there + are not enough lines to display the full window height. Return: ~ (table) Options @@ -1892,8 +1901,9 @@ open_floating_preview({contents}, {syntax}, {opts}) Parameters: ~ • {contents} (table) of lines to show in window • {syntax} (string) of syntax to set for opened buffer - • {opts} (table) with optional fields (additional keys are passed - on to |nvim_open_win()|) + • {opts} (table) with optional fields (additional keys are filtered + with |vim.lsp.util.make_floating_popup_options()| before + they are passed on to |nvim_open_win()|) • height: (integer) height of floating window • width: (integer) width of floating window • wrap: (boolean, default true) wrap long lines diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 972f37f0e9..cd977a8b5f 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -120,6 +120,8 @@ The following new APIs and features were added. indicator to see if a server supports a feature. Instead use `client.supports_method()`. It considers both the dynamic capabilities and static `server_capabilities`. + • Added a new `anchor_bias` option to |lsp-handlers| to aid in positioning of + floating windows. • Treesitter • Bundled parsers and queries (highlight, folds) for Markdown, Python, and diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a6b70ac911..81d4d6cceb 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -355,7 +355,7 @@ end ---@param config table Configuration table. --- - border: (default=nil) --- - Add borders to the floating window ---- - See |nvim_open_win()| +--- - See |vim.lsp.util.open_floating_preview()| for more options. function M.hover(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -442,7 +442,7 @@ M[ms.textDocument_implementation] = location_handler ---@param config table Configuration table. --- - border: (default=nil) --- - Add borders to the floating window ---- - See |nvim_open_win()| +--- - See |vim.lsp.util.open_floating_preview()| for more options function M.signature_help(_, result, ctx, config) config = config or {} config.focus_id = ctx.method diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index a6d17afa1b..e76fd15612 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1087,6 +1087,12 @@ end --- - focusable (string or table) override `focusable` --- - zindex (string or table) override `zindex`, defaults to 50 --- - relative ("mouse"|"cursor") defaults to "cursor" +--- - anchor_bias ("auto"|"above"|"below") defaults to "auto" +--- - "auto": place window based on which side of the cursor has more lines +--- - "above": place the window above the cursor unless there are not enough lines +--- to display the full window height. +--- - "below": place the window below the cursor unless there are not enough lines +--- to display the full window height. ---@return table Options function M.make_floating_popup_options(width, height, opts) validate({ @@ -1105,7 +1111,20 @@ function M.make_floating_popup_options(width, height, opts) or vim.fn.winline() - 1 local lines_below = vim.fn.winheight(0) - lines_above - if lines_above < lines_below then + local anchor_bias = opts.anchor_bias or 'auto' + + local anchor_below + + if anchor_bias == 'below' then + anchor_below = (lines_below > lines_above) or (height <= lines_below) + elseif anchor_bias == 'above' then + local anchor_above = (lines_above > lines_below) or (height <= lines_above) + anchor_below = not anchor_above + else + anchor_below = lines_below > lines_above + end + + if anchor_below then anchor = anchor .. 'N' height = math.min(lines_below, height) row = 1 @@ -1635,7 +1654,8 @@ end --- ---@param contents table of lines to show in window ---@param syntax string of syntax to set for opened buffer ----@param opts table with optional fields (additional keys are passed on to |nvim_open_win()|) +---@param opts table with optional fields (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()| +--- before they are passed on to |nvim_open_win()|) --- - height: (integer) height of floating window --- - width: (integer) width of floating window --- - wrap: (boolean, default true) wrap long lines diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua index c91fffa90f..f544255d81 100644 --- a/test/functional/plugin/lsp/utils_spec.lua +++ b/test/functional/plugin/lsp/utils_spec.lua @@ -1,4 +1,6 @@ local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local feed = helpers.feed local eq = helpers.eq local exec_lua = helpers.exec_lua @@ -85,4 +87,98 @@ describe('vim.lsp.util', function() eq(expected, stylize_markdown(lines, opts)) end) end) + + describe("make_floating_popup_options", function () + + local function assert_anchor(anchor_bias, expected_anchor) + local opts = exec_lua([[ + local args = { ... } + local anchor_bias = args[1] + return vim.lsp.util.make_floating_popup_options(30, 10, { anchor_bias = anchor_bias }) + ]], anchor_bias) + + eq(expected_anchor, string.sub(opts.anchor, 1, 1)) + end + + local screen + before_each(function () + helpers.clear() + screen = Screen.new(80, 80) + screen:attach() + feed("79i") -- fill screen with empty lines + end) + + describe('when on the first line it places window below', function () + before_each(function () + feed('gg') + end) + + it('for anchor_bias = "auto"', function () + assert_anchor('auto', 'N') + end) + + it('for anchor_bias = "above"', function () + assert_anchor('above', 'N') + end) + + it('for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + end) + + describe('when on the last line it places window above', function () + before_each(function () + feed('G') + end) + + it('for anchor_bias = "auto"', function () + assert_anchor('auto', 'S') + end) + + it('for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('for anchor_bias = "below"', function () + assert_anchor('below', 'S') + end) + end) + + describe('with 20 lines above, 59 lines below', function () + before_each(function () + feed('gg20j') + end) + + it('places window below for anchor_bias = "auto"', function () + assert_anchor('auto', 'N') + end) + + it('places window above for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + end) + + describe('with 59 lines above, 20 lines below', function () + before_each(function () + feed('G20k') + end) + + it('places window above for anchor_bias = "auto"', function () + assert_anchor('auto', 'S') + end) + + it('places window above for anchor_bias = "above"', function () + assert_anchor('above', 'S') + end) + + it('places window below for anchor_bias = "below"', function () + assert_anchor('below', 'N') + end) + end) + end) + end)