diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 2a98c30e58..c12e01a44d 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -302,6 +302,7 @@ LSP • Support for `textDocument/semanticTokens/range`. • Support for `textDocument/codeLens` |lsp-codelens| has been reimplemented: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_codeLens +• Code lenses now display as virtual lines • Support for `workspace/codeLens/refresh`: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#codeLens_refresh diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index 87cbbe425b..ed66d023ae 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -210,9 +210,10 @@ function Provider:on_win(toprow, botrow) for row = toprow, botrow do if self.row_version[row] ~= self.version then for client_id, state in pairs(self.client_state) do + local bufnr = self.bufnr local namespace = state.namespace - api.nvim_buf_clear_namespace(self.bufnr, namespace, row, row + 1) + api.nvim_buf_clear_namespace(bufnr, namespace, row, row + 1) local lenses = state.row_lenses[row] if lenses then @@ -220,25 +221,37 @@ function Provider:on_win(toprow, botrow) return a.range.start.character < b.range.start.character end) - ---@type [string, string][] - local virt_text = {} + ---@type integer + local indent = api.nvim_buf_call(bufnr, function() + return vim.fn.indent(row + 1) + end) + + ---@type [string, string|integer][][] + local virt_lines = { { { string.rep(' ', indent), 'LspCodeLensSeparator' } } } + local virt_text = virt_lines[1] for _, lens in ipairs(lenses) do -- A code lens is unresolved when no command is associated to it. if not lens.command then - local client = assert(vim.lsp.get_client_by_id(client_id)) + local client = assert(vim.lsp.get_client_by_id(client_id)) ---@type vim.lsp.Client self:resolve(client, lens) else - vim.list_extend(virt_text, { - { lens.command.title, 'LspCodeLens' }, - { ' | ', 'LspCodeLensSeparator' }, - }) + virt_text[#virt_text + 1] = { lens.command.title, 'LspCodeLens' } + virt_text[#virt_text + 1] = { ' | ', 'LspCodeLensSeparator' } end end - -- Remove trailing separator. - table.remove(virt_text) - api.nvim_buf_set_extmark(self.bufnr, namespace, row, 0, { - virt_text = virt_text, + if #virt_text > 1 then + -- Remove trailing separator. + virt_text[#virt_text] = nil + else + -- Use a placeholder to prevent flickering caused by layout shifts. + virt_text[#virt_text + 1] = { '...', 'LspCodeLens' } + end + + api.nvim_buf_set_extmark(bufnr, namespace, row, 0, { + virt_lines = virt_lines, + virt_lines_above = true, + virt_lines_overflow = 'scroll', hl_mode = 'combine', }) end @@ -246,6 +259,12 @@ function Provider:on_win(toprow, botrow) end end end + + if botrow == api.nvim_buf_line_count(self.bufnr) - 1 then + for _, state in pairs(self.client_state) do + api.nvim_buf_clear_namespace(self.bufnr, state.namespace, botrow, -1) + end + end end local namespace = api.nvim_create_namespace('nvim.lsp.codelens') diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua index f9856ed90f..50c4f4d0bd 100644 --- a/test/functional/plugin/lsp/codelens_spec.lua +++ b/test/functional/plugin/lsp/codelens_spec.lua @@ -16,6 +16,7 @@ local create_server_definition = t_lsp.create_server_definition describe('vim.lsp.codelens', function() local text = dedent([[ + https://github.com/neovim/neovim/issues/16166 struct S { a: i32, b: String, @@ -34,7 +35,9 @@ describe('vim.lsp.codelens', function() ]]) local grid_with_lenses = dedent([[ - struct S { {1:1 implementation} | + ^https://github.com/neovim/neovim/issues/16166 | + {1:1 implementation} | + struct S { | a: i32, | b: String, | } | @@ -45,17 +48,18 @@ describe('vim.lsp.codelens', function() } | } | | - fn main() { {1:▶︎ Run } | + {1:▶︎ Run } | + fn main() { | let s = S::new(42, String::from("Hello, world!"))| ; | println!("S.a: {}, S.b: {}", s.a, s.b); | } | - ^ | - {1:~ }|*2 + | | ]]) local grid_without_lenses = dedent([[ + ^https://github.com/neovim/neovim/issues/16166 | struct S { | a: i32, | b: String, | @@ -72,7 +76,7 @@ describe('vim.lsp.codelens', function() ; | println!("S.a: {}, S.b: {}", s.a, s.b); | } | - ^ | + | {1:~ }|*2 | ]]) @@ -87,7 +91,7 @@ describe('vim.lsp.codelens', function() clear_notrace() exec_lua(create_server_definition) - screen = Screen.new(nil, 20) + screen = Screen.new(nil, 21) client_id = exec_lua(function() _G.server = _G._create_server({ @@ -105,7 +109,7 @@ describe('vim.lsp.codelens', function() impls = { position = { character = 7, - line = 0, + line = 1, }, }, }, @@ -114,11 +118,11 @@ describe('vim.lsp.codelens', function() range = { ['end'] = { character = 8, - line = 0, + line = 1, }, start = { character = 7, - line = 0, + line = 1, }, }, }, @@ -131,11 +135,11 @@ describe('vim.lsp.codelens', function() range = { ['end'] = { character = 7, - line = 11, + line = 12, }, start = { character = 3, - line = 11, + line = 12, }, }, }, @@ -152,11 +156,11 @@ describe('vim.lsp.codelens', function() range = { ['end'] = { character = 8, - line = 0, + line = 1, }, start = { character = 7, - line = 0, + line = 1, }, }, }) @@ -174,6 +178,7 @@ describe('vim.lsp.codelens', function() vim.lsp.codelens.enable() end) + feed('gg') screen:expect({ grid = grid_with_lenses }) end) @@ -211,11 +216,11 @@ describe('vim.lsp.codelens', function() range = { ['end'] = { character = 8, - line = 0, + line = 1, }, start = { character = 7, - line = 0, + line = 1, }, }, }, @@ -231,11 +236,11 @@ describe('vim.lsp.codelens', function() range = { ['end'] = { character = 7, - line = 11, + line = 12, }, start = { character = 3, - line = 11, + line = 12, }, }, }, @@ -244,10 +249,12 @@ describe('vim.lsp.codelens', function() end) it('refreshes code lenses on request', function() - feed('ggdd') + feed('2Gdd') screen:expect([[ - ^a: i32, {1:1 implementation} | + https://github.com/neovim/neovim/issues/16166 | + {1:1 implementation} | + ^a: i32, | b: String, | } | | @@ -257,13 +264,14 @@ describe('vim.lsp.codelens', function() } | } | | - fn main() { {1:▶︎ Run } | + {1:▶︎ Run } | + fn main() { | let s = S::new(42, String::from("Hello, world!"))| ; | println!("S.a: {}, S.b: {}", s.a, s.b); | } | | - {1:~ }|*3 + {1:~ }|*1 | ]]) exec_lua(function() @@ -274,7 +282,9 @@ describe('vim.lsp.codelens', function() ) end) screen:expect([[ - ^a: i32, {1:1 implementation} | + https://github.com/neovim/neovim/issues/16166 | + {1: 1 implementation} | + ^a: i32, | b: String, | } | | @@ -285,16 +295,39 @@ describe('vim.lsp.codelens', function() } | | fn main() { | + {1: ▶︎ Run } | let s = S::new(42, String::from("Hello, world!"))| - ; {1:▶︎ Run } | + ; | println!("S.a: {}, S.b: {}", s.a, s.b); | } | | - {1:~ }|*3 + {1:~ }|*1 | ]]) end) + it('clears extmarks beyond the bottom of the buffer', function() + feed('13G4dd') + screen:expect([[ + https://github.com/neovim/neovim/issues/16166 | + {1:1 implementation} | + struct S { | + a: i32, | + b: String, | + } | + | + impl S { | + fn new(a: i32, b: String) -> Self { | + S { a, b } | + } | + } | + | + ^ | + {1:~ }|*6 + 4 fewer lines | + ]]) + end) + after_each(function() api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) end)