mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	feat(lsp): support window/showDocument (#19977)
This commit is contained in:
		| @@ -190,6 +190,7 @@ specification. These LSP requests/notifications are defined by default: | |||||||
|     textDocument/typeDefinition* |     textDocument/typeDefinition* | ||||||
|     window/logMessage |     window/logMessage | ||||||
|     window/showMessage |     window/showMessage | ||||||
|  |     window/showDocument | ||||||
|     window/showMessageRequest |     window/showMessageRequest | ||||||
|     workspace/applyEdit |     workspace/applyEdit | ||||||
|     workspace/symbol |     workspace/symbol | ||||||
| @@ -1499,12 +1500,12 @@ jump_to_location({location}, {offset_encoding}, {reuse_win}) | |||||||
|  |  | ||||||
|     Parameters: ~ |     Parameters: ~ | ||||||
|       • {location}         (table) (`Location`|`LocationLink`) |       • {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 |       • {reuse_win}        (boolean) Jump to existing window if buffer is | ||||||
|                            already opened. |                            already open. | ||||||
|  |  | ||||||
|     Return: ~ |     Return: ~ | ||||||
|         `true` if the jump succeeded |         (boolean) `true` if the jump succeeded | ||||||
|  |  | ||||||
|                                            *vim.lsp.util.locations_to_items()* |                                            *vim.lsp.util.locations_to_items()* | ||||||
| locations_to_items({locations}, {offset_encoding}) | locations_to_items({locations}, {offset_encoding}) | ||||||
| @@ -1715,6 +1716,22 @@ set_lines({lines}, {A}, {B}, {new_lines})           *vim.lsp.util.set_lines()* | |||||||
|     Return: ~ |     Return: ~ | ||||||
|         (table) The modified {lines} object |         (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()* |                                              *vim.lsp.util.stylize_markdown()* | ||||||
| stylize_markdown({bufnr}, {contents}, {opts}) | stylize_markdown({bufnr}, {contents}, {opts}) | ||||||
|     Converts markdown into syntax highlighted regions by stripping the code |     Converts markdown into syntax highlighted regions by stripping the code | ||||||
|   | |||||||
| @@ -512,6 +512,52 @@ M['window/showMessage'] = function(_, result, ctx, _) | |||||||
|   return result |   return result | ||||||
| end | 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. | -- Add boilerplate error validation and logging for all of these. | ||||||
| for k, fn in pairs(M) do | for k, fn in pairs(M) do | ||||||
|   M[k] = function(err, result, ctx, config) |   M[k] = function(err, result, ctx, config) | ||||||
|   | |||||||
| @@ -778,7 +778,7 @@ function protocol.make_client_capabilities() | |||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       showDocument = { |       showDocument = { | ||||||
|         support = false, |         support = true, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -110,6 +110,15 @@ local function split_lines(value) | |||||||
|   return split(value, '\n', true) |   return split(value, '\n', true) | ||||||
| end | 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. | --- Convert byte index to `encoding` index. | ||||||
| --- Convenience wrapper around vim.str_utfindex | --- Convenience wrapper around vim.str_utfindex | ||||||
| ---@param line string line to be indexed | ---@param line string line to be indexed | ||||||
| @@ -1056,48 +1065,78 @@ function M.make_floating_popup_options(width, height, opts) | |||||||
|   } |   } | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Jumps to a location. | --- Shows document and optionally jumps to the location. | ||||||
| --- | --- | ||||||
| ---@param location table (`Location`|`LocationLink`) | ---@param location table (`Location`|`LocationLink`) | ||||||
| ---@param offset_encoding string utf-8|utf-16|utf-32 (required) | ---@param offset_encoding "utf-8" | "utf-16" | "utf-32" | ||||||
| ---@param reuse_win boolean Jump to existing window if buffer is already opened. | ---@param opts table options | ||||||
| ---@returns `true` if the jump succeeded | ---        - reuse_win (boolean) Jump to existing window if buffer is already open. | ||||||
| function M.jump_to_location(location, offset_encoding, reuse_win) | ---        - 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 |   -- location may be Location or LocationLink | ||||||
|   local uri = location.uri or location.targetUri |   local uri = location.uri or location.targetUri | ||||||
|   if uri == nil then |   if uri == nil then | ||||||
|     return |     return false | ||||||
|   end |   end | ||||||
|  |   if offset_encoding == nil then | ||||||
|  |     vim.notify_once('show_document must be called with valid offset encoding', vim.log.levels.WARN) | ||||||
|  |   end | ||||||
|  |   local bufnr = vim.uri_to_bufnr(uri) | ||||||
|  |  | ||||||
|  |   opts = opts or {} | ||||||
|  |   local focus = vim.F.if_nil(opts.focus, true) | ||||||
|  |   if focus then | ||||||
|  |     -- 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') | ||||||
|  |   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) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   -- location may be Location or LocationLink | ||||||
|  |   local range = location.range or location.targetSelectionRange | ||||||
|  |   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 |   if offset_encoding == nil then | ||||||
|     vim.notify_once( |     vim.notify_once( | ||||||
|       'jump_to_location must be called with valid offset encoding', |       'jump_to_location must be called with valid offset encoding', | ||||||
|       vim.log.levels.WARN |       vim.log.levels.WARN | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|   local bufnr = vim.uri_to_bufnr(uri) |  | ||||||
|   -- Save position in jumplist |  | ||||||
|   vim.cmd("normal! m'") |  | ||||||
|  |  | ||||||
|   -- Push a new item into tagstack |   return M.show_document(location, offset_encoding, { reuse_win = reuse_win, focus = true }) | ||||||
|   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') |  | ||||||
|  |  | ||||||
|   --- Jump to new location (adjusting for UTF-16 encoding of characters) |  | ||||||
|   local win = reuse_win and bufwinid(bufnr) |  | ||||||
|   if win then |  | ||||||
|     api.nvim_set_current_win(win) |  | ||||||
|   else |  | ||||||
|     api.nvim_buf_set_option(bufnr, 'buflisted', true) |  | ||||||
|     api.nvim_set_current_buf(bufnr) |  | ||||||
|   end |  | ||||||
|   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') |  | ||||||
|   return true |  | ||||||
| end | end | ||||||
|  |  | ||||||
| --- Previews a location in a floating window | --- Previews a location in a floating window | ||||||
|   | |||||||
| @@ -2586,7 +2586,7 @@ describe('LSP', function() | |||||||
|       local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") |       local mark = funcs.nvim_buf_get_mark(target_bufnr, "'") | ||||||
|       eq({ 1, 0 }, mark) |       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)) |       jump(location(0, 9, 0, 9)) | ||||||
|  |  | ||||||
|       mark = funcs.nvim_buf_get_mark(target_bufnr, "'") |       mark = funcs.nvim_buf_get_mark(target_bufnr, "'") | ||||||
| @@ -2594,6 +2594,166 @@ describe('LSP', function() | |||||||
|     end) |     end) | ||||||
|   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() |   describe('lsp.util._make_floating_popup_size', function() | ||||||
|     before_each(function() |     before_each(function() | ||||||
|       exec_lua [[ contents = |       exec_lua [[ contents = | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 lvimuser
					lvimuser