diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index b727cff2cd..d70c24bb54 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -856,13 +856,6 @@ get_log_path() *vim.lsp.get_log_path()* Return: ~ (string) path to log file -inlay_hint({bufnr}, {enable}) *vim.lsp.inlay_hint()* - Enable/disable/toggle inlay hints for a buffer - - Parameters: ~ - • {bufnr} (integer) Buffer handle, or 0 for current - • {enable} (boolean|nil) true/false to enable/disable, nil to toggle - omnifunc({findstart}, {base}) *vim.lsp.omnifunc()* Implements 'omnifunc' compatible LSP completion. @@ -1469,6 +1462,53 @@ save({lenses}, {bufnr}, {client_id}) *vim.lsp.codelens.save()* • {client_id} (integer) +============================================================================== +Lua module: vim.lsp.inlay_hint *lsp-inlay_hint* + +enable({bufnr}, {enable}) *vim.lsp.inlay_hint.enable()* + Enable/disable/toggle inlay hints for a buffer + + Parameters: ~ + • {bufnr} (integer|nil) Buffer handle, or 0 or nil for current + • {enable} (boolean|nil) true/nil to enable, false to disable + +get({filter}) *vim.lsp.inlay_hint.get()* + Get the list of inlay hints, (optionally) restricted by buffer, client, or + range. + + Example usage: >lua + local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer + + local client = vim.lsp.get_client_by_id(hint.client_id) + resolved_hint = client.request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0).result + vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding) + + location = resolved_hint.label[1].location + client.request("textDocument/hover", { + textDocument = { uri = location.uri }, + position = location.range.start, + }) +< + + Parameters: ~ + • {filter} vim.lsp.inlay_hint.get.filter ? Optional filters |kwargs|: + • bufnr (integer?): 0 for current buffer + • range (lsp.Range?) + + Return: ~ + vim.lsp.inlay_hint.get.ret [] Each list item is a table with the following fields: + • bufnr (integer) + • client_id (integer) + • inlay_hint (lsp.InlayHint) + +is_enabled({bufnr}) *vim.lsp.inlay_hint.is_enabled()* + Parameters: ~ + • {bufnr} (integer|nil) Buffer handle, or 0 or nil for current + + Return: ~ + (boolean) + + ============================================================================== Lua module: vim.lsp.semantic_tokens *lsp-semantic_tokens* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index ac84f59308..9d531b8efc 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -130,7 +130,7 @@ The following new APIs and features were added. • LSP • LSP method names are available in |vim.lsp.protocol.Methods|. - • Implemented LSP inlay hints: |vim.lsp.inlay_hint()| + • Implemented LSP inlay hints: |lsp-inlay_hint| https://microsoft.github.io/language-server-protocol/specification/#textDocument_inlayHint • Implemented pull diagnostic textDocument/diagnostic: |vim.lsp.diagnostic.on_diagnostic()| https://microsoft.github.io/language-server-protocol/specification/#textDocument_diagnostic diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 82a88772bd..261a3aa5de 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -24,6 +24,7 @@ local lsp = { buf = require('vim.lsp.buf'), diagnostic = require('vim.lsp.diagnostic'), codelens = require('vim.lsp.codelens'), + inlay_hint = require('vim.lsp.inlay_hint'), semantic_tokens = semantic_tokens, util = util, @@ -2439,13 +2440,6 @@ function lsp.with(handler, override_config) end end ---- Enable/disable/toggle inlay hints for a buffer ----@param bufnr (integer) Buffer handle, or 0 for current ----@param enable (boolean|nil) true/false to enable/disable, nil to toggle -function lsp.inlay_hint(bufnr, enable) - return require('vim.lsp.inlay_hint')(bufnr, enable) -end - --- Helper function to use when implementing a handler. --- This will check that all of the keys in the user configuration --- are valid keys and make sense to include for this handler. diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index 7b58188c53..cdda5dcc17 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -98,6 +98,107 @@ function M.on_refresh(err, _, ctx, _) return vim.NIL end +--- @class vim.lsp.inlay_hint.get.filter +--- @field bufnr integer? +--- @field range lsp.Range? +--- +--- @class vim.lsp.inlay_hint.get.ret +--- @field bufnr integer +--- @field client_id integer +--- @field inlay_hint lsp.InlayHint + +--- Get the list of inlay hints, (optionally) restricted by buffer, client, or range. +--- +--- Example usage: +--- +--- ```lua +--- local hint = vim.lsp.inlay_hint.get({ bufnr = 0 })[1] -- 0 for current buffer +--- +--- local client = vim.lsp.get_client_by_id(hint.client_id) +--- resolved_hint = client.request_sync('inlayHint/resolve', hint.inlay_hint, 100, 0).result +--- vim.lsp.util.apply_text_edits(resolved_hint.textEdits, 0, client.encoding) +--- +--- location = resolved_hint.label[1].location +--- client.request('textDocument/hover', { +--- textDocument = { uri = location.uri }, +--- position = location.range.start, +--- }) +--- ``` +--- +--- @param filter vim.lsp.inlay_hint.get.filter? +--- Optional filters |kwargs|: +--- - bufnr (integer?): 0 for current buffer +--- - range (lsp.Range?) +--- +--- @return vim.lsp.inlay_hint.get.ret[] +--- Each list item is a table with the following fields: +--- - bufnr (integer) +--- - client_id (integer) +--- - inlay_hint (lsp.InlayHint) +function M.get(filter) + vim.validate({ filter = { filter, 'table', true } }) + filter = filter or {} + + local bufnr = filter.bufnr + if not bufnr then + --- @type vim.lsp.inlay_hint.get.ret[] + local hints = {} + --- @param buf integer + vim.tbl_map(function(buf) + vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter))) + end, vim.api.nvim_list_bufs()) + return hints + elseif bufnr == 0 then + bufnr = api.nvim_get_current_buf() + end + + local bufstate = bufstates[bufnr] + if not (bufstate and bufstate.client_hint) then + return {} + end + + local clients = vim.lsp.get_clients({ + bufnr = bufnr, + method = ms.textDocument_inlayHint, + }) + if #clients == 0 then + return {} + end + + local range = filter.range + if not range then + range = { + start = { line = 0, character = 0 }, + ['end'] = { line = api.nvim_buf_line_count(bufnr), character = 0 }, + } + end + + --- @type vim.lsp.inlay_hint.get.ret[] + local hints = {} + for _, client in pairs(clients) do + local hints_by_lnum = bufstate.client_hint[client.id] + if hints_by_lnum then + for lnum = range.start.line, range['end'].line do + local line_hints = hints_by_lnum[lnum] or {} + for _, hint in pairs(line_hints) do + local line, char = hint.position.line, hint.position.character + if + (line > range.start.line or char >= range.start.character) + and (line < range['end'].line or char <= range['end'].character) + then + table.insert(hints, { + bufnr = bufnr, + client_id = client.id, + inlay_hint = hint, + }) + end + end + end + end + end + return hints +end + --- Clear inlay hints ---@param bufnr (integer) Buffer handle, or 0 for current local function clear(bufnr) @@ -120,8 +221,8 @@ local function clear(bufnr) end --- Disable inlay hints for a buffer ----@param bufnr (integer) Buffer handle, or 0 for current -local function disable(bufnr) +---@param bufnr (integer|nil) Buffer handle, or 0 or nil for current +local function _disable(bufnr) if bufnr == nil or bufnr == 0 then bufnr = api.nvim_get_current_buf() end @@ -142,8 +243,8 @@ local function _refresh(bufnr, opts) end --- Enable inlay hints for a buffer ----@param bufnr (integer) Buffer handle, or 0 for current -local function enable(bufnr) +---@param bufnr (integer|nil) Buffer handle, or 0 or nil for current +local function _enable(bufnr) if bufnr == nil or bufnr == 0 then bufnr = api.nvim_get_current_buf() end @@ -175,7 +276,7 @@ local function enable(bufnr) end end, on_detach = function(_, cb_bufnr) - disable(cb_bufnr) + _disable(cb_bufnr) end, }) api.nvim_create_autocmd('LspDetach', { @@ -188,7 +289,7 @@ local function enable(bufnr) return c.id ~= args.data.client_id end) then - disable(bufnr) + _disable(bufnr) end end, group = augroup, @@ -199,20 +300,6 @@ local function enable(bufnr) end end ---- Toggle inlay hints for a buffer ----@param bufnr (integer) Buffer handle, or 0 for current -local function toggle(bufnr) - if bufnr == nil or bufnr == 0 then - bufnr = api.nvim_get_current_buf() - end - local bufstate = bufstates[bufnr] - if bufstate and bufstate.enabled then - disable(bufnr) - else - enable(bufnr) - end -end - api.nvim_set_decoration_provider(namespace, { on_win = function(_, _, bufnr, topline, botline) local bufstate = bufstates[bufnr] @@ -260,15 +347,27 @@ api.nvim_set_decoration_provider(namespace, { end, }) -return setmetatable(M, { - __call = function(_, bufnr, enable_) - vim.validate({ enable = { enable_, { 'boolean', 'nil' } }, bufnr = { bufnr, 'number' } }) - if enable_ then - enable(bufnr) - elseif enable_ == false then - disable(bufnr) - else - toggle(bufnr) - end - end, -}) +--- @param bufnr (integer|nil) Buffer handle, or 0 or nil for current +--- @return boolean +function M.is_enabled(bufnr) + vim.validate({ bufnr = { bufnr, 'number', true } }) + if bufnr == nil or bufnr == 0 then + bufnr = api.nvim_get_current_buf() + end + return bufstates[bufnr] and bufstates[bufnr].enabled or false +end + +--- Enable/disable/toggle inlay hints for a buffer +--- +--- @param bufnr (integer|nil) Buffer handle, or 0 or nil for current +--- @param enable (boolean|nil) true/nil to enable, false to disable +function M.enable(bufnr, enable) + vim.validate({ enable = { enable, 'boolean', true }, bufnr = { bufnr, 'number', true } }) + if enable == false then + _disable(bufnr) + else + _enable(bufnr) + end +end + +return M diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 7a48c800c6..e2396e9a5f 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -648,7 +648,12 @@ function protocol.make_client_capabilities() inlayHint = { dynamicRegistration = true, resolveSupport = { - properties = {}, + properties = { + 'textEdits', + 'tooltip', + 'location', + 'command', + }, }, }, semanticTokens = { diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py index 1f10a39e35..c738242c5d 100755 --- a/scripts/gen_vimdoc.py +++ b/scripts/gen_vimdoc.py @@ -259,6 +259,7 @@ CONFIG = { 'buf.lua', 'diagnostic.lua', 'codelens.lua', + 'inlay_hint.lua', 'tagfunc.lua', 'semantic_tokens.lua', 'handlers.lua', diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 6f5397a089..761e7dc522 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -18,8 +18,7 @@ local sleep = global_helpers.sleep local tbl_contains = global_helpers.tbl_contains local fail = global_helpers.fail -local module = { -} +local module = {} local start_dir = luv.cwd() local runtime_set = 'set runtimepath^=./build/lib/nvim/' @@ -834,6 +833,8 @@ function module.exec_capture(code) return module.meths.exec2(code, { output = true }).output end +--- @param code string +--- @return any function module.exec_lua(code, ...) return module.meths.exec_lua(code, {...}) end @@ -948,8 +949,10 @@ function module.mkdir_p(path) or 'mkdir -p '..path)) end +--- @class test.functional.helpers: test.helpers module = global_helpers.tbl_extend('error', module, global_helpers) +--- @return test.functional.helpers return function(after_each) if after_each then after_each(function() diff --git a/test/functional/plugin/lsp/inlay_hint_spec.lua b/test/functional/plugin/lsp/inlay_hint_spec.lua index eec86fdb8e..d0d55df72b 100644 --- a/test/functional/plugin/lsp/inlay_hint_spec.lua +++ b/test/functional/plugin/lsp/inlay_hint_spec.lua @@ -10,228 +10,195 @@ local insert = helpers.insert local clear_notrace = lsp_helpers.clear_notrace local create_server_definition = lsp_helpers.create_server_definition +local text = dedent([[ +auto add(int a, int b) { return a + b; } + +int main() { + int x = 1; + int y = 2; + return add(x,y); +} +}]]) + +local response = [==[ +[ +{"kind":1,"paddingLeft":false,"label":"-> int","position":{"character":22,"line":0},"paddingRight":false}, +{"kind":2,"paddingLeft":false,"label":"a:","position":{"character":15,"line":5},"paddingRight":true}, +{"kind":2,"paddingLeft":false,"label":"b:","position":{"character":17,"line":5},"paddingRight":true} +] +]==] + +local grid_without_inlay_hints = [[ + auto add(int a, int b) { return a + b; } | + | + int main() { | + int x = 1; | + int y = 2; | + return add(x,y); | + } | + ^} | + | +]] + +local grid_with_inlay_hints = [[ + auto add(int a, int b)-> int { return a + b; } | + | + int main() { | + int x = 1; | + int y = 2; | + return add(a: x,b: y); | + } | + ^} | + | +]] + +--- @type test.functional.ui.screen +local screen before_each(function() clear_notrace() + screen = Screen.new(50, 9) + screen:attach() + + exec_lua(create_server_definition) + exec_lua([[ + local response = ... + server = _create_server({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function() + return vim.json.decode(response) + end, + } + }) + + bufnr = vim.api.nvim_get_current_buf() + vim.api.nvim_win_set_buf(0, bufnr) + + client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) + ]], response) + + insert(text) + exec_lua([[vim.lsp.inlay_hint.enable(bufnr)]]) + screen:expect({ grid = grid_with_inlay_hints }) end) after_each(function() exec_lua("vim.api.nvim_exec_autocmds('VimLeavePre', { modeline = false })") end) -describe('inlay hints', function() - local screen - before_each(function() - screen = Screen.new(50, 9) - screen:attach() +describe('vim.lsp.inlay_hint', function() + it('clears inlay hints when sole client detaches', function() + exec_lua([[vim.lsp.stop_client(client_id)]]) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) end) - describe('general', function() - local text = dedent([[ - auto add(int a, int b) { return a + b; } + it('does not clear inlay hints when one of several clients detaches', function() + exec_lua([[ + server2 = _create_server({ + capabilities = { + inlayHintProvider = true, + }, + handlers = { + ['textDocument/inlayHint'] = function() + return {} + end, + } + }) + client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + vim.lsp.inlay_hint.enable(bufnr) + ]]) - int main() { - int x = 1; - int y = 2; - return add(x,y); - } - }]]) + exec_lua([[ vim.lsp.stop_client(client2) ]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + end) + describe('enable()', function() + it('clears/applies inlay hints when passed false/true/nil', function() + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, false)]]) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) - local response = [==[ - [ - {"kind":1,"paddingLeft":false,"label":"-> int","position":{"character":22,"line":0},"paddingRight":false}, - {"kind":2,"paddingLeft":false,"label":"a:","position":{"character":15,"line":5},"paddingRight":true}, - {"kind":2,"paddingLeft":false,"label":"b:","position":{"character":17,"line":5},"paddingRight":true} - ] - ]==] + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, true)]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + exec_lua([[vim.lsp.inlay_hint.enable(bufnr, not vim.lsp.inlay_hint.is_enabled(bufnr))]]) + screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) + + exec_lua([[vim.lsp.inlay_hint.enable(bufnr)]]) + screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) + end) + end) + + describe('get()', function() + it('returns filtered inlay hints', function() + --- @type lsp.InlayHint[] + local expected = vim.json.decode(response) + local expected2 = { + kind = 1, + paddingLeft = false, + label = ': int', + position = { + character = 10, + line = 2, + }, + paddingRight = false, + } - before_each(function() - exec_lua(create_server_definition) exec_lua([[ - local response = ... - server = _create_server({ + local expected2 = ... + server2 = _create_server({ capabilities = { inlayHintProvider = true, }, handlers = { ['textDocument/inlayHint'] = function() - return vim.json.decode(response) + return { expected2 } end, } }) - ]], response) + client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) + vim.lsp.inlay_hint.enable(bufnr) + ]], expected2) + + --- @type vim.lsp.inlay_hint.get.ret + local res = exec_lua([[return vim.lsp.inlay_hint.get()]]) + eq(res, { + { bufnr = 1, client_id = 1, inlay_hint = expected[1] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, + { bufnr = 1, client_id = 2, inlay_hint = expected2 }, + }) + + --- @type vim.lsp.inlay_hint.get.ret + res = exec_lua([[return vim.lsp.inlay_hint.get({ + range = { + start = { line = 2, character = 10 }, + ["end"] = { line = 2, character = 10 }, + }, + })]]) + eq(res, { + { bufnr = 1, client_id = 2, inlay_hint = expected2 }, + }) + + --- @type vim.lsp.inlay_hint.get.ret + res = exec_lua([[return vim.lsp.inlay_hint.get({ + bufnr = vim.api.nvim_get_current_buf(), + range = { + start = { line = 4, character = 18 }, + ["end"] = { line = 5, character = 17 }, + }, + })]]) + eq(res, { + { bufnr = 1, client_id = 1, inlay_hint = expected[2] }, + { bufnr = 1, client_id = 1, inlay_hint = expected[3] }, + }) + + --- @type vim.lsp.inlay_hint.get.ret + res = exec_lua([[return vim.lsp.inlay_hint.get({ + bufnr = vim.api.nvim_get_current_buf() + 1, + })]]) + eq(res, {}) end) - - it( - 'inlay hints are applied when vim.lsp.inlay_hint(true) is called', - function() - local res = exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - local client = vim.lsp.get_client_by_id(client_id) - return { - supports_method = client.supports_method("textDocument/inlayHint") - } - ]]) - eq(res, { supports_method = true }) - - - insert(text) - exec_lua([[vim.lsp.inlay_hint(bufnr, true)]]) - screen:expect({ - grid = [[ - auto add(int a, int b)-> int { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(a: x,b: y); | - } | - ^} | - | -]] - }) - end) - - it( - 'inlay hints are cleared when vim.lsp.inlay_hint(false) is called', - function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - - insert(text) - exec_lua([[vim.lsp.inlay_hint(bufnr, true)]]) - screen:expect({ - grid = [[ - auto add(int a, int b)-> int { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(a: x,b: y); | - } | - ^} | - | -]] - }) - exec_lua([[vim.lsp.inlay_hint(bufnr, false)]]) - screen:expect({ - grid = [[ - auto add(int a, int b) { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(x,y); | - } | - ^} | - | -]], - unchanged = true - }) - end) - - it( - 'inlay hints are cleared when the client detaches', - function() - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - ]]) - - insert(text) - exec_lua([[vim.lsp.inlay_hint(bufnr, true)]]) - screen:expect({ - grid = [[ - auto add(int a, int b)-> int { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(a: x,b: y); | - } | - ^} | - | -]] - }) - exec_lua([[vim.lsp.stop_client(client_id)]]) - screen:expect({ - grid = [[ - auto add(int a, int b) { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(x,y); | - } | - ^} | - | -]], - unchanged = true - }) - end) - - it( - 'inlay hints are not cleared when one of several clients detaches', - function() - -- Start two clients - exec_lua([[ - bufnr = vim.api.nvim_get_current_buf() - vim.api.nvim_win_set_buf(0, bufnr) - server2 = _create_server({ - capabilities = { - inlayHintProvider = true, - }, - handlers = { - ['textDocument/inlayHint'] = function() - return {} - end, - } - }) - client1 = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) - client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd }) - ]]) - - insert(text) - exec_lua([[vim.lsp.inlay_hint(bufnr, true)]]) - screen:expect({ - grid = [[ - auto add(int a, int b)-> int { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(a: x,b: y); | - } | - ^} | - | -]] - }) - - -- Now stop one client - exec_lua([[ vim.lsp.stop_client(client2) ]]) - - -- We should still see the hints - screen:expect({ - grid = [[ - auto add(int a, int b)-> int { return a + b; } | - | - int main() { | - int x = 1; | - int y = 2; | - return add(a: x,b: y); | - } | - ^} | - | -]], - unchanged = true - }) - end) end) end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index d56e5b4afa..bb8d775838 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1291,7 +1291,7 @@ describe('LSP', function() on_handler = function(err, result, ctx) if ctx.method == 'start' then exec_lua [[ - vim.lsp.inlay_hint(BUFFER, true) + vim.lsp.inlay_hint.enable(BUFFER) ]] end if ctx.method == 'textDocument/inlayHint' then diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index d3ffb07749..810a68d387 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -88,6 +88,7 @@ local function isempty(v) return type(v) == 'table' and next(v) == nil end +--- @class test.functional.ui.screen local Screen = {} Screen.__index = Screen