mirror of
https://github.com/neovim/neovim.git
synced 2025-09-07 03:48:18 +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