aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorlvimuser <109605931+lvimuser@users.noreply.github.com>2022-10-08 05:22:25 -0300
committerGitHub <noreply@github.com>2022-10-08 10:22:25 +0200
commit0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1 (patch)
treebd41979a0da6a0b0f5043620633d514111510557
parentcfdb4cbada8c65aa57e69776bcc0f7b8b298317a (diff)
downloadrneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.tar.gz
rneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.tar.bz2
rneovim-0773a9ee3a21db54cd6b2376dd2e087bc09d5ea1.zip
feat(lsp): support window/showDocument (#19977)
-rw-r--r--runtime/doc/lsp.txt23
-rw-r--r--runtime/lua/vim/lsp/handlers.lua46
-rw-r--r--runtime/lua/vim/lsp/protocol.lua2
-rw-r--r--runtime/lua/vim/lsp/util.lua93
-rw-r--r--test/functional/plugin/lsp_spec.lua162
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 =