diff options
author | lvimuser <109605931+lvimuser@users.noreply.github.com> | 2022-10-08 05:22:25 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-08 10:22:25 +0200 |
commit | 0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1 (patch) | |
tree | bd41979a0da6a0b0f5043620633d514111510557 | |
parent | cfdb4cbada8c65aa57e69776bcc0f7b8b298317a (diff) | |
download | rneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.tar.gz rneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.tar.bz2 rneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.zip |
feat(lsp): support window/showDocument (#19977)
-rw-r--r-- | runtime/doc/lsp.txt | 23 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 46 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 93 | ||||
-rw-r--r-- | test/functional/plugin/lsp_spec.lua | 162 |
5 files changed, 294 insertions, 32 deletions
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index ae969491f7..139f4c6bc5 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -190,6 +190,7 @@ specification. These LSP requests/notifications are defined by default: textDocument/typeDefinition* window/logMessage window/showMessage + window/showDocument window/showMessageRequest workspace/applyEdit workspace/symbol @@ -1499,12 +1500,12 @@ jump_to_location({location}, {offset_encoding}, {reuse_win}) Parameters: ~ • {location} (table) (`Location`|`LocationLink`) - • {offset_encoding} (string) utf-8|utf-16|utf-32 (required) + • {offset_encoding} "utf-8" | "utf-16" | "utf-32" • {reuse_win} (boolean) Jump to existing window if buffer is - already opened. + already open. Return: ~ - `true` if the jump succeeded + (boolean) `true` if the jump succeeded *vim.lsp.util.locations_to_items()* locations_to_items({locations}, {offset_encoding}) @@ -1715,6 +1716,22 @@ set_lines({lines}, {A}, {B}, {new_lines}) *vim.lsp.util.set_lines()* Return: ~ (table) The modified {lines} object + *vim.lsp.util.show_document()* +show_document({location}, {offset_encoding}, {opts}) + Shows document and optionally jumps to the location. + + Parameters: ~ + • {location} (table) (`Location`|`LocationLink`) + • {offset_encoding} "utf-8" | "utf-16" | "utf-32" + • {opts} (table) options + • reuse_win (boolean) Jump to existing window if + buffer is already open. + • focus (boolean) Whether to focus/jump to location + if possible. Defaults to true. + + Return: ~ + (boolean) `true` if succeeded + *vim.lsp.util.stylize_markdown()* stylize_markdown({bufnr}, {contents}, {opts}) Converts markdown into syntax highlighted regions by stripping the code diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index a32c59dd17..d7d9ca7ce9 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -512,6 +512,52 @@ M['window/showMessage'] = function(_, result, ctx, _) return result end +--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window_showDocument +M['window/showDocument'] = function(_, result, ctx, _) + local uri = result.uri + + if result.external then + -- TODO(lvimuser): ask the user for confirmation + local cmd + if vim.fn.has('win32') == 1 then + cmd = { 'cmd.exe', '/c', 'start', '""', vim.fn.shellescape(uri) } + elseif vim.fn.has('macunix') == 1 then + cmd = { 'open', vim.fn.shellescape(uri) } + else + cmd = { 'xdg-open', vim.fn.shellescape(uri) } + end + + local ret = vim.fn.system(cmd) + if vim.v.shellerror ~= 0 then + return { + success = false, + error = { + code = protocol.ErrorCodes.UnknownErrorCode, + message = ret, + }, + } + end + + return { success = true } + end + + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format('id=%d', client_id) + if not client then + err_message({ 'LSP[', client_name, '] client has shut down after sending ', ctx.method }) + return vim.NIL + end + + local location = { + uri = uri, + range = result.selection, + } + + local success = util.show_document(location, client.offset_encoding, true, result.takeFocus) + return { success = success or false } +end + -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do M[k] = function(err, result, ctx, config) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 27da60b4ae..4034753322 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -778,7 +778,7 @@ function protocol.make_client_capabilities() }, }, showDocument = { - support = false, + support = true, }, }, } diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 88667caf68..617d33f88c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -110,6 +110,15 @@ local function split_lines(value) return split(value, '\n', true) end +---@private +local function create_window_without_focus() + local prev = vim.api.nvim_get_current_win() + vim.cmd.new() + local new = vim.api.nvim_get_current_win() + vim.api.nvim_set_current_win(prev) + return new +end + --- Convert byte index to `encoding` index. --- Convenience wrapper around vim.str_utfindex ---@param line string line to be indexed @@ -1056,50 +1065,80 @@ function M.make_floating_popup_options(width, height, opts) } end ---- Jumps to a location. +--- Shows document and optionally jumps to the location. --- ---@param location table (`Location`|`LocationLink`) ----@param offset_encoding string utf-8|utf-16|utf-32 (required) ----@param reuse_win boolean Jump to existing window if buffer is already opened. ----@returns `true` if the jump succeeded -function M.jump_to_location(location, offset_encoding, reuse_win) +---@param offset_encoding "utf-8" | "utf-16" | "utf-32" +---@param opts table options +--- - reuse_win (boolean) Jump to existing window if buffer is already open. +--- - focus (boolean) Whether to focus/jump to location if possible. Defaults to true. +---@return boolean `true` if succeeded +function M.show_document(location, offset_encoding, opts) -- location may be Location or LocationLink local uri = location.uri or location.targetUri if uri == nil then - return + return false end if offset_encoding == nil then - vim.notify_once( - 'jump_to_location must be called with valid offset encoding', - vim.log.levels.WARN - ) + vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN) end local bufnr = vim.uri_to_bufnr(uri) - -- Save position in jumplist - vim.cmd("normal! m'") - -- Push a new item into tagstack - local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 } - local items = { { tagname = vim.fn.expand('<cword>'), from = from } } - vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't') + opts = opts or {} + local focus = vim.F.if_nil(opts.focus, true) + if focus then + -- Save position in jumplist + vim.cmd("normal! m'") - --- Jump to new location (adjusting for UTF-16 encoding of characters) - local win = reuse_win and bufwinid(bufnr) - if win then + -- Push a new item into tagstack + local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 } + local items = { { tagname = vim.fn.expand('<cword>'), from = from } } + vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't') + end + + local win = opts.reuse_win and bufwinid(bufnr) + or focus and api.nvim_get_current_win() + or create_window_without_focus() + + api.nvim_buf_set_option(bufnr, 'buflisted', true) + api.nvim_win_set_buf(win, bufnr) + if focus then api.nvim_set_current_win(win) - else - api.nvim_buf_set_option(bufnr, 'buflisted', true) - api.nvim_set_current_buf(bufnr) end + + -- location may be Location or LocationLink local range = location.range or location.targetSelectionRange - local row = range.start.line - local col = get_line_byte_from_position(bufnr, range.start, offset_encoding) - api.nvim_win_set_cursor(0, { row + 1, col }) - -- Open folds under the cursor - vim.cmd('normal! zv') + if range then + --- Jump to new location (adjusting for encoding of characters) + local row = range.start.line + local col = get_line_byte_from_position(bufnr, range.start, offset_encoding) + api.nvim_win_set_cursor(win, { row + 1, col }) + api.nvim_win_call(win, function() + -- Open folds under the cursor + vim.cmd('normal! zv') + end) + end + return true end +--- Jumps to a location. +--- +---@param location table (`Location`|`LocationLink`) +---@param offset_encoding "utf-8" | "utf-16" | "utf-32" +---@param reuse_win boolean Jump to existing window if buffer is already open. +---@return boolean `true` if the jump succeeded +function M.jump_to_location(location, offset_encoding, reuse_win) + if offset_encoding == nil then + vim.notify_once( + 'jump_to_location must be called with valid offset encoding', + vim.log.levels.WARN + ) + end + + return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true }) +end + --- Previews a location in a floating window --- --- behavior depends on type of location: diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index bcae9b4084..425427be54 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -2586,7 +2586,7 @@ describe('LSP', function() local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") eq({ 1, 0 }, mark) - funcs.nvim_win_set_cursor(0, {2, 3}) + funcs.nvim_win_set_cursor(0, { 2, 3 }) jump(location(0, 9, 0, 9)) mark = funcs.nvim_buf_get_mark(target_bufnr, "'") @@ -2594,6 +2594,166 @@ describe('LSP', function() end) end) + describe('lsp.util.show_document', function() + local target_bufnr + local target_bufnr2 + + before_each(function() + target_bufnr = exec_lua([[ + local bufnr = vim.uri_to_bufnr("file:///fake/uri") + local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"} + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + ]]) + + target_bufnr2 = exec_lua([[ + local bufnr = vim.uri_to_bufnr("file:///fake/uri2") + local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"} + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + ]]) + end) + + local location = function(start_line, start_char, end_line, end_char, second_uri) + return { + uri = second_uri and 'file:///fake/uri2' or 'file:///fake/uri', + range = { + start = { line = start_line, character = start_char }, + ['end'] = { line = end_line, character = end_char }, + }, + } + end + + local show_document = function(msg, focus, reuse_win) + eq( + true, + exec_lua( + 'return vim.lsp.util.show_document(...)', + msg, + 'utf-16', + { reuse_win = reuse_win, focus = focus } + ) + ) + if focus == true or focus == nil then + eq(target_bufnr, exec_lua([[return vim.fn.bufnr('%')]])) + end + return { + line = exec_lua([[return vim.fn.line('.')]]), + col = exec_lua([[return vim.fn.col('.')]]), + } + end + + it('jumps to a Location if focus is true', function() + local pos = show_document(location(0, 9, 0, 9), true, true) + eq(1, pos.line) + eq(10, pos.col) + end) + + it('jumps to a Location if focus not set', function() + local pos = show_document(location(0, 9, 0, 9), nil, true) + eq(1, pos.line) + eq(10, pos.col) + end) + + it('does not add current position to jumplist if not focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + + funcs.nvim_win_set_cursor(0, { 2, 3 }) + show_document(location(0, 9, 0, 9), false, true) + show_document(location(0, 9, 0, 9, true), false, true) + + mark = funcs.nvim_buf_get_mark(target_bufnr, "'") + eq({ 1, 0 }, mark) + end) + + it('does not change cursor position if not focus and not reuse_win', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local cursor = funcs.nvim_win_get_cursor(0) + + show_document(location(0, 9, 0, 9), false, false) + eq(cursor, funcs.nvim_win_get_cursor(0)) + end) + + it('does not change window if not focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local win = funcs.nvim_get_current_win() + + -- same document/bufnr + show_document(location(0, 9, 0, 9), false, true) + eq(win, funcs.nvim_get_current_win()) + + -- different document/bufnr, new window/split + show_document(location(0, 9, 0, 9, true), false, true) + eq(2, #funcs.nvim_list_wins()) + eq(win, funcs.nvim_get_current_win()) + end) + + it("respects 'reuse_win' parameter", function() + funcs.nvim_win_set_buf(0, target_bufnr) + + -- does not create a new window if the buffer is already open + show_document(location(0, 9, 0, 9), false, true) + eq(1, #funcs.nvim_list_wins()) + + -- creates a new window even if the buffer is already open + show_document(location(0, 9, 0, 9), false, false) + eq(2, #funcs.nvim_list_wins()) + end) + + it('correctly sets the cursor of the split if range is given without focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + + show_document(location(0, 9, 0, 9, true), false, true) + + local wins = funcs.nvim_list_wins() + eq(2, #wins) + table.sort(wins) + + eq({ 1, 0 }, funcs.nvim_win_get_cursor(wins[1])) + eq({ 1, 9 }, funcs.nvim_win_get_cursor(wins[2])) + end) + + it('does not change cursor of the split if not range and not focus', function() + funcs.nvim_win_set_buf(0, target_bufnr) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + + exec_lua([[vim.cmd.new()]]) + funcs.nvim_win_set_buf(0, target_bufnr2) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + + show_document({ uri = 'file:///fake/uri2' }, false, true) + + local wins = funcs.nvim_list_wins() + eq(2, #wins) + eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[1])) + eq({ 2, 3 }, funcs.nvim_win_get_cursor(wins[2])) + end) + + it('respects existing buffers', function() + funcs.nvim_win_set_buf(0, target_bufnr) + local win = funcs.nvim_get_current_win() + + exec_lua([[vim.cmd.new()]]) + funcs.nvim_win_set_buf(0, target_bufnr2) + funcs.nvim_win_set_cursor(0, { 2, 3 }) + local split = funcs.nvim_get_current_win() + + -- reuse win for open document/bufnr if called from split + show_document(location(0, 9, 0, 9, true), false, true) + eq({ 1, 9 }, funcs.nvim_win_get_cursor(split)) + eq(2, #funcs.nvim_list_wins()) + + funcs.nvim_set_current_win(win) + + -- reuse win for open document/bufnr if called outside the split + show_document(location(0, 9, 0, 9, true), false, true) + eq({ 1, 9 }, funcs.nvim_win_get_cursor(split)) + eq(2, #funcs.nvim_list_wins()) + end) + end) + describe('lsp.util._make_floating_popup_size', function() before_each(function() exec_lua [[ contents = |