2019-11-13 13:55:26 -07:00
local protocol = require ( ' vim.lsp.protocol ' )
2023-10-01 09:54:04 -07:00
local snippet = require ( ' vim.lsp._snippet_grammar ' )
2019-11-13 13:55:26 -07:00
local validate = vim.validate
2022-07-15 09:26:47 -07:00
local api = vim.api
2019-11-26 06:59:40 -07:00
local list_extend = vim.list_extend
2024-01-22 10:23:28 -07:00
local highlight = vim.highlight
2023-06-03 03:06:00 -07:00
local uv = vim.uv
2019-11-13 13:55:26 -07:00
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
2020-11-12 20:21:34 -07:00
local npcall = vim.F . npcall
local split = vim.split
2019-11-13 13:55:26 -07:00
local M = { }
2021-04-08 10:32:05 -07:00
local default_border = {
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
{ ' ' , ' NormalFloat ' } ,
}
2021-08-22 13:55:28 -07:00
--- Check the border given by opts or the default border for the additional
--- size it adds to a float.
2023-07-01 03:42:37 -07:00
---@param opts table optional options for the floating window
2021-08-27 04:19:17 -07:00
--- - border (string or table) the border
2023-07-01 03:42:37 -07:00
---@return table size of border in the form of { height = height, width = width }
2021-04-08 10:32:05 -07:00
local function get_border_size ( opts )
local border = opts and opts.border or default_border
local height = 0
local width = 0
if type ( border ) == ' string ' then
2021-06-14 14:19:45 -07:00
local border_size = {
none = { 0 , 0 } ,
single = { 2 , 2 } ,
double = { 2 , 2 } ,
rounded = { 2 , 2 } ,
solid = { 2 , 2 } ,
shadow = { 1 , 1 } ,
}
2021-06-02 03:41:25 -07:00
if border_size [ border ] == nil then
2021-08-27 04:19:17 -07:00
error (
string.format (
' invalid floating preview border: %s. :help vim.api.nvim_open_win() ' ,
vim.inspect ( border )
2022-07-07 09:27:18 -07:00
)
)
2021-06-02 03:41:25 -07:00
end
height , width = unpack ( border_size [ border ] )
2021-04-08 10:32:05 -07:00
else
2021-08-27 04:19:17 -07:00
if 8 % # border ~= 0 then
error (
string.format (
' invalid floating preview border: %s. :help vim.api.nvim_open_win() ' ,
vim.inspect ( border )
2022-07-07 09:27:18 -07:00
)
)
2021-08-27 04:19:17 -07:00
end
2021-06-01 00:01:58 -07:00
local function border_width ( id )
2021-08-27 04:19:17 -07:00
id = ( id - 1 ) % # border + 1
2021-06-01 00:01:58 -07:00
if type ( border [ id ] ) == ' table ' then
-- border specified as a table of <character, highlight group>
return vim.fn . strdisplaywidth ( border [ id ] [ 1 ] )
elseif type ( border [ id ] ) == ' string ' then
-- border specified as a list of border characters
return vim.fn . strdisplaywidth ( border [ id ] )
end
2021-08-27 04:19:17 -07:00
error (
string.format (
' invalid floating preview border: %s. :help vim.api.nvim_open_win() ' ,
vim.inspect ( border )
2022-07-07 09:27:18 -07:00
)
)
2021-06-01 00:01:58 -07:00
end
local function border_height ( id )
2021-08-27 04:19:17 -07:00
id = ( id - 1 ) % # border + 1
2021-06-01 00:01:58 -07:00
if type ( border [ id ] ) == ' table ' then
-- border specified as a table of <character, highlight group>
return # border [ id ] [ 1 ] > 0 and 1 or 0
elseif type ( border [ id ] ) == ' string ' then
-- border specified as a list of border characters
return # border [ id ] > 0 and 1 or 0
end
2021-08-27 04:19:17 -07:00
error (
string.format (
' invalid floating preview border: %s. :help vim.api.nvim_open_win() ' ,
vim.inspect ( border )
2022-07-07 09:27:18 -07:00
)
)
2021-06-01 00:01:58 -07:00
end
height = height + border_height ( 2 ) -- top
height = height + border_height ( 6 ) -- bottom
width = width + border_width ( 4 ) -- right
width = width + border_width ( 8 ) -- left
2021-04-08 10:32:05 -07:00
end
return { height = height , width = width }
end
2019-11-13 13:55:26 -07:00
local function split_lines ( value )
2022-08-05 06:23:01 -07:00
value = string.gsub ( value , ' \r \n ? ' , ' \n ' )
2023-09-18 11:04:01 -07:00
return split ( value , ' \n ' , { plain = true , trimempty = true } )
2019-11-13 13:55:26 -07:00
end
2022-10-08 01:22:25 -07:00
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
2021-12-10 07:17:50 -07:00
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
2023-03-06 23:17:52 -07:00
---@param index integer|nil byte index (utf-8), or `nil` for length
2023-07-01 03:42:37 -07:00
---@param encoding string|nil utf-8|utf-16|utf-32|nil defaults to utf-16
2023-03-06 23:17:52 -07:00
---@return integer `encoding` index of `index` in `line`
2021-12-10 07:17:50 -07:00
function M . _str_utfindex_enc ( line , index , encoding )
2021-12-20 09:54:05 -07:00
if not encoding then
encoding = ' utf-16 '
end
if encoding == ' utf-8 ' then
if index then
return index
else
return # line
end
elseif encoding == ' utf-16 ' then
local _ , col16 = vim.str_utfindex ( line , index )
return col16
elseif encoding == ' utf-32 ' then
local col32 , _ = vim.str_utfindex ( line , index )
return col32
2021-12-10 07:17:50 -07:00
else
2021-12-20 09:54:05 -07:00
error ( ' Invalid encoding: ' .. vim.inspect ( encoding ) )
2021-12-10 07:17:50 -07:00
end
end
--- Convert UTF index to `encoding` index.
--- Convenience wrapper around vim.str_byteindex
---Alternative to vim.str_byteindex that takes an encoding.
---@param line string line to be indexed
2023-03-06 23:17:52 -07:00
---@param index integer UTF index
2023-07-01 03:42:37 -07:00
---@param encoding string utf-8|utf-16|utf-32| defaults to utf-16
2023-03-06 23:17:52 -07:00
---@return integer byte (utf-8) index of `encoding` index `index` in `line`
2021-12-10 07:17:50 -07:00
function M . _str_byteindex_enc ( line , index , encoding )
2021-12-20 09:54:05 -07:00
if not encoding then
encoding = ' utf-16 '
end
if encoding == ' utf-8 ' then
if index then
return index
else
return # line
end
elseif encoding == ' utf-16 ' then
return vim.str_byteindex ( line , index , true )
elseif encoding == ' utf-32 ' then
return vim.str_byteindex ( line , index )
2021-12-10 07:17:50 -07:00
else
2021-12-20 09:54:05 -07:00
error ( ' Invalid encoding: ' .. vim.inspect ( encoding ) )
2021-12-10 07:17:50 -07:00
end
end
local _str_utfindex_enc = M._str_utfindex_enc
local _str_byteindex_enc = M._str_byteindex_enc
2023-09-29 08:37:14 -07:00
2020-08-19 09:17:08 -07:00
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
---
2023-09-29 08:37:14 -07:00
---@deprecated
2021-08-22 13:55:28 -07:00
---@param lines (table) Original list of strings
2023-06-24 04:47:10 -07:00
---@param A (table) Start position; a 2-tuple of {line,col} numbers
---@param B (table) End position; a 2-tuple of {line,col} numbers
2023-07-01 03:42:37 -07:00
---@param new_lines (table) list of strings to replace the original
---@return table The modified {lines} object
2019-11-21 16:19:06 -07:00
function M . set_lines ( lines , A , B , new_lines )
2023-12-25 13:28:28 -07:00
vim.deprecate ( ' vim.lsp.util.set_lines() ' , ' nil ' , ' 0.12 ' )
2019-11-21 16:19:06 -07:00
-- 0-indexing to 1-indexing
local i_0 = A [ 1 ] + 1
2019-12-20 03:50:37 -07:00
-- If it extends past the end, truncate it to the end. This is because the
-- way the LSP describes the range including the last newline is by
-- specifying a line number after what we would call the last line.
local i_n = math.min ( B [ 1 ] + 1 , # lines )
2021-05-21 00:06:00 -07:00
if not ( i_0 >= 1 and i_0 <= # lines + 1 and i_n >= 1 and i_n <= # lines ) then
2019-11-21 16:19:06 -07:00
error ( ' Invalid range: ' .. vim.inspect ( { A = A , B = B , # lines , new_lines } ) )
2019-11-20 21:51:44 -07:00
end
local prefix = ' '
2019-11-21 16:19:06 -07:00
local suffix = lines [ i_n ] : sub ( B [ 2 ] + 1 )
if A [ 2 ] > 0 then
prefix = lines [ i_0 ] : sub ( 1 , A [ 2 ] )
2019-11-20 21:51:44 -07:00
end
2019-11-22 00:58:32 -07:00
local n = i_n - i_0 + 1
if n ~= # new_lines then
for _ = 1 , n - # new_lines do
table.remove ( lines , i_0 )
end
for _ = 1 , # new_lines - n do
table.insert ( lines , i_0 , ' ' )
end
end
for i = 1 , # new_lines do
lines [ i - 1 + i_0 ] = new_lines [ i ]
end
2019-11-20 21:51:44 -07:00
if # suffix > 0 then
2019-11-22 00:58:32 -07:00
local i = i_0 + # new_lines - 1
lines [ i ] = lines [ i ] .. suffix
2019-11-20 21:51:44 -07:00
end
if # prefix > 0 then
2019-11-22 00:58:32 -07:00
lines [ i_0 ] = prefix .. lines [ i_0 ]
2019-11-20 21:51:44 -07:00
end
2019-11-22 00:58:32 -07:00
return lines
2019-11-21 16:19:06 -07:00
end
local function sort_by_key ( fn )
return function ( a , b )
local ka , kb = fn ( a ) , fn ( b )
assert ( # ka == # kb )
for i = 1 , # ka do
if ka [ i ] ~= kb [ i ] then
return ka [ i ] < kb [ i ]
end
2019-11-20 21:51:44 -07:00
end
2019-11-21 16:19:06 -07:00
-- every value must have been equal here, which means it's not less than.
return false
2019-11-20 21:51:44 -07:00
end
end
2021-11-24 18:49:34 -07:00
--- Gets the zero-indexed lines from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
---
2023-03-06 23:17:52 -07:00
---@param bufnr integer bufnr to get the lines from
---@param rows integer[] zero-indexed line numbers
2023-07-01 03:42:37 -07:00
---@return table<integer, string>|string a table mapping rows to lines
2021-11-24 18:49:34 -07:00
local function get_lines ( bufnr , rows )
rows = type ( rows ) == ' table ' and rows or { rows }
2022-01-08 03:30:59 -07:00
-- This is needed for bufload and bufloaded
if bufnr == 0 then
2022-07-15 09:26:47 -07:00
bufnr = api.nvim_get_current_buf ( )
2022-01-08 03:30:59 -07:00
end
2021-11-24 18:49:34 -07:00
local function buf_lines ( )
local lines = { }
2023-05-04 22:41:36 -07:00
for _ , row in ipairs ( rows ) do
2022-07-15 09:26:47 -07:00
lines [ row ] = ( api.nvim_buf_get_lines ( bufnr , row , row + 1 , false ) or { ' ' } ) [ 1 ]
2021-11-24 18:49:34 -07:00
end
return lines
end
2023-05-04 22:41:36 -07:00
-- use loaded buffers if available
if vim.fn . bufloaded ( bufnr ) == 1 then
return buf_lines ( )
end
2021-11-24 18:49:34 -07:00
local uri = vim.uri_from_bufnr ( bufnr )
-- load the buffer if this is not a file uri
-- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
if uri : sub ( 1 , 4 ) ~= ' file ' then
vim.fn . bufload ( bufnr )
return buf_lines ( )
end
2022-07-15 09:26:47 -07:00
local filename = api.nvim_buf_get_name ( bufnr )
2021-11-24 18:49:34 -07:00
-- get the data from the file
local fd = uv.fs_open ( filename , ' r ' , 438 )
if not fd then
return ' '
end
local stat = uv.fs_fstat ( fd )
local data = uv.fs_read ( fd , stat.size , 0 )
uv.fs_close ( fd )
local lines = { } -- rows we need to retrieve
local need = 0 -- keep track of how many unique rows we need
for _ , row in pairs ( rows ) do
if not lines [ row ] then
need = need + 1
end
lines [ row ] = true
end
local found = 0
local lnum = 0
for line in string.gmatch ( data , ' ([^ \n ]*) \n ? ' ) do
if lines [ lnum ] == true then
lines [ lnum ] = line
found = found + 1
if found == need then
break
end
end
lnum = lnum + 1
end
-- change any lines we didn't find to the empty string
for i , line in pairs ( lines ) do
if line == true then
lines [ i ] = ' '
end
end
return lines
end
--- Gets the zero-indexed line from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
---
2023-03-06 23:17:52 -07:00
---@param bufnr integer
---@param row integer zero-indexed line number
2021-11-24 18:49:34 -07:00
---@return string the line at row in filename
local function get_line ( bufnr , row )
return get_lines ( bufnr , { row } ) [ row ]
end
2020-05-18 23:49:13 -07:00
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
2023-07-01 03:42:37 -07:00
---@param offset_encoding string|nil utf-8|utf-16|utf-32
---@return integer
2021-11-06 15:05:52 -07:00
local function get_line_byte_from_position ( bufnr , position , offset_encoding )
2020-05-18 23:49:13 -07:00
-- LSP's line and characters are 0-indexed
-- Vim's line and columns are 1-indexed
local col = position.character
-- When on the first character, we can ignore the difference between byte and
-- character
if col > 0 then
2022-01-13 02:28:13 -07:00
local line = get_line ( bufnr , position.line ) or ' '
2021-11-24 18:49:34 -07:00
local ok , result
2021-12-10 07:17:50 -07:00
ok , result = pcall ( _str_byteindex_enc , line , col , offset_encoding )
2021-11-24 18:49:34 -07:00
if ok then
return result
2020-05-17 10:47:14 -07:00
end
2021-11-24 18:49:34 -07:00
return math.min ( # line , col )
2020-05-17 10:47:14 -07:00
end
2020-05-18 23:49:13 -07:00
return col
2020-05-17 10:47:14 -07:00
end
2020-12-20 13:59:25 -07:00
--- Process and return progress reports from lsp server
2021-11-29 20:31:19 -07:00
---@private
2023-06-09 02:32:43 -07:00
---@deprecated Use vim.lsp.status() or access client.progress directly
2020-12-20 13:59:25 -07:00
function M . get_progress_messages ( )
2023-12-25 13:28:28 -07:00
vim.deprecate ( ' vim.lsp.util.get_progress_messages() ' , ' vim.lsp.status() ' , ' 0.11 ' )
2020-12-20 13:59:25 -07:00
local new_messages = { }
local progress_remove = { }
2023-07-17 09:27:16 -07:00
for _ , client in ipairs ( vim.lsp . get_clients ( ) ) do
2023-06-09 02:32:43 -07:00
local groups = { }
for progress in client.progress do
local value = progress.value
if type ( value ) == ' table ' and value.kind then
local group = groups [ progress.token ]
if not group then
group = {
done = false ,
progress = true ,
title = ' empty title ' ,
}
groups [ progress.token ] = group
end
group.title = value.title or group.title
group.cancellable = value.cancellable or group.cancellable
if value.kind == ' end ' then
group.done = true
end
group.message = value.message or group.message
group.percentage = value.percentage or group.percentage
end
end
for _ , group in pairs ( groups ) do
table.insert ( new_messages , group )
end
2020-12-20 13:59:25 -07:00
local messages = client.messages
local data = messages
for token , ctx in pairs ( data.progress ) do
local new_report = {
name = data.name ,
title = ctx.title or ' empty title ' ,
message = ctx.message ,
percentage = ctx.percentage ,
2021-10-10 11:41:16 -07:00
done = ctx.done ,
2020-12-20 13:59:25 -07:00
progress = true ,
}
table.insert ( new_messages , new_report )
if ctx.done then
table.insert ( progress_remove , { client = client , token = token } )
end
2022-05-09 02:23:51 -07:00
end
2021-10-21 09:36:27 -07:00
end
for _ , item in ipairs ( progress_remove ) do
item.client . messages.progress [ item.token ] = nil
2020-12-20 13:59:25 -07:00
end
return new_messages
end
2020-08-19 09:17:08 -07:00
--- Applies a list of text edits to a buffer.
2021-09-18 13:19:21 -07:00
---@param text_edits table list of `TextEdit` objects
2023-03-06 23:17:52 -07:00
---@param bufnr integer Buffer id
2022-03-30 12:04:17 -07:00
---@param offset_encoding string utf-8|utf-16|utf-32
2021-08-09 10:45:00 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
2021-12-10 07:17:50 -07:00
function M . apply_text_edits ( text_edits , bufnr , offset_encoding )
2021-11-26 02:36:37 -07:00
validate ( {
text_edits = { text_edits , ' t ' , false } ,
bufnr = { bufnr , ' number ' , false } ,
2022-01-13 03:34:04 -07:00
offset_encoding = { offset_encoding , ' string ' , false } ,
2021-11-26 02:36:37 -07:00
} )
2019-11-20 21:51:44 -07:00
if not next ( text_edits ) then
return
end
2024-02-24 18:21:57 -07:00
assert ( bufnr ~= 0 , ' Explicit buffer number is required ' )
2022-07-15 09:26:47 -07:00
if not api.nvim_buf_is_loaded ( bufnr ) then
2020-05-08 07:04:41 -07:00
vim.fn . bufload ( bufnr )
end
2022-12-19 09:37:45 -07:00
vim.bo [ bufnr ] . buflisted = true
2019-11-21 16:19:06 -07:00
2021-09-18 13:19:21 -07:00
-- Fix reversed range and indexing each text_edits
local index = 0
text_edits = vim.tbl_map ( function ( text_edit )
index = index + 1
text_edit._index = index
if
text_edit.range . start.line > text_edit.range [ ' end ' ] . line
or text_edit.range . start.line == text_edit.range [ ' end ' ] . line
and text_edit.range . start.character > text_edit.range [ ' end ' ] . character
then
local start = text_edit.range . start
text_edit.range . start = text_edit.range [ ' end ' ]
text_edit.range [ ' end ' ] = start
end
return text_edit
end , text_edits )
-- Sort text_edits
table.sort ( text_edits , function ( a , b )
if a.range . start.line ~= b.range . start.line then
return a.range . start.line > b.range . start.line
end
if a.range . start.character ~= b.range . start.character then
return a.range . start.character > b.range . start.character
end
if a._index ~= b._index then
return a._index > b._index
end
end )
2023-06-04 16:45:01 -07:00
-- save and restore local marks since they get deleted by nvim_buf_set_lines
local marks = { }
2024-02-24 18:21:57 -07:00
for _ , m in pairs ( vim.fn . getmarklist ( bufnr ) ) do
2023-06-04 16:45:01 -07:00
if m.mark : match ( " ^'[a-z]$ " ) then
marks [ m.mark : sub ( 2 , 2 ) ] = { m.pos [ 2 ] , m.pos [ 3 ] - 1 } -- api-indexed
end
end
2021-09-18 13:19:21 -07:00
-- Apply text edits.
2022-01-13 02:28:13 -07:00
local has_eol_text_edit = false
2021-09-18 13:19:21 -07:00
for _ , text_edit in ipairs ( text_edits ) do
2022-01-14 09:02:44 -07:00
-- Normalize line ending
text_edit.newText , _ = string.gsub ( text_edit.newText , ' \r \n ? ' , ' \n ' )
2022-01-13 02:28:13 -07:00
-- Convert from LSP style ranges to Neovim style ranges.
2021-09-18 13:19:21 -07:00
local e = {
start_row = text_edit.range . start.line ,
2022-01-13 02:28:13 -07:00
start_col = get_line_byte_from_position ( bufnr , text_edit.range . start , offset_encoding ) ,
2021-09-18 13:19:21 -07:00
end_row = text_edit.range [ ' end ' ] . line ,
2022-01-13 02:28:13 -07:00
end_col = get_line_byte_from_position ( bufnr , text_edit.range [ ' end ' ] , offset_encoding ) ,
2023-03-14 04:59:43 -07:00
text = split ( text_edit.newText , ' \n ' , { plain = true } ) ,
2021-09-18 13:19:21 -07:00
}
2022-01-13 02:28:13 -07:00
2022-07-15 09:26:47 -07:00
local max = api.nvim_buf_line_count ( bufnr )
2022-09-20 13:14:58 -07:00
-- If the whole edit is after the lines in the buffer we can simply add the new text to the end
-- of the buffer.
if max <= e.start_row then
api.nvim_buf_set_lines ( bufnr , max , max , false , e.text )
else
local last_line_len = # ( get_line ( bufnr , math.min ( e.end_row , max - 1 ) ) or ' ' )
-- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
-- accept it so we should fix it here.
2022-01-13 02:28:13 -07:00
if max <= e.end_row then
e.end_row = max - 1
2022-09-20 13:14:58 -07:00
e.end_col = last_line_len
has_eol_text_edit = true
else
-- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the
-- replacement text ends with a newline We can likely assume that the replacement is assumed
-- to be meant to replace the newline with another newline and we need to make sure this
-- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
-- in the file some servers (clangd on windows) will include that character in the line
-- while nvim_buf_set_text doesn't count it as part of the line.
if
e.end_col > last_line_len
and # text_edit.newText > 0
and string.sub ( text_edit.newText , - 1 ) == ' \n '
then
table.remove ( e.text , # e.text )
end
2022-01-13 02:28:13 -07:00
end
2022-09-20 13:14:58 -07:00
-- Make sure we don't go out of bounds for e.end_col
e.end_col = math.min ( last_line_len , e.end_col )
api.nvim_buf_set_text ( bufnr , e.start_row , e.start_col , e.end_row , e.end_col , e.text )
2021-09-18 13:19:21 -07:00
end
2019-11-21 16:19:06 -07:00
end
2022-07-15 09:26:47 -07:00
local max = api.nvim_buf_line_count ( bufnr )
2022-01-13 02:28:13 -07:00
2023-06-04 16:45:01 -07:00
-- no need to restore marks that still exist
2024-02-24 18:21:57 -07:00
for _ , m in pairs ( vim.fn . getmarklist ( bufnr ) ) do
2023-06-04 16:45:01 -07:00
marks [ m.mark : sub ( 2 , 2 ) ] = nil
end
-- restore marks
for mark , pos in pairs ( marks ) do
if pos then
-- make sure we don't go out of bounds
pos [ 1 ] = math.min ( pos [ 1 ] , max )
pos [ 2 ] = math.min ( pos [ 2 ] , # ( get_line ( bufnr , pos [ 1 ] - 1 ) or ' ' ) )
vim.api . nvim_buf_set_mark ( bufnr or 0 , mark , pos [ 1 ] , pos [ 2 ] , { } )
end
end
2021-09-18 13:19:21 -07:00
-- Remove final line if needed
local fix_eol = has_eol_text_edit
2022-12-19 09:37:45 -07:00
fix_eol = fix_eol and ( vim.bo [ bufnr ] . eol or ( vim.bo [ bufnr ] . fixeol and not vim.bo [ bufnr ] . binary ) )
2022-01-13 02:28:13 -07:00
fix_eol = fix_eol and get_line ( bufnr , max - 1 ) == ' '
2021-09-18 13:19:21 -07:00
if fix_eol then
2022-07-15 09:26:47 -07:00
api.nvim_buf_set_lines ( bufnr , - 2 , - 1 , false , { } )
2019-11-20 21:51:44 -07:00
end
end
2019-11-13 13:55:26 -07:00
-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
-- local valid_unix_path_characters = "[^/]"
-- https://github.com/davidm/lua-glob-pattern
-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
-- function M.glob_to_regex(glob)
-- end
2020-08-19 09:17:08 -07:00
--- Can be used to extract the completion items from a
--- `textDocument/completion` request, which may return one of
--- `CompletionItem[]`, `CompletionList` or null.
2024-02-26 12:42:51 -07:00
---
--- Note that this method doesn't apply `itemDefaults` to `CompletionList`s, and hence the returned
--- results might be incorrect.
---
2023-10-21 00:57:50 -07:00
---@deprecated
2023-07-01 03:42:37 -07:00
---@param result table The result of a `textDocument/completion` request
2023-10-21 00:47:24 -07:00
---@return lsp.CompletionItem[] List of completion items
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
2019-11-13 13:55:26 -07:00
function M . extract_completion_items ( result )
2023-12-27 01:42:30 -07:00
vim.deprecate ( ' vim.lsp.util.extract_completion_items() ' , nil , ' 0.11 ' )
2019-11-13 13:55:26 -07:00
if type ( result ) == ' table ' and result.items then
2020-08-19 09:17:08 -07:00
-- result is a `CompletionList`
2019-11-13 13:55:26 -07:00
return result.items
elseif result ~= nil then
2020-08-19 09:17:08 -07:00
-- result is `CompletionItem[]`
2019-11-13 13:55:26 -07:00
return result
else
2020-08-19 09:17:08 -07:00
-- result is `null`
2019-11-13 13:55:26 -07:00
return { }
end
end
2020-08-19 09:17:08 -07:00
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
2021-01-11 09:39:11 -07:00
--- document.
2020-08-19 09:17:08 -07:00
---
2021-01-11 09:39:11 -07:00
---@param text_document_edit table: a `TextDocumentEdit` object
2023-03-06 23:17:52 -07:00
---@param index integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
2024-02-15 10:16:04 -07:00
---@param offset_encoding? string
2021-01-11 09:39:11 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
2022-01-13 03:34:04 -07:00
function M . apply_text_document_edit ( text_document_edit , index , offset_encoding )
2019-11-20 21:51:44 -07:00
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr ( text_document.uri )
2022-01-13 03:34:04 -07:00
if offset_encoding == nil then
vim.notify_once (
' apply_text_document_edit must be called with valid offset encoding ' ,
vim.log . levels.WARN
)
end
2020-11-02 06:50:44 -07:00
2021-01-11 09:39:11 -07:00
-- For lists of text document edits,
-- do not check the version after the first edit.
local should_check_version = true
if index and index > 1 then
should_check_version = false
end
2020-11-02 06:50:44 -07:00
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
2021-01-11 09:39:11 -07:00
if
should_check_version
and (
text_document.version
2021-04-14 12:29:32 -07:00
and text_document.version > 0
2020-11-02 06:50:44 -07:00
and M.buf_versions [ bufnr ]
2021-01-11 09:39:11 -07:00
and M.buf_versions [ bufnr ] > text_document.version
)
then
2020-11-02 06:50:44 -07:00
print ( ' Buffer ' , text_document.uri , ' newer than edits. ' )
return
2019-11-13 13:55:26 -07:00
end
2020-11-02 06:50:44 -07:00
2022-01-13 03:34:04 -07:00
M.apply_text_edits ( text_document_edit.edits , bufnr , offset_encoding )
2019-11-13 13:55:26 -07:00
end
2020-08-19 09:17:08 -07:00
--- Parses snippets in a completion entry.
---
2023-10-21 00:57:50 -07:00
---@deprecated
2021-09-14 04:31:41 -07:00
---@param input string unparsed snippet
2023-07-01 03:42:37 -07:00
---@return string parsed snippet
2020-05-28 05:31:56 -07:00
function M . parse_snippet ( input )
2023-12-27 01:42:30 -07:00
vim.deprecate ( ' vim.lsp.util.parse_snippet() ' , nil , ' 0.11 ' )
2021-09-14 04:31:41 -07:00
local ok , parsed = pcall ( function ( )
2023-10-01 09:54:04 -07:00
return snippet.parse ( input )
2021-09-14 04:31:41 -07:00
end )
if not ok then
return input
end
2023-10-01 09:54:04 -07:00
2023-10-20 23:51:26 -07:00
return tostring ( parsed )
2020-05-28 05:31:56 -07:00
end
2020-08-19 09:17:08 -07:00
--- Turns the result of a `textDocument/completion` request into vim-compatible
--- |complete-items|.
---
2023-10-21 00:57:50 -07:00
---@deprecated
2023-08-09 02:06:13 -07:00
---@param result table The result of a `textDocument/completion` call, e.g.
--- from |vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`,
2020-08-19 09:17:08 -07:00
--- `CompletionList` or `null`
2021-08-22 13:55:28 -07:00
---@param prefix (string) the prefix to filter the completion items
2023-10-21 00:47:24 -07:00
---@return table[] items
2023-07-01 03:42:37 -07:00
---@see complete-items
2020-02-17 21:38:52 -07:00
function M . text_document_completion_list_to_complete_items ( result , prefix )
2023-12-27 01:42:30 -07:00
vim.deprecate ( ' vim.lsp.util.text_document_completion_list_to_complete_items() ' , nil , ' 0.11 ' )
2024-01-22 10:23:28 -07:00
return vim.lsp . _completion._lsp_to_complete_items ( result , prefix )
2019-11-13 13:55:26 -07:00
end
2024-02-24 08:47:34 -07:00
local function path_components ( path )
return vim.split ( path , ' / ' , { plain = true } )
end
local function path_under_prefix ( path , prefix )
for i , c in ipairs ( prefix ) do
if c ~= path [ i ] then
return false
end
end
return true
end
--- Get list of buffers whose filename matches the given path prefix (normalized full path)
---@return integer[]
local function get_bufs_with_prefix ( prefix )
prefix = path_components ( prefix )
2023-03-14 05:08:37 -07:00
local buffers = { }
for _ , v in ipairs ( vim.api . nvim_list_bufs ( ) ) do
2024-02-24 08:47:34 -07:00
local bname = vim.api . nvim_buf_get_name ( v )
local path = path_components ( vim.fs . normalize ( bname , { expand_env = false } ) )
if path_under_prefix ( path , prefix ) then
2023-03-14 05:08:37 -07:00
table.insert ( buffers , v )
end
end
return buffers
end
2024-03-02 07:21:53 -07:00
local function escape_gsub_repl ( s )
return ( s : gsub ( ' %% ' , ' %%%% ' ) )
end
2024-02-27 08:20:32 -07:00
--- @class vim.lsp.util.rename.Opts
--- @inlinedoc
--- @field overwrite? boolean
--- @field ignoreIfExists? boolean
2021-03-15 14:34:22 -07:00
--- Rename old_fname to new_fname
2024-03-02 07:21:53 -07:00
---
--- Existing buffers are renamed as well, while maintaining their bufnr.
---
--- It deletes existing buffers that conflict with the renamed file name only when
--- * `opts` requests overwriting; or
2024-03-11 22:51:53 -07:00
--- * the conflicting buffers are not loaded, so that deleting them does not result in data loss.
2024-03-02 07:21:53 -07:00
---
2024-02-27 08:20:32 -07:00
--- @param old_fname string
--- @param new_fname string
--- @param opts? vim.lsp.util.rename.Opts Options:
2021-03-15 14:34:22 -07:00
function M . rename ( old_fname , new_fname , opts )
opts = opts or { }
2024-02-24 08:47:34 -07:00
local skip = not opts.overwrite or opts.ignoreIfExists
local old_fname_full = vim.uv . fs_realpath ( vim.fs . normalize ( old_fname , { expand_env = false } ) )
if not old_fname_full then
vim.notify ( ' Invalid path: ' .. old_fname , vim.log . levels.ERROR )
return
end
2022-07-09 16:57:35 -07:00
local target_exists = uv.fs_stat ( new_fname ) ~= nil
2024-02-24 08:47:34 -07:00
if target_exists and skip then
vim.notify ( new_fname .. ' already exists. Skipping rename. ' , vim.log . levels.ERROR )
2021-03-15 14:34:22 -07:00
return
end
2021-11-14 04:55:16 -07:00
2024-03-02 07:21:53 -07:00
local buf_rename = { } ---@type table<integer, {from: string, to: string}>
local old_fname_pat = ' ^ ' .. vim.pesc ( old_fname_full )
for b in
vim.iter ( get_bufs_with_prefix ( old_fname_full ) ) : filter ( function ( b )
-- No need to care about unloaded or nofile buffers. Also :saveas won't work for them.
return api.nvim_buf_is_loaded ( b )
and not vim.list_contains ( { ' nofile ' , ' nowrite ' } , vim.bo [ b ] . buftype )
end )
do
-- Renaming a buffer may conflict with another buffer that happens to have the same name. In
-- most cases, this would have been already detected by the file conflict check above, but the
-- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile"
-- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet.
-- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer.
local old_bname = vim.api . nvim_buf_get_name ( b )
local new_bname = old_bname : gsub ( old_fname_pat , escape_gsub_repl ( new_fname ) )
if vim.fn . bufexists ( new_bname ) == 1 then
local existing_buf = vim.fn . bufnr ( new_bname )
if api.nvim_buf_is_loaded ( existing_buf ) and skip then
vim.notify (
new_bname .. ' already exists in the buffer list. Skipping rename. ' ,
vim.log . levels.ERROR
)
return
end
-- no need to preserve if such a buffer is empty
api.nvim_buf_delete ( existing_buf , { } )
2024-02-28 09:32:25 -07:00
end
2024-03-02 07:21:53 -07:00
buf_rename [ b ] = { from = old_bname , to = new_bname }
2023-02-17 23:43:59 -07:00
end
2021-11-14 04:55:16 -07:00
2024-02-08 14:06:54 -07:00
local newdir = assert ( vim.fs . dirname ( new_fname ) )
vim.fn . mkdir ( newdir , ' p ' )
2024-02-24 08:47:34 -07:00
local ok , err = os.rename ( old_fname_full , new_fname )
2021-03-15 14:34:22 -07:00
assert ( ok , err )
2021-11-14 04:55:16 -07:00
2024-03-01 10:31:54 -07:00
local old_undofile = vim.fn . undofile ( old_fname_full )
if uv.fs_stat ( old_undofile ) ~= nil then
local new_undofile = vim.fn . undofile ( new_fname )
vim.fn . mkdir ( assert ( vim.fs . dirname ( new_undofile ) ) , ' p ' )
os.rename ( old_undofile , new_undofile )
end
2024-03-02 07:21:53 -07:00
for b , rename in pairs ( buf_rename ) do
-- Rename with :saveas. This does two things:
-- * Unset BF_WRITE_MASK, so that users don't get E13 when they do :write.
-- * Send didClose and didOpen via textDocument/didSave handler.
api.nvim_buf_call ( b , function ( )
vim.cmd ( ' keepalt saveas! ' .. vim.fn . fnameescape ( rename.to ) )
end )
-- Delete the new buffer with the old name created by :saveas. nvim_buf_delete and
-- :bwipeout are futile because the buffer will be added again somewhere else.
vim.cmd ( ' bdelete! ' .. vim.fn . bufnr ( rename.from ) )
2021-11-14 04:55:16 -07:00
end
2021-03-15 14:34:22 -07:00
end
2021-03-18 03:07:06 -07:00
local function create_file ( change )
local opts = change.options or { }
-- from spec: Overwrite wins over `ignoreIfExists`
local fname = vim.uri_to_fname ( change.uri )
if not opts.ignoreIfExists or opts.overwrite then
2022-09-24 03:46:21 -07:00
vim.fn . mkdir ( vim.fs . dirname ( fname ) , ' p ' )
2021-03-18 03:07:06 -07:00
local file = io.open ( fname , ' w ' )
2022-09-24 03:46:21 -07:00
if file then
file : close ( )
end
2021-03-18 03:07:06 -07:00
end
vim.fn . bufadd ( fname )
end
2021-03-18 03:32:33 -07:00
local function delete_file ( change )
local opts = change.options or { }
local fname = vim.uri_to_fname ( change.uri )
2022-07-09 16:57:35 -07:00
local stat = uv.fs_stat ( fname )
2021-03-18 03:32:33 -07:00
if opts.ignoreIfNotExists and not stat then
return
end
assert ( stat , ' Cannot delete not existing file or folder ' .. fname )
local flags
if stat and stat.type == ' directory ' then
flags = opts.recursive and ' rf ' or ' d '
else
flags = ' '
end
local bufnr = vim.fn . bufadd ( fname )
local result = tonumber ( vim.fn . delete ( fname , flags ) )
assert ( result == 0 , ' Could not delete file: ' .. fname .. ' , stat: ' .. vim.inspect ( stat ) )
2022-07-15 09:26:47 -07:00
api.nvim_buf_delete ( bufnr , { force = true } )
2021-03-18 03:32:33 -07:00
end
2020-08-19 09:17:08 -07:00
--- Applies a `WorkspaceEdit`.
---
2022-01-13 10:28:27 -07:00
---@param workspace_edit table `WorkspaceEdit`
---@param offset_encoding string utf-8|utf-16|utf-32 (required)
2024-02-15 10:16:04 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
2022-01-13 03:34:04 -07:00
function M . apply_workspace_edit ( workspace_edit , offset_encoding )
if offset_encoding == nil then
vim.notify_once (
' apply_workspace_edit must be called with valid offset encoding ' ,
vim.log . levels.WARN
)
end
2019-11-13 13:55:26 -07:00
if workspace_edit.documentChanges then
2021-01-11 09:39:11 -07:00
for idx , change in ipairs ( workspace_edit.documentChanges ) do
2021-03-15 14:34:22 -07:00
if change.kind == ' rename ' then
M.rename ( vim.uri_to_fname ( change.oldUri ) , vim.uri_to_fname ( change.newUri ) , change.options )
2021-03-18 03:07:06 -07:00
elseif change.kind == ' create ' then
create_file ( change )
2021-03-18 03:32:33 -07:00
elseif change.kind == ' delete ' then
delete_file ( change )
2021-03-15 14:34:22 -07:00
elseif change.kind then
2019-11-13 13:55:26 -07:00
error ( string.format ( ' Unsupported change: %q ' , vim.inspect ( change ) ) )
else
2022-01-13 03:34:04 -07:00
M.apply_text_document_edit ( change , idx , offset_encoding )
2019-11-13 13:55:26 -07:00
end
end
return
end
2019-11-20 17:35:11 -07:00
local all_changes = workspace_edit.changes
if not ( all_changes and not vim.tbl_isempty ( all_changes ) ) then
2019-11-13 13:55:26 -07:00
return
end
2019-11-20 17:35:11 -07:00
for uri , changes in pairs ( all_changes ) do
local bufnr = vim.uri_to_bufnr ( uri )
2022-01-13 07:47:35 -07:00
M.apply_text_edits ( changes , bufnr , offset_encoding )
2019-11-13 13:55:26 -07:00
end
end
2020-08-19 09:17:08 -07:00
--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
--- a list of lines containing valid markdown. Useful to populate the hover
--- window for `textDocument/hover`, for parsing the result of
--- `textDocument/signatureHelp`, and potentially others.
---
2023-09-12 20:51:21 -07:00
--- Note that if the input is of type `MarkupContent` and its kind is `plaintext`,
--- then the corresponding value is returned without further modifications.
---
2024-01-09 10:36:46 -07:00
---@param input (lsp.MarkedString | lsp.MarkedString[] | lsp.MarkupContent)
2023-01-12 07:51:19 -07:00
---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}.
2023-09-12 20:51:21 -07:00
---@return string[] extended with lines of converted markdown.
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
2019-11-13 13:55:26 -07:00
function M . convert_input_to_markdown_lines ( input , contents )
contents = contents or { }
-- MarkedString variation 1
if type ( input ) == ' string ' then
list_extend ( contents , split_lines ( input ) )
else
2023-09-12 20:51:21 -07:00
assert ( type ( input ) == ' table ' , ' Expected a table for LSP input ' )
2019-11-13 13:55:26 -07:00
-- MarkupContent
if input.kind then
2021-07-04 15:39:46 -07:00
local value = input.value or ' '
list_extend ( contents , split_lines ( value ) )
2019-11-13 13:55:26 -07:00
-- MarkupString variation 2
elseif input.language then
table.insert ( contents , ' ``` ' .. input.language )
list_extend ( contents , split_lines ( input.value or ' ' ) )
table.insert ( contents , ' ``` ' )
-- By deduction, this must be MarkedString[]
else
-- Use our existing logic to handle MarkedString
for _ , marked_string in ipairs ( input ) do
M.convert_input_to_markdown_lines ( marked_string , contents )
end
end
end
2020-02-26 16:00:06 -07:00
if ( contents [ 1 ] == ' ' or contents [ 1 ] == nil ) and # contents == 1 then
2019-11-13 13:55:26 -07:00
return { }
end
return contents
end
2023-09-12 20:51:21 -07:00
--- Converts `textDocument/signatureHelp` response to markdown lines.
2020-08-19 09:17:08 -07:00
---
2023-07-01 03:42:37 -07:00
---@param signature_help table Response of `textDocument/SignatureHelp`
---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block
---@param triggers table|nil list of trigger characters from the lsp server. used to better determine parameter offsets
---@return table|nil table list of lines of converted markdown.
---@return table|nil table of active hl
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
2021-07-09 03:38:29 -07:00
function M . convert_signature_help_to_markdown_lines ( signature_help , ft , triggers )
2020-04-19 14:40:54 -07:00
if not signature_help.signatures then
return
end
--The active signature. If omitted or the value lies outside the range of
2022-09-24 17:20:47 -07:00
--`signatures` the value defaults to zero or is ignored if `signatures.length == 0`.
--Whenever possible implementors should make an active decision about
2020-04-19 14:40:54 -07:00
--the active signature and shouldn't rely on a default value.
local contents = { }
2021-07-07 23:04:35 -07:00
local active_hl
2020-11-02 06:50:44 -07:00
local active_signature = signature_help.activeSignature or 0
2020-04-19 14:40:54 -07:00
-- If the activeSignature is not inside the valid range, then clip it.
2022-01-13 02:47:36 -07:00
-- In 3.15 of the protocol, activeSignature was allowed to be negative
if active_signature >= # signature_help.signatures or active_signature < 0 then
2020-04-19 14:40:54 -07:00
active_signature = 0
end
local signature = signature_help.signatures [ active_signature + 1 ]
if not signature then
return
end
2021-05-21 10:19:17 -07:00
local label = signature.label
if ft then
2023-09-12 20:51:21 -07:00
-- wrap inside a code block for proper rendering
2021-05-21 10:19:17 -07:00
label = ( ' ```%s \n %s \n ``` ' ) : format ( ft , label )
end
2023-09-18 11:04:01 -07:00
list_extend ( contents , split ( label , ' \n ' , { plain = true , trimempty = true } ) )
2020-04-19 14:40:54 -07:00
if signature.documentation then
2023-07-25 05:40:13 -07:00
-- if LSP returns plain string, we treat it as plaintext. This avoids
-- special characters like underscore or similar from being interpreted
-- as markdown font modifiers
if type ( signature.documentation ) == ' string ' then
signature.documentation = { kind = ' plaintext ' , value = signature.documentation }
end
2020-04-19 14:40:54 -07:00
M.convert_input_to_markdown_lines ( signature.documentation , contents )
end
2020-09-12 09:33:31 -07:00
if signature.parameters and # signature.parameters > 0 then
2021-07-07 23:04:35 -07:00
local active_parameter = ( signature.activeParameter or signature_help.activeParameter or 0 )
2021-07-09 03:38:29 -07:00
if active_parameter < 0 then
active_parameter = 0
end
-- If the activeParameter is > #parameters, then set it to the last
-- NOTE: this is not fully according to the spec, but a client-side interpretation
2020-09-12 09:33:31 -07:00
if active_parameter >= # signature.parameters then
2021-07-09 03:38:29 -07:00
active_parameter = # signature.parameters - 1
2020-04-19 14:40:54 -07:00
end
2021-07-09 03:38:29 -07:00
2020-09-12 09:33:31 -07:00
local parameter = signature.parameters [ active_parameter + 1 ]
2020-04-19 14:40:54 -07:00
if parameter then
--[=[
--Represents a parameter of a callable-signature. A parameter can
--have a label and a doc-comment.
interface ParameterInformation {
--The label of this parameter information.
--
--Either a string or an inclusive start and exclusive end offsets within its containing
--signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
--string representation as `Position` and `Range` does.
--
--*Note*: a label of type string should be a substring of its containing signature label.
--Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
label : string | [ number , number ] ;
--The human-readable doc-comment of this parameter. Will be shown
--in the UI but can be omitted.
documentation ? : string | MarkupContent ;
}
--]=]
2021-07-07 23:04:35 -07:00
if parameter.label then
if type ( parameter.label ) == ' table ' then
active_hl = parameter.label
else
2021-07-09 03:38:29 -07:00
local offset = 1
-- try to set the initial offset to the first found trigger character
for _ , t in ipairs ( triggers or { } ) do
local trigger_offset = signature.label : find ( t , 1 , true )
if trigger_offset and ( offset == 1 or trigger_offset < offset ) then
offset = trigger_offset
end
end
for p , param in pairs ( signature.parameters ) do
offset = signature.label : find ( param.label , offset , true )
if not offset then
break
end
if p == active_parameter + 1 then
active_hl = { offset - 1 , offset + # parameter.label - 1 }
break
end
offset = offset + # param.label + 1
end
2021-07-07 23:04:35 -07:00
end
end
2020-11-02 06:50:44 -07:00
if parameter.documentation then
2020-09-12 09:33:31 -07:00
M.convert_input_to_markdown_lines ( parameter.documentation , contents )
2020-04-19 14:40:54 -07:00
end
end
end
2021-07-07 23:04:35 -07:00
return contents , active_hl
2020-04-19 14:40:54 -07:00
end
2020-08-19 09:17:08 -07:00
--- Creates a table with sensible default options for a floating window. The
--- table can be passed to |nvim_open_win()|.
---
2023-07-01 03:42:37 -07:00
---@param width integer window width (in character cells)
---@param height integer window height (in character cells)
---@param opts table optional
2023-03-06 23:17:52 -07:00
--- - offset_x (integer) offset to add to `col`
--- - offset_y (integer) offset to add to `row`
2021-10-02 17:55:54 -07:00
--- - border (string or table) override `border`
--- - focusable (string or table) override `focusable`
--- - zindex (string or table) override `zindex`, defaults to 50
feat(float): open float relative to mouse #21531
Problem:
No easy way to position a LSP hover window relative to mouse.
Solution:
Introduce another option to the `relative` key in `nvim_open_win()`.
With this PR it should be possible to override the handler and do something
similar to this https://github.com/neovim/neovim/pull/19481#issuecomment-1193248674
to have hover information displayed from the mouse.
Test case:
```lua
local util = require('vim.lsp.util')
local function make_position_param(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
local row, col
local mouse = vim.fn.getmousepos()
row = mouse.line
col = mouse.column
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
row = row - 1
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
if #line < col then
return { line = 0, character = 0 }
end
col = util._str_utfindex_enc(line, col, offset_encoding)
return { line = row, character = col }
end
local make_params = function(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
return {
textDocument = util.make_text_document_params(buf),
position = make_position_param(window, offset_encoding),
}
end
local hover_timer = nil
vim.o.mousemoveevent = true
vim.keymap.set({ '', 'i' }, '<MouseMove>', function()
if hover_timer then
hover_timer:close()
end
hover_timer = vim.defer_fn(function()
hover_timer = nil
local params = make_params()
vim.lsp.buf_request(
0,
'textDocument/hover',
params,
vim.lsp.with(vim.lsp.handlers.hover, {
silent = true,
focusable = false,
relative = 'mouse',
})
)
end, 500)
return '<MouseMove>'
end, { expr = true })
```
2023-01-10 03:22:41 -07:00
--- - relative ("mouse"|"cursor") defaults to "cursor"
2023-09-10 01:02:23 -07:00
--- - anchor_bias ("auto"|"above"|"below") defaults to "auto"
--- - "auto": place window based on which side of the cursor has more lines
--- - "above": place the window above the cursor unless there are not enough lines
--- to display the full window height.
--- - "below": place the window below the cursor unless there are not enough lines
--- to display the full window height.
2023-07-01 03:42:37 -07:00
---@return table Options
2019-11-13 13:55:26 -07:00
function M . make_floating_popup_options ( width , height , opts )
validate ( {
opts = { opts , ' t ' , true } ,
} )
opts = opts or { }
validate ( {
[ ' opts.offset_x ' ] = { opts.offset_x , ' n ' , true } ,
[ ' opts.offset_y ' ] = { opts.offset_y , ' n ' , true } ,
} )
local anchor = ' '
local row , col
feat(float): open float relative to mouse #21531
Problem:
No easy way to position a LSP hover window relative to mouse.
Solution:
Introduce another option to the `relative` key in `nvim_open_win()`.
With this PR it should be possible to override the handler and do something
similar to this https://github.com/neovim/neovim/pull/19481#issuecomment-1193248674
to have hover information displayed from the mouse.
Test case:
```lua
local util = require('vim.lsp.util')
local function make_position_param(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
local row, col
local mouse = vim.fn.getmousepos()
row = mouse.line
col = mouse.column
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
row = row - 1
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
if #line < col then
return { line = 0, character = 0 }
end
col = util._str_utfindex_enc(line, col, offset_encoding)
return { line = row, character = col }
end
local make_params = function(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
return {
textDocument = util.make_text_document_params(buf),
position = make_position_param(window, offset_encoding),
}
end
local hover_timer = nil
vim.o.mousemoveevent = true
vim.keymap.set({ '', 'i' }, '<MouseMove>', function()
if hover_timer then
hover_timer:close()
end
hover_timer = vim.defer_fn(function()
hover_timer = nil
local params = make_params()
vim.lsp.buf_request(
0,
'textDocument/hover',
params,
vim.lsp.with(vim.lsp.handlers.hover, {
silent = true,
focusable = false,
relative = 'mouse',
})
)
end, 500)
return '<MouseMove>'
end, { expr = true })
```
2023-01-10 03:22:41 -07:00
local lines_above = opts.relative == ' mouse ' and vim.fn . getmousepos ( ) . line - 1
or vim.fn . winline ( ) - 1
2020-01-03 05:39:25 -07:00
local lines_below = vim.fn . winheight ( 0 ) - lines_above
2023-09-10 01:02:23 -07:00
local anchor_bias = opts.anchor_bias or ' auto '
local anchor_below
if anchor_bias == ' below ' then
anchor_below = ( lines_below > lines_above ) or ( height <= lines_below )
elseif anchor_bias == ' above ' then
local anchor_above = ( lines_above > lines_below ) or ( height <= lines_above )
anchor_below = not anchor_above
else
anchor_below = lines_below > lines_above
end
2023-10-08 01:09:25 -07:00
local border_height = get_border_size ( opts ) . height
2023-09-10 01:02:23 -07:00
if anchor_below then
2019-11-13 13:55:26 -07:00
anchor = anchor .. ' N '
2023-10-08 01:09:25 -07:00
height = math.max ( math.min ( lines_below - border_height , height ) , 0 )
2019-11-13 13:55:26 -07:00
row = 1
else
anchor = anchor .. ' S '
2023-10-08 01:09:25 -07:00
height = math.max ( math.min ( lines_above - border_height , height ) , 0 )
2021-10-02 17:36:24 -07:00
row = 0
2019-11-13 13:55:26 -07:00
end
feat(float): open float relative to mouse #21531
Problem:
No easy way to position a LSP hover window relative to mouse.
Solution:
Introduce another option to the `relative` key in `nvim_open_win()`.
With this PR it should be possible to override the handler and do something
similar to this https://github.com/neovim/neovim/pull/19481#issuecomment-1193248674
to have hover information displayed from the mouse.
Test case:
```lua
local util = require('vim.lsp.util')
local function make_position_param(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
local row, col
local mouse = vim.fn.getmousepos()
row = mouse.line
col = mouse.column
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
row = row - 1
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
if #line < col then
return { line = 0, character = 0 }
end
col = util._str_utfindex_enc(line, col, offset_encoding)
return { line = row, character = col }
end
local make_params = function(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
return {
textDocument = util.make_text_document_params(buf),
position = make_position_param(window, offset_encoding),
}
end
local hover_timer = nil
vim.o.mousemoveevent = true
vim.keymap.set({ '', 'i' }, '<MouseMove>', function()
if hover_timer then
hover_timer:close()
end
hover_timer = vim.defer_fn(function()
hover_timer = nil
local params = make_params()
vim.lsp.buf_request(
0,
'textDocument/hover',
params,
vim.lsp.with(vim.lsp.handlers.hover, {
silent = true,
focusable = false,
relative = 'mouse',
})
)
end, 500)
return '<MouseMove>'
end, { expr = true })
```
2023-01-10 03:22:41 -07:00
local wincol = opts.relative == ' mouse ' and vim.fn . getmousepos ( ) . column or vim.fn . wincol ( )
2022-12-19 09:37:45 -07:00
if wincol + width + ( opts.offset_x or 0 ) <= vim.o . columns then
2019-11-13 13:55:26 -07:00
anchor = anchor .. ' W '
col = 0
else
anchor = anchor .. ' E '
col = 1
end
2022-11-21 03:06:14 -07:00
local title = ( opts.border and opts.title ) and opts.title or nil
local title_pos
if title then
title_pos = opts.title_pos or ' center '
end
2019-11-13 13:55:26 -07:00
return {
anchor = anchor ,
col = col + ( opts.offset_x or 0 ) ,
height = height ,
2021-06-01 00:33:07 -07:00
focusable = opts.focusable ,
feat(float): open float relative to mouse #21531
Problem:
No easy way to position a LSP hover window relative to mouse.
Solution:
Introduce another option to the `relative` key in `nvim_open_win()`.
With this PR it should be possible to override the handler and do something
similar to this https://github.com/neovim/neovim/pull/19481#issuecomment-1193248674
to have hover information displayed from the mouse.
Test case:
```lua
local util = require('vim.lsp.util')
local function make_position_param(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
local row, col
local mouse = vim.fn.getmousepos()
row = mouse.line
col = mouse.column
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
row = row - 1
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
if not line then
return { line = 0, character = 0 }
end
if #line < col then
return { line = 0, character = 0 }
end
col = util._str_utfindex_enc(line, col, offset_encoding)
return { line = row, character = col }
end
local make_params = function(window, offset_encoding)
window = window or 0
local buf = vim.api.nvim_win_get_buf(window)
offset_encoding = offset_encoding or util._get_offset_encoding(buf)
return {
textDocument = util.make_text_document_params(buf),
position = make_position_param(window, offset_encoding),
}
end
local hover_timer = nil
vim.o.mousemoveevent = true
vim.keymap.set({ '', 'i' }, '<MouseMove>', function()
if hover_timer then
hover_timer:close()
end
hover_timer = vim.defer_fn(function()
hover_timer = nil
local params = make_params()
vim.lsp.buf_request(
0,
'textDocument/hover',
params,
vim.lsp.with(vim.lsp.handlers.hover, {
silent = true,
focusable = false,
relative = 'mouse',
})
)
end, 500)
return '<MouseMove>'
end, { expr = true })
```
2023-01-10 03:22:41 -07:00
relative = opts.relative == ' mouse ' and ' mouse ' or ' cursor ' ,
2019-11-13 13:55:26 -07:00
row = row + ( opts.offset_y or 0 ) ,
style = ' minimal ' ,
width = width ,
2021-04-08 10:32:05 -07:00
border = opts.border or default_border ,
2021-07-19 09:03:59 -07:00
zindex = opts.zindex or 50 ,
2022-11-21 03:06:14 -07:00
title = title ,
title_pos = title_pos ,
2019-11-13 13:55:26 -07:00
}
end
2022-10-08 01:22:25 -07:00
--- Shows document and optionally jumps to the location.
2020-08-19 09:17:08 -07:00
---
2022-01-13 03:34:04 -07:00
---@param location table (`Location`|`LocationLink`)
2023-07-01 03:42:37 -07:00
---@param offset_encoding string|nil utf-8|utf-16|utf-32
2023-01-04 04:48:41 -07:00
---@param opts table|nil options
2022-10-08 01:22:25 -07:00
--- - 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 )
2020-05-02 06:21:07 -07:00
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
2022-10-08 01:22:25 -07:00
return false
2020-05-02 06:21:07 -07:00
end
2022-01-13 03:34:04 -07:00
if offset_encoding == nil then
2022-10-08 01:22:25 -07:00
vim.notify_once ( ' show_document must be called with valid offset encoding ' , vim.log . levels.WARN )
2022-01-13 03:34:04 -07:00
end
2021-05-19 06:33:32 -07:00
local bufnr = vim.uri_to_bufnr ( uri )
2020-04-28 07:47:22 -07:00
2022-10-08 01:22:25 -07:00
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
2020-04-28 07:47:22 -07:00
2024-02-24 05:14:12 -07:00
local win = opts.reuse_win and vim.fn . win_findbuf ( bufnr ) [ 1 ]
2022-10-08 01:22:25 -07:00
or focus and api.nvim_get_current_win ( )
or create_window_without_focus ( )
2022-12-19 09:37:45 -07:00
vim.bo [ bufnr ] . buflisted = true
2022-10-08 01:22:25 -07:00
api.nvim_win_set_buf ( win , bufnr )
if focus then
2022-07-15 09:26:47 -07:00
api.nvim_set_current_win ( win )
2022-05-18 12:03:24 -07:00
end
2022-10-08 01:22:25 -07:00
-- location may be Location or LocationLink
2020-05-02 06:21:07 -07:00
local range = location.range or location.targetSelectionRange
2022-10-08 01:22:25 -07:00
if range then
2023-12-28 16:00:30 -07:00
-- Jump to new location (adjusting for encoding of characters)
2022-10-08 01:22:25 -07:00
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
2019-11-26 06:59:40 -07:00
return true
end
2022-10-08 01:22:25 -07:00
--- Jumps to a location.
---
---@param location table (`Location`|`LocationLink`)
2023-07-01 03:42:37 -07:00
---@param offset_encoding string|nil utf-8|utf-16|utf-32
2023-01-04 04:48:41 -07:00
---@param reuse_win boolean|nil Jump to existing window if buffer is already open.
2022-10-08 01:22:25 -07:00
---@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
2020-08-19 09:17:08 -07:00
--- Previews a location in a floating window
2020-05-26 06:07:10 -07:00
---
--- behavior depends on type of location:
--- - for Location, range is shown (e.g., function definition)
--- - for LocationLink, targetRange is shown (e.g., body of function definition)
---
2023-07-01 03:42:37 -07:00
---@param location table a single `Location` or `LocationLink`
2024-02-15 10:16:04 -07:00
---@param opts table
2023-07-01 03:42:37 -07:00
---@return integer|nil buffer id of float window
---@return integer|nil window id of float window
2021-05-21 10:16:50 -07:00
function M . preview_location ( location , opts )
2020-05-26 06:07:10 -07:00
-- location may be LocationLink or Location (more useful for the former)
local uri = location.targetUri or location.uri
if uri == nil then
return
end
local bufnr = vim.uri_to_bufnr ( uri )
2022-07-15 09:26:47 -07:00
if not api.nvim_buf_is_loaded ( bufnr ) then
2020-05-26 06:07:10 -07:00
vim.fn . bufload ( bufnr )
end
local range = location.targetRange or location.range
2022-07-15 09:26:47 -07:00
local contents = api.nvim_buf_get_lines ( bufnr , range.start . line , range [ ' end ' ] . line + 1 , false )
2022-12-19 09:37:45 -07:00
local syntax = vim.bo [ bufnr ] . syntax
2021-05-21 10:16:50 -07:00
if syntax == ' ' then
-- When no syntax is set, we use filetype as fallback. This might not result
2023-09-12 20:51:21 -07:00
-- in a valid syntax definition.
2021-05-21 10:16:50 -07:00
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
2022-12-19 09:37:45 -07:00
syntax = vim.bo [ bufnr ] . filetype
2021-05-21 10:16:50 -07:00
end
2021-05-26 14:47:53 -07:00
opts = opts or { }
opts.focus_id = ' location '
2021-05-21 10:16:50 -07:00
return M.open_floating_preview ( contents , syntax , opts )
2020-05-26 06:07:10 -07:00
end
2019-11-26 06:59:40 -07:00
local function find_window_by_var ( name , value )
2022-07-15 09:26:47 -07:00
for _ , win in ipairs ( api.nvim_list_wins ( ) ) do
if npcall ( api.nvim_win_get_var , win , name ) == value then
2019-11-26 06:59:40 -07:00
return win
end
end
end
2023-09-12 20:51:21 -07:00
---Returns true if the line is empty or only contains whitespace.
---@param line string
---@return boolean
local function is_blank_line ( line )
return line and line : match ( ' ^%s*$ ' )
end
---Returns true if the line corresponds to a Markdown thematic break.
---@param line string
---@return boolean
local function is_separator_line ( line )
return line and line : match ( ' ^ ? ? ?%-%-%-+%s*$ ' )
end
---Replaces separator lines by the given divider and removing surrounding blank lines.
---@param contents string[]
---@param divider string
---@return string[]
local function replace_separators ( contents , divider )
local trimmed = { }
local l = 1
while l <= # contents do
local line = contents [ l ]
if is_separator_line ( line ) then
if l > 1 and is_blank_line ( contents [ l - 1 ] ) then
table.remove ( trimmed )
end
table.insert ( trimmed , divider )
if is_blank_line ( contents [ l + 1 ] ) then
l = l + 1
end
else
table.insert ( trimmed , line )
2020-07-05 18:09:52 -07:00
end
2023-09-12 20:51:21 -07:00
l = l + 1
2020-07-05 18:09:52 -07:00
end
2023-09-12 20:51:21 -07:00
return trimmed
end
---Collapses successive blank lines in the input table into a single one.
---@param contents string[]
---@return string[]
local function collapse_blank_lines ( contents )
local collapsed = { }
local l = 1
while l <= # contents do
local line = contents [ l ]
if is_blank_line ( line ) then
while is_blank_line ( contents [ l + 1 ] ) do
l = l + 1
end
2020-07-05 18:09:52 -07:00
end
2023-09-12 20:51:21 -07:00
table.insert ( collapsed , line )
l = l + 1
2020-07-05 18:09:52 -07:00
end
2023-09-12 20:51:21 -07:00
return collapsed
2020-06-04 11:23:03 -07:00
end
2021-06-25 06:23:02 -07:00
local function get_markdown_fences ( )
local fences = { }
for _ , fence in pairs ( vim.g . markdown_fenced_languages or { } ) do
local lang , syntax = fence : match ( ' ^(.*)=(.*)$ ' )
if lang then
fences [ lang ] = syntax
end
end
return fences
end
2020-08-19 09:17:08 -07:00
--- Converts markdown into syntax highlighted regions by stripping the code
2020-06-04 11:23:03 -07:00
--- blocks and converting them into highlighted code.
--- This will by default insert a blank line separator after those code block
--- regions to improve readability.
2021-05-26 14:47:53 -07:00
---
--- This method configures the given buffer and returns the lines to set.
---
--- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
2020-06-04 11:23:03 -07:00
---
2024-02-15 10:16:04 -07:00
---@param bufnr integer
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
2020-11-12 20:21:34 -07:00
---@param contents table of lines to show in window
2023-07-01 03:42:37 -07:00
---@param opts table with optional fields
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
2020-11-12 20:21:34 -07:00
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
--- - separator insert separator after code block
2023-07-01 03:42:37 -07:00
---@return table stripped content
2021-05-26 14:47:53 -07:00
function M . stylize_markdown ( bufnr , contents , opts )
2020-06-04 11:23:03 -07:00
validate ( {
contents = { contents , ' t ' } ,
opts = { opts , ' t ' , true } ,
} )
opts = opts or { }
2021-06-10 08:40:26 -07:00
-- table of fence types to {ft, begin, end}
-- when ft is nil, we get the ft from the regex match
local matchers = {
2023-07-16 03:11:45 -07:00
block = { nil , ' ```+%s*([a-zA-Z0-9_]*) ' , ' ```+ ' } ,
2022-12-03 05:51:57 -07:00
pre = { nil , ' <pre>([a-z0-9]*) ' , ' </pre> ' } ,
2021-06-10 08:40:26 -07:00
code = { ' ' , ' <code> ' , ' </code> ' } ,
2022-03-28 09:16:11 -07:00
text = { ' text ' , ' <text> ' , ' </text> ' } ,
2021-06-10 08:40:26 -07:00
}
local match_begin = function ( line )
for type , pattern in pairs ( matchers ) do
local ret = line : match ( string.format ( ' ^%%s*%s%%s*$ ' , pattern [ 2 ] ) )
if ret then
return {
type = type ,
ft = pattern [ 1 ] or ret ,
}
end
end
end
local match_end = function ( line , match )
local pattern = matchers [ match.type ]
return line : match ( string.format ( ' ^%%s*%s%%s*$ ' , pattern [ 3 ] ) )
end
2021-06-14 15:41:07 -07:00
-- Clean up
2023-09-18 11:04:01 -07:00
contents = vim.split ( table.concat ( contents , ' \n ' ) , ' \n ' , { trimempty = true } )
2021-06-14 15:41:07 -07:00
2019-12-20 03:50:37 -07:00
local stripped = { }
local highlights = { }
2021-06-14 15:41:07 -07:00
-- keep track of lnums that contain markdown
local markdown_lines = { }
2019-12-20 03:50:37 -07:00
do
local i = 1
while i <= # contents do
local line = contents [ i ]
2021-06-10 08:40:26 -07:00
local match = match_begin ( line )
if match then
2019-12-20 03:50:37 -07:00
local start = # stripped
i = i + 1
while i <= # contents do
line = contents [ i ]
2021-06-10 08:40:26 -07:00
if match_end ( line , match ) then
2019-12-20 03:50:37 -07:00
i = i + 1
break
end
table.insert ( stripped , line )
i = i + 1
end
table.insert ( highlights , {
2021-06-10 08:40:26 -07:00
ft = match.ft ,
2019-12-20 03:50:37 -07:00
start = start + 1 ,
2021-06-26 10:43:18 -07:00
finish = # stripped ,
2019-12-20 03:50:37 -07:00
} )
2021-08-06 14:15:49 -07:00
-- add a separator, but not on the last line
2022-12-03 05:51:57 -07:00
if opts.separator and i < # contents then
2021-08-06 14:15:49 -07:00
table.insert ( stripped , ' --- ' )
markdown_lines [ # stripped ] = true
end
2019-12-20 03:50:37 -07:00
else
2021-08-06 14:15:49 -07:00
-- strip any empty lines or separators prior to this separator in actual markdown
if line : match ( ' ^---+$ ' ) then
while
markdown_lines [ # stripped ]
and ( stripped [ # stripped ] : match ( ' ^%s*$ ' ) or stripped [ # stripped ] : match ( ' ^---+$ ' ) )
do
markdown_lines [ # stripped ] = false
table.remove ( stripped , # stripped )
end
end
-- add the line if its not an empty line following a separator
if
not (
line : match ( ' ^%s*$ ' )
and markdown_lines [ # stripped ]
and stripped [ # stripped ] : match ( ' ^---+$ ' )
2022-07-07 09:27:18 -07:00
)
2021-08-06 14:15:49 -07:00
then
table.insert ( stripped , line )
markdown_lines [ # stripped ] = true
end
2019-12-20 03:50:37 -07:00
i = i + 1
end
end
end
2020-06-04 11:23:03 -07:00
2023-03-25 09:46:07 -07:00
-- Handle some common html escape sequences
stripped = vim.tbl_map ( function ( line )
local escapes = {
[ ' > ' ] = ' > ' ,
[ ' < ' ] = ' < ' ,
[ ' " ' ] = ' " ' ,
[ ' ' ' ] = " ' " ,
[ '   ' ] = ' ' ,
[ '   ' ] = ' ' ,
[ ' & ' ] = ' & ' ,
}
return ( string.gsub ( line , ' &[^ ;]+; ' , escapes ) )
end , stripped )
2020-06-04 11:23:03 -07:00
-- Compute size of float needed to show (wrapped) lines
2022-07-15 09:26:47 -07:00
opts.wrap_at = opts.wrap_at or ( vim.wo [ ' wrap ' ] and api.nvim_win_get_width ( 0 ) )
2021-08-06 14:15:49 -07:00
local width = M._make_floating_popup_size ( stripped , opts )
2020-06-04 11:23:03 -07:00
2021-06-14 15:41:07 -07:00
local sep_line = string.rep ( ' ─ ' , math.min ( width , opts.wrap_at or width ) )
for l in pairs ( markdown_lines ) do
if stripped [ l ] : match ( ' ^---+$ ' ) then
stripped [ l ] = sep_line
end
end
2022-07-15 09:26:47 -07:00
api.nvim_buf_set_lines ( bufnr , 0 , - 1 , false , stripped )
2021-05-21 10:18:22 -07:00
2019-12-20 03:50:37 -07:00
local idx = 1
2021-05-26 14:47:53 -07:00
-- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
local langs = { }
2021-06-25 06:23:02 -07:00
local fences = get_markdown_fences ( )
2020-05-31 11:56:00 -07:00
local function apply_syntax_to_region ( ft , start , finish )
2021-05-20 01:42:08 -07:00
if ft == ' ' then
2021-05-21 10:18:22 -07:00
vim.cmd (
string.format (
' syntax region markdownCode start=+ \\ %%%dl+ end=+ \\ %%%dl+ keepend extend ' ,
start ,
finish + 1
)
2022-05-09 02:23:51 -07:00
)
2021-05-20 01:42:08 -07:00
return
end
2021-06-25 06:23:02 -07:00
ft = fences [ ft ] or ft
2019-12-20 03:50:37 -07:00
local name = ft .. idx
idx = idx + 1
local lang = ' @ ' .. ft : upper ( )
2021-05-26 14:47:53 -07:00
if not langs [ lang ] then
-- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
2022-07-15 09:26:47 -07:00
pcall ( api.nvim_buf_del_var , bufnr , ' current_syntax ' )
2023-07-01 03:42:37 -07:00
if # api.nvim_get_runtime_file ( ( ' syntax/%s.vim ' ) : format ( ft ) , true ) == 0 then
2021-05-26 14:47:53 -07:00
return
end
2023-07-25 04:38:48 -07:00
pcall ( vim.cmd , string.format ( ' syntax include %s syntax/%s.vim ' , lang , ft ) )
2021-05-26 14:47:53 -07:00
langs [ lang ] = true
2019-12-20 03:50:37 -07:00
end
2021-05-20 01:42:08 -07:00
vim.cmd (
string.format (
' syntax region %s start=+ \\ %%%dl+ end=+ \\ %%%dl+ contains=%s keepend ' ,
name ,
start ,
finish + 1 ,
lang
)
2022-05-09 02:23:51 -07:00
)
2019-12-20 03:50:37 -07:00
end
2021-05-21 10:18:22 -07:00
2021-05-26 14:47:53 -07:00
-- needs to run in the buffer for the regions to work
2022-07-15 09:26:47 -07:00
api.nvim_buf_call ( bufnr , function ( )
2021-05-26 14:47:53 -07:00
-- we need to apply lsp_markdown regions speperately, since otherwise
-- markdown regions can "bleed" through the other syntax regions
-- and mess up the formatting
local last = 1
for _ , h in ipairs ( highlights ) do
if last < h.start then
apply_syntax_to_region ( ' lsp_markdown ' , last , h.start - 1 )
end
apply_syntax_to_region ( h.ft , h.start , h.finish )
last = h.finish + 1
end
2021-06-25 11:45:42 -07:00
if last <= # stripped then
2021-05-26 14:47:53 -07:00
apply_syntax_to_region ( ' lsp_markdown ' , last , # stripped )
end
end )
2019-12-20 03:50:37 -07:00
2021-05-26 14:47:53 -07:00
return stripped
2019-12-20 03:50:37 -07:00
end
2024-02-27 08:20:32 -07:00
--- @class (private) vim.lsp.util._normalize_markdown.Opts
2023-09-12 20:51:21 -07:00
--- @field width integer Thematic breaks are expanded to this size. Defaults to 80.
--- Normalizes Markdown input to a canonical form.
---
--- The returned Markdown adheres to the GitHub Flavored Markdown (GFM)
--- specification.
---
--- The following transformations are made:
---
2023-09-18 11:04:01 -07:00
--- 1. Carriage returns ('\r') and empty lines at the beginning and end are removed
--- 2. Successive empty lines are collapsed into a single empty line
--- 3. Thematic breaks are expanded to the given width
2023-09-12 20:51:21 -07:00
---
---@private
---@param contents string[]
2024-02-27 08:20:32 -07:00
---@param opts? vim.lsp.util._normalize_markdown.Opts
2023-09-12 20:51:21 -07:00
---@return string[] table of lines containing normalized Markdown
---@see https://github.github.com/gfm
function M . _normalize_markdown ( contents , opts )
validate ( {
contents = { contents , ' t ' } ,
opts = { opts , ' t ' , true } ,
} )
opts = opts or { }
2023-09-18 11:04:01 -07:00
-- 1. Carriage returns are removed
contents = vim.split ( table.concat ( contents , ' \n ' ) : gsub ( ' \r ' , ' ' ) , ' \n ' , { trimempty = true } )
2023-09-12 20:51:21 -07:00
2023-09-18 11:04:01 -07:00
-- 2. Successive empty lines are collapsed into a single empty line
2023-09-12 20:51:21 -07:00
contents = collapse_blank_lines ( contents )
2023-09-18 11:04:01 -07:00
-- 3. Thematic breaks are expanded to the given width
2023-09-12 20:51:21 -07:00
local divider = string.rep ( ' ─ ' , opts.width or 80 )
contents = replace_separators ( contents , divider )
return contents
end
2021-12-15 07:53:09 -07:00
--- Closes the preview window
---
2023-03-06 23:17:52 -07:00
---@param winnr integer window id of preview window
2021-12-15 07:53:09 -07:00
---@param bufnrs table|nil optional list of ignored buffers
2022-07-09 09:40:32 -07:00
local function close_preview_window ( winnr , bufnrs )
2021-12-15 07:53:09 -07:00
vim.schedule ( function ( )
-- exit if we are in one of ignored buffers
2023-04-14 01:39:57 -07:00
if bufnrs and vim.list_contains ( bufnrs , api.nvim_get_current_buf ( ) ) then
2021-12-15 07:53:09 -07:00
return
end
local augroup = ' preview_window_ ' .. winnr
2022-07-15 09:26:47 -07:00
pcall ( api.nvim_del_augroup_by_name , augroup )
pcall ( api.nvim_win_close , winnr , true )
2021-12-15 07:53:09 -07:00
end )
end
2022-07-09 09:40:32 -07:00
--- Creates autocommands to close a preview window when events happen.
---
---@param events table list of events
2023-03-06 23:17:52 -07:00
---@param winnr integer window id of preview window
2022-07-09 09:40:32 -07:00
---@param bufnrs table list of buffers where the preview window will remain visible
2023-07-01 03:42:37 -07:00
---@see autocmd-events
2022-07-09 09:40:32 -07:00
local function close_preview_autocmd ( events , winnr , bufnrs )
2022-07-15 09:26:47 -07:00
local augroup = api.nvim_create_augroup ( ' preview_window_ ' .. winnr , {
2022-07-09 09:40:32 -07:00
clear = true ,
} )
-- close the preview window when entered a buffer that is not
-- the floating window buffer or the buffer that spawned it
2022-07-15 09:26:47 -07:00
api.nvim_create_autocmd ( ' BufEnter ' , {
2022-07-09 09:40:32 -07:00
group = augroup ,
callback = function ( )
close_preview_window ( winnr , bufnrs )
end ,
} )
if # events > 0 then
2022-07-15 09:26:47 -07:00
api.nvim_create_autocmd ( events , {
2022-07-17 16:11:41 -07:00
group = augroup ,
2022-07-09 09:40:32 -07:00
buffer = bufnrs [ 2 ] ,
callback = function ( )
close_preview_window ( winnr )
end ,
} )
end
end
2024-02-27 08:20:32 -07:00
---@private
2020-08-19 09:17:08 -07:00
--- Computes size of float needed to show contents (with optional wrapping)
2020-06-04 11:23:03 -07:00
---
2021-08-22 13:55:28 -07:00
---@param contents table of lines to show in window
2023-07-01 03:42:37 -07:00
---@param opts table with optional fields
2021-08-22 13:55:28 -07:00
--- - height of floating window
--- - width of floating window
--- - wrap_at character to wrap at for computing height
--- - max_width maximal width of floating window
--- - max_height maximal height of floating window
2023-07-01 03:42:37 -07:00
---@return integer width size of float
---@return integer height size of float
2020-06-04 11:23:03 -07:00
function M . _make_floating_popup_size ( contents , opts )
2019-11-13 13:55:26 -07:00
validate ( {
contents = { contents , ' t ' } ,
opts = { opts , ' t ' , true } ,
} )
2019-11-20 16:35:18 -07:00
opts = opts or { }
2019-11-20 12:34:10 -07:00
2019-11-20 16:35:18 -07:00
local width = opts.width
2020-06-04 11:23:03 -07:00
local height = opts.height
2020-07-05 18:09:52 -07:00
local wrap_at = opts.wrap_at
local max_width = opts.max_width
local max_height = opts.max_height
2020-06-04 11:23:03 -07:00
local line_widths = { }
2019-11-20 16:35:18 -07:00
if not width then
width = 0
for i , line in ipairs ( contents ) do
-- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
2023-10-12 00:39:39 -07:00
line_widths [ i ] = vim.fn . strdisplaywidth ( line : gsub ( ' %z ' , ' \n ' ) )
2020-06-04 11:23:03 -07:00
width = math.max ( line_widths [ i ] , width )
2019-11-13 13:55:26 -07:00
end
end
2021-04-08 10:32:05 -07:00
local border_width = get_border_size ( opts ) . width
2022-07-15 09:26:47 -07:00
local screen_width = api.nvim_win_get_width ( 0 )
2021-04-08 10:32:05 -07:00
width = math.min ( width , screen_width )
-- make sure borders are always inside the screen
if width + border_width > screen_width then
width = width - ( width + border_width - screen_width )
end
2021-04-14 04:01:14 -07:00
if wrap_at and wrap_at > width then
2021-04-08 10:32:05 -07:00
wrap_at = width
end
2020-07-05 18:09:52 -07:00
if max_width then
width = math.min ( width , max_width )
wrap_at = math.min ( wrap_at or max_width , max_width )
end
2019-11-13 13:55:26 -07:00
2020-06-04 11:23:03 -07:00
if not height then
height = # contents
2020-07-05 18:09:52 -07:00
if wrap_at and width >= wrap_at then
2020-06-04 11:23:03 -07:00
height = 0
if vim.tbl_isempty ( line_widths ) then
for _ , line in ipairs ( contents ) do
2023-10-12 00:39:39 -07:00
local line_width = vim.fn . strdisplaywidth ( line : gsub ( ' %z ' , ' \n ' ) )
2020-06-04 11:23:03 -07:00
height = height + math.ceil ( line_width / wrap_at )
end
else
for i = 1 , # contents do
2020-07-05 18:09:52 -07:00
height = height + math.max ( 1 , math.ceil ( line_widths [ i ] / wrap_at ) )
2020-06-04 11:23:03 -07:00
end
end
end
end
2020-07-05 18:09:52 -07:00
if max_height then
height = math.min ( height , max_height )
end
2020-06-04 11:23:03 -07:00
return width , height
end
2024-02-27 08:20:32 -07:00
--- @class vim.lsp.util.open_floating_preview.Opts
--- @inlinedoc
---
--- Height of floating window
--- @field height? integer
---
--- Width of floating window
--- @field width? integer
---
--- Wrap long lines
--- (default: `true`)
--- @field wrap? boolean
---
--- Character to wrap at for computing height when wrap is enabled
--- @field wrap_at? integer
---
--- Maximal width of floating window
--- @field max_width? integer
---
--- Maximal height of floating window
--- @field max_height? integer
---
--- If a popup with this id is opened, then focus it
--- @field focus_id? string
---
--- List of events that closes the floating window
--- @field close_events? table
---
--- Make float focusable.
--- (default: `true`)
--- @field focusable? boolean
---
--- If `true`, and if {focusable} is also `true`, focus an existing floating
--- window with the same {focus_id}
--- (default: `true`)
--- @field focus? boolean
2020-08-19 09:17:08 -07:00
--- Shows contents in a floating window.
2020-06-04 11:23:03 -07:00
---
2021-08-22 13:55:28 -07:00
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
2024-02-27 08:20:32 -07:00
---@param opts? vim.lsp.util.open_floating_preview.Opts with optional fields
--- (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()|
--- before they are passed on to |nvim_open_win()|)
2023-07-01 03:42:37 -07:00
---@return integer bufnr of newly created float window
2023-09-12 20:51:21 -07:00
---@return integer winid of newly created float window preview window
2021-03-19 12:55:58 -07:00
function M . open_floating_preview ( contents , syntax , opts )
2020-06-04 11:23:03 -07:00
validate ( {
contents = { contents , ' t ' } ,
2021-03-19 12:55:58 -07:00
syntax = { syntax , ' s ' , true } ,
2020-06-04 11:23:03 -07:00
opts = { opts , ' t ' , true } ,
} )
opts = opts or { }
2021-05-26 14:47:53 -07:00
opts.wrap = opts.wrap ~= false -- wrapping by default
2021-11-29 15:37:55 -07:00
opts.focus = opts.focus ~= false
2021-12-15 07:53:09 -07:00
opts.close_events = opts.close_events or { ' CursorMoved ' , ' CursorMovedI ' , ' InsertCharPre ' }
2021-05-26 14:47:53 -07:00
2022-07-15 09:26:47 -07:00
local bufnr = api.nvim_get_current_buf ( )
2021-05-26 14:47:53 -07:00
-- check if this popup is focusable and we need to focus
2021-11-29 15:37:55 -07:00
if opts.focus_id and opts.focusable ~= false and opts.focus then
2021-05-26 14:47:53 -07:00
-- Go back to previous window if we are in a focusable one
2022-07-15 09:26:47 -07:00
local current_winnr = api.nvim_get_current_win ( )
if npcall ( api.nvim_win_get_var , current_winnr , opts.focus_id ) then
api.nvim_command ( ' wincmd p ' )
2021-05-26 14:47:53 -07:00
return bufnr , current_winnr
end
do
local win = find_window_by_var ( opts.focus_id , bufnr )
2022-07-15 09:26:47 -07:00
if win and api.nvim_win_is_valid ( win ) and vim.fn . pumvisible ( ) == 0 then
2021-05-26 14:47:53 -07:00
-- focus and return the existing buf, win
2022-07-15 09:26:47 -07:00
api.nvim_set_current_win ( win )
api.nvim_command ( ' stopinsert ' )
return api.nvim_win_get_buf ( win ) , win
2021-05-26 14:47:53 -07:00
end
end
end
2021-06-05 01:13:01 -07:00
-- check if another floating preview already exists for this buffer
-- and close it if needed
2022-07-15 09:26:47 -07:00
local existing_float = npcall ( api.nvim_buf_get_var , bufnr , ' lsp_floating_preview ' )
if existing_float and api.nvim_win_is_valid ( existing_float ) then
api.nvim_win_close ( existing_float , true )
2021-06-05 01:13:01 -07:00
end
2023-09-12 20:51:21 -07:00
-- Create the buffer
2022-07-15 09:26:47 -07:00
local floating_bufnr = api.nvim_create_buf ( false , true )
2020-06-04 11:23:03 -07:00
2023-09-12 20:51:21 -07:00
-- Set up the contents, using treesitter for markdown
local do_stylize = syntax == ' markdown ' and vim.g . syntax_on ~= nil
2021-05-26 14:47:53 -07:00
if do_stylize then
2023-09-12 20:51:21 -07:00
local width = M._make_floating_popup_size ( contents , opts )
contents = M._normalize_markdown ( contents , { width = width } )
vim.bo [ floating_bufnr ] . filetype = ' markdown '
vim.treesitter . start ( floating_bufnr )
api.nvim_buf_set_lines ( floating_bufnr , 0 , - 1 , false , contents )
2021-05-26 14:47:53 -07:00
else
2023-09-18 11:04:01 -07:00
-- Clean up input: trim empty lines
contents = vim.split ( table.concat ( contents , ' \n ' ) , ' \n ' , { trimempty = true } )
2023-09-12 20:51:21 -07:00
2021-05-26 14:47:53 -07:00
if syntax then
2022-12-19 09:37:45 -07:00
vim.bo [ floating_bufnr ] . syntax = syntax
2021-05-26 14:47:53 -07:00
end
2022-07-15 09:26:47 -07:00
api.nvim_buf_set_lines ( floating_bufnr , 0 , - 1 , true , contents )
2021-05-26 14:47:53 -07:00
end
2020-06-04 11:23:03 -07:00
-- Compute size of float needed to show (wrapped) lines
2021-05-26 14:47:53 -07:00
if opts.wrap then
2022-07-15 09:26:47 -07:00
opts.wrap_at = opts.wrap_at or api.nvim_win_get_width ( 0 )
2021-05-26 14:47:53 -07:00
else
opts.wrap_at = nil
end
2020-06-04 11:23:03 -07:00
local width , height = M._make_floating_popup_size ( contents , opts )
2019-11-13 13:55:26 -07:00
local float_option = M.make_floating_popup_options ( width , height , opts )
2022-07-15 09:26:47 -07:00
local floating_winnr = api.nvim_open_win ( floating_bufnr , false , float_option )
2023-09-12 20:51:21 -07:00
2021-05-26 14:47:53 -07:00
if do_stylize then
2022-12-19 09:37:45 -07:00
vim.wo [ floating_winnr ] . conceallevel = 2
2019-11-13 13:55:26 -07:00
end
2021-05-26 14:47:53 -07:00
-- disable folding
2022-12-19 09:37:45 -07:00
vim.wo [ floating_winnr ] . foldenable = false
2021-05-26 14:47:53 -07:00
-- soft wrapping
2022-12-19 09:37:45 -07:00
vim.wo [ floating_winnr ] . wrap = opts.wrap
2021-05-26 14:47:53 -07:00
2022-12-19 09:37:45 -07:00
vim.bo [ floating_bufnr ] . modifiable = false
vim.bo [ floating_bufnr ] . bufhidden = ' wipe '
2023-09-12 20:51:21 -07:00
2022-07-15 09:26:47 -07:00
api.nvim_buf_set_keymap (
2021-11-24 15:57:33 -07:00
floating_bufnr ,
' n ' ,
' q ' ,
' <cmd>bdelete<cr> ' ,
{ silent = true , noremap = true , nowait = true }
)
2021-12-15 07:53:09 -07:00
close_preview_autocmd ( opts.close_events , floating_winnr , { floating_bufnr , bufnr } )
2021-05-26 14:47:53 -07:00
-- save focus_id
if opts.focus_id then
2022-07-15 09:26:47 -07:00
api.nvim_win_set_var ( floating_winnr , opts.focus_id , bufnr )
2021-05-26 14:47:53 -07:00
end
2022-07-15 09:26:47 -07:00
api.nvim_buf_set_var ( bufnr , ' lsp_floating_preview ' , floating_winnr )
2021-05-26 14:47:53 -07:00
2019-11-13 13:55:26 -07:00
return floating_bufnr , floating_winnr
end
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
2020-11-12 20:21:34 -07:00
do --[[ References ]]
2022-07-15 09:26:47 -07:00
local reference_ns = api.nvim_create_namespace ( ' vim_lsp_references ' )
2019-11-13 13:55:26 -07:00
2020-08-19 09:17:08 -07:00
--- Removes document highlights from a buffer.
---
2023-07-04 05:30:31 -07:00
---@param bufnr integer|nil Buffer id
2020-02-26 12:10:16 -07:00
function M . buf_clear_references ( bufnr )
2023-07-04 05:30:31 -07:00
validate ( { bufnr = { bufnr , { ' n ' } , true } } )
2022-07-15 09:26:47 -07:00
api.nvim_buf_clear_namespace ( bufnr or 0 , reference_ns , 0 , - 1 )
2020-02-26 12:10:16 -07:00
end
2020-08-19 09:17:08 -07:00
--- Shows a list of document highlights for a certain buffer.
---
2023-03-06 23:17:52 -07:00
---@param bufnr integer Buffer id
2021-11-29 22:11:33 -07:00
---@param references table List of `DocumentHighlight` objects to highlight
2022-01-13 03:34:04 -07:00
---@param offset_encoding string One of "utf-8", "utf-16", "utf-32".
2023-10-16 08:13:37 -07:00
---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
2021-11-17 13:14:45 -07:00
function M . buf_highlight_references ( bufnr , references , offset_encoding )
2022-01-13 03:34:04 -07:00
validate ( {
bufnr = { bufnr , ' n ' , true } ,
offset_encoding = { offset_encoding , ' string ' , false } ,
} )
2020-02-26 12:10:16 -07:00
for _ , reference in ipairs ( references ) do
2021-11-06 15:05:52 -07:00
local start_line , start_char =
reference [ ' range ' ] [ ' start ' ] [ ' line ' ] , reference [ ' range ' ] [ ' start ' ] [ ' character ' ]
local end_line , end_char =
reference [ ' range ' ] [ ' end ' ] [ ' line ' ] , reference [ ' range ' ] [ ' end ' ] [ ' character ' ]
2021-11-17 13:14:45 -07:00
local start_idx = get_line_byte_from_position (
bufnr ,
{ line = start_line , character = start_char } ,
offset_encoding
)
local end_idx = get_line_byte_from_position (
bufnr ,
{ line = start_line , character = end_char } ,
offset_encoding
)
2021-11-06 15:05:52 -07:00
2020-02-26 12:10:16 -07:00
local document_highlight_kind = {
[ protocol.DocumentHighlightKind . Text ] = ' LspReferenceText ' ,
[ protocol.DocumentHighlightKind . Read ] = ' LspReferenceRead ' ,
[ protocol.DocumentHighlightKind . Write ] = ' LspReferenceWrite ' ,
}
2020-04-16 09:30:03 -07:00
local kind = reference [ ' kind ' ] or protocol.DocumentHighlightKind . Text
2021-11-06 15:05:52 -07:00
highlight.range (
bufnr ,
reference_ns ,
document_highlight_kind [ kind ] ,
{ start_line , start_idx } ,
2022-01-15 15:19:20 -07:00
{ end_line , end_idx } ,
2022-02-21 13:21:42 -07:00
{ priority = vim.highlight . priorities.user }
)
2020-02-26 12:10:16 -07:00
end
end
2019-11-13 13:55:26 -07:00
end
2019-11-24 04:01:18 -07:00
local position_sort = sort_by_key ( function ( v )
2020-01-28 02:45:25 -07:00
return { v.start . line , v.start . character }
2019-11-24 04:01:18 -07:00
end )
2019-11-13 13:55:26 -07:00
2024-02-27 08:20:32 -07:00
---@class vim.lsp.util.locations_to_items.ret
---@inlinedoc
2023-12-22 03:38:02 -07:00
---@field filename string
---@field lnum integer 1-indexed line number
---@field col integer 1-indexed column
---@field text string
---@field user_data lsp.Location|lsp.LocationLink
2020-08-19 09:17:08 -07:00
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
2023-09-07 01:12:02 -07:00
--- The `user_data` field of each resulting item will contain the original
--- `Location` or `LocationLink` it was computed from.
---
2021-09-06 19:21:18 -07:00
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
2023-12-22 03:38:02 -07:00
---@param locations lsp.Location[]|lsp.LocationLink[]
2022-01-13 03:34:04 -07:00
---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
2023-07-01 03:42:37 -07:00
--- default to first client of buffer
2024-02-27 08:20:32 -07:00
---@return vim.lsp.util.locations_to_items.ret[]
2022-01-13 03:34:04 -07:00
function M . locations_to_items ( locations , offset_encoding )
if offset_encoding == nil then
vim.notify_once (
' locations_to_items must be called with valid offset encoding ' ,
vim.log . levels.WARN
)
2023-07-17 09:27:16 -07:00
offset_encoding = vim.lsp . get_clients ( { bufnr = 0 } ) [ 1 ] . offset_encoding
2022-01-13 03:34:04 -07:00
end
2019-11-13 13:55:26 -07:00
local items = { }
2023-12-22 03:38:02 -07:00
---@type table<string, {start: lsp.Position, location: lsp.Location|lsp.LocationLink}[]>
2019-11-24 04:01:18 -07:00
local grouped = setmetatable ( { } , {
__index = function ( t , k )
local v = { }
rawset ( t , k , v )
return v
end ,
} )
2019-11-13 13:55:26 -07:00
for _ , d in ipairs ( locations ) do
2020-05-02 06:21:07 -07:00
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
2023-09-07 01:12:02 -07:00
table.insert ( grouped [ uri ] , { start = range.start , location = d } )
2019-11-24 04:01:18 -07:00
end
2020-01-28 02:45:25 -07:00
2023-12-22 03:38:02 -07:00
---@type string[]
2019-11-24 04:01:18 -07:00
local keys = vim.tbl_keys ( grouped )
table.sort ( keys )
-- TODO(ashkan) I wish we could do this lazily.
2020-06-30 08:48:04 -07:00
for _ , uri in ipairs ( keys ) do
local rows = grouped [ uri ]
2019-11-24 04:01:18 -07:00
table.sort ( rows , position_sort )
2020-06-30 08:48:04 -07:00
local filename = vim.uri_to_fname ( uri )
2021-05-25 03:09:37 -07:00
2023-12-22 03:38:02 -07:00
local line_numbers = { }
2021-05-25 03:09:37 -07:00
for _ , temp in ipairs ( rows ) do
2023-12-22 03:38:02 -07:00
table.insert ( line_numbers , temp.start . line )
2021-05-25 03:09:37 -07:00
end
-- get all the lines for this uri
2023-12-22 03:38:02 -07:00
local lines = get_lines ( vim.uri_to_bufnr ( uri ) , line_numbers )
2021-05-25 03:09:37 -07:00
2020-06-30 08:48:04 -07:00
for _ , temp in ipairs ( rows ) do
local pos = temp.start
local row = pos.line
2021-05-25 03:09:37 -07:00
local line = lines [ row ] or ' '
2022-01-13 03:34:04 -07:00
local col = M._str_byteindex_enc ( line , pos.character , offset_encoding )
2020-06-30 08:48:04 -07:00
table.insert ( items , {
filename = filename ,
lnum = row + 1 ,
col = col + 1 ,
text = line ,
2023-09-07 01:12:02 -07:00
user_data = temp.location ,
2020-06-30 08:48:04 -07:00
} )
2019-11-24 04:01:18 -07:00
end
2019-11-13 13:55:26 -07:00
end
2019-11-24 04:01:18 -07:00
return items
end
2020-05-07 13:23:25 -07:00
-- According to LSP spec, if the client set "symbolKind.valueSet",
-- the client must handle it properly even if it receives a value outside the specification.
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol
function M . _get_symbol_kind_name ( symbol_kind )
return protocol.SymbolKind [ symbol_kind ] or ' Unknown '
end
2020-08-19 09:17:08 -07:00
--- Converts symbols to quickfix list items.
2020-02-22 05:20:38 -07:00
---
2023-07-01 03:42:37 -07:00
---@param symbols table DocumentSymbol[] or SymbolInformation[]
2024-02-15 10:16:04 -07:00
---@param bufnr integer
2020-02-22 05:20:38 -07:00
function M . symbols_to_items ( symbols , bufnr )
local function _symbols_to_items ( _symbols , _items , _bufnr )
for _ , symbol in ipairs ( _symbols ) do
if symbol.location then -- SymbolInformation type
local range = symbol.location . range
2020-05-07 13:23:25 -07:00
local kind = M._get_symbol_kind_name ( symbol.kind )
2020-02-22 05:20:38 -07:00
table.insert ( _items , {
filename = vim.uri_to_fname ( symbol.location . uri ) ,
lnum = range.start . line + 1 ,
col = range.start . character + 1 ,
kind = kind ,
text = ' [ ' .. kind .. ' ] ' .. symbol.name ,
} )
2021-04-01 05:16:01 -07:00
elseif symbol.selectionRange then -- DocumentSymbole type
2020-05-07 13:23:25 -07:00
local kind = M._get_symbol_kind_name ( symbol.kind )
2020-02-22 05:20:38 -07:00
table.insert ( _items , {
-- bufnr = _bufnr,
2022-07-15 09:26:47 -07:00
filename = api.nvim_buf_get_name ( _bufnr ) ,
2021-04-01 05:16:01 -07:00
lnum = symbol.selectionRange . start.line + 1 ,
col = symbol.selectionRange . start.character + 1 ,
2020-02-22 05:20:38 -07:00
kind = kind ,
text = ' [ ' .. kind .. ' ] ' .. symbol.name ,
} )
if symbol.children then
2020-05-01 23:08:52 -07:00
for _ , v in ipairs ( _symbols_to_items ( symbol.children , _items , _bufnr ) ) do
2021-10-31 15:18:51 -07:00
for _ , s in ipairs ( v ) do
table.insert ( _items , s )
end
2020-02-22 05:20:38 -07:00
end
end
end
end
return _items
end
2022-01-03 06:48:01 -07:00
return _symbols_to_items ( symbols , { } , bufnr or 0 )
2020-02-22 05:20:38 -07:00
end
2020-08-19 09:17:08 -07:00
--- Removes empty lines from the beginning and end.
2023-09-18 11:04:01 -07:00
---@deprecated use `vim.split()` with `trimempty` instead
2023-07-01 03:42:37 -07:00
---@param lines table list of lines to trim
---@return table trimmed list of lines
2019-11-20 16:35:18 -07:00
function M . trim_empty_lines ( lines )
2023-12-25 13:28:28 -07:00
vim.deprecate ( ' vim.lsp.util.trim_empty_lines() ' , ' vim.split() with `trimempty` ' , ' 0.12 ' )
2019-11-20 16:35:18 -07:00
local start = 1
for i = 1 , # lines do
2021-08-10 11:52:30 -07:00
if lines [ i ] ~= nil and # lines [ i ] > 0 then
2019-11-20 16:35:18 -07:00
start = i
break
end
end
local finish = 1
for i = # lines , 1 , - 1 do
2021-08-10 11:52:30 -07:00
if lines [ i ] ~= nil and # lines [ i ] > 0 then
2019-11-20 16:35:18 -07:00
finish = i
break
end
end
2022-07-09 16:57:35 -07:00
return list_extend ( { } , lines , start , finish )
2019-11-20 16:35:18 -07:00
end
2020-08-19 09:17:08 -07:00
--- Accepts markdown lines and tries to reduce them to a filetype if they
--- comprise just a single code block.
---
--- CAUTION: Modifies the input in-place!
---
2023-09-29 08:37:14 -07:00
---@deprecated
2023-07-01 03:42:37 -07:00
---@param lines table list of lines
---@return string filetype or "markdown" if it was unchanged.
2019-11-20 16:35:18 -07:00
function M . try_trim_markdown_code_blocks ( lines )
2023-12-25 13:28:28 -07:00
vim.deprecate ( ' vim.lsp.util.try_trim_markdown_code_blocks() ' , ' nil ' , ' 0.12 ' )
2019-11-20 16:35:18 -07:00
local language_id = lines [ 1 ] : match ( ' ^```(.*) ' )
if language_id then
local has_inner_code_fence = false
for i = 2 , ( # lines - 1 ) do
local line = lines [ i ]
if line : sub ( 1 , 3 ) == ' ``` ' then
has_inner_code_fence = true
break
end
end
-- No inner code fences + starting with code fence = hooray.
if not has_inner_code_fence then
table.remove ( lines , 1 )
table.remove ( lines )
return language_id
end
end
return ' markdown '
end
2023-03-06 23:17:52 -07:00
---@param window integer|nil: window handle or 0 for current, defaults to current
2021-12-10 07:17:50 -07:00
---@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
local function make_position_param ( window , offset_encoding )
window = window or 0
2022-07-15 09:26:47 -07:00
local buf = api.nvim_win_get_buf ( window )
local row , col = unpack ( api.nvim_win_get_cursor ( window ) )
2021-12-10 07:17:50 -07:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
2019-11-21 16:41:32 -07:00
row = row - 1
2022-07-15 09:26:47 -07:00
local line = api.nvim_buf_get_lines ( buf , row , row + 1 , true ) [ 1 ]
2020-09-01 20:45:47 -07:00
if not line then
return { line = 0 , character = 0 }
end
2021-12-10 07:17:50 -07:00
col = _str_utfindex_enc ( line , col , offset_encoding )
2020-05-15 16:18:59 -07:00
return { line = row , character = col }
end
2020-08-19 09:17:08 -07:00
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
2023-03-06 23:17:52 -07:00
---@param window integer|nil: window handle or 0 for current, defaults to current
2023-01-04 04:48:41 -07:00
---@param offset_encoding string|nil utf-8|utf-16|utf-32|nil defaults to `offset_encoding` of first client of buffer of `window`
2023-07-01 03:42:37 -07:00
---@return table `TextDocumentPositionParams` object
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
2021-12-10 07:17:50 -07:00
function M . make_position_params ( window , offset_encoding )
window = window or 0
2022-07-15 09:26:47 -07:00
local buf = api.nvim_win_get_buf ( window )
2021-12-10 07:17:50 -07:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
2019-11-21 16:41:32 -07:00
return {
2021-12-10 07:17:50 -07:00
textDocument = M.make_text_document_params ( buf ) ,
position = make_position_param ( window , offset_encoding ) ,
}
end
--- Utility function for getting the encoding of the first LSP client on the given buffer.
2023-03-06 23:17:52 -07:00
---@param bufnr (integer) buffer handle or 0 for current, defaults to current
2023-07-01 03:42:37 -07:00
---@return string encoding first client if there is one, nil otherwise
2021-12-10 07:17:50 -07:00
function M . _get_offset_encoding ( bufnr )
validate ( {
bufnr = { bufnr , ' n ' , true } ,
2020-05-15 16:18:59 -07:00
} )
2021-12-10 07:17:50 -07:00
local offset_encoding
2023-07-17 09:27:16 -07:00
for _ , client in pairs ( vim.lsp . get_clients ( { bufnr = bufnr } ) ) do
2022-01-13 03:34:04 -07:00
if client.offset_encoding == nil then
vim.notify_once (
string.format (
' Client (id: %s) offset_encoding is nil. Do not unset offset_encoding. ' ,
client.id
) ,
vim.log . levels.ERROR
)
end
local this_offset_encoding = client.offset_encoding
2021-12-10 07:17:50 -07:00
if not offset_encoding then
offset_encoding = this_offset_encoding
elseif offset_encoding ~= this_offset_encoding then
2023-07-24 08:58:59 -07:00
vim.notify_once (
2021-12-10 07:17:50 -07:00
' warning: multiple different client offset_encodings detected for buffer, this is not supported yet ' ,
vim.log . levels.WARN
)
end
end
return offset_encoding
2020-05-15 16:18:59 -07:00
end
2020-08-19 09:17:08 -07:00
--- Using the current position in the current buffer, creates an object that
--- can be used as a building block for several LSP requests, such as
--- `textDocument/codeAction`, `textDocument/colorPresentation`,
--- `textDocument/rangeFormatting`.
---
2023-03-06 23:17:52 -07:00
---@param window integer|nil: window handle or 0 for current, defaults to current
2022-07-28 10:19:07 -07:00
---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of buffer of `window`
2023-07-01 03:42:37 -07:00
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
2020-08-19 09:17:08 -07:00
---`current_position`, end = `current_position` } }
2021-12-10 07:17:50 -07:00
function M . make_range_params ( window , offset_encoding )
2022-07-15 09:26:47 -07:00
local buf = api.nvim_win_get_buf ( window or 0 )
2021-12-10 07:17:50 -07:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
local position = make_position_param ( window , offset_encoding )
2020-05-15 16:18:59 -07:00
return {
2021-12-10 07:17:50 -07:00
textDocument = M.make_text_document_params ( buf ) ,
2020-05-15 16:18:59 -07:00
range = { start = position , [ ' end ' ] = position } ,
2019-11-21 16:41:32 -07:00
}
end
2020-09-24 12:53:08 -07:00
--- Using the given range in the current buffer, creates an object that
--- is similar to |vim.lsp.util.make_range_params()|.
---
2023-06-24 04:47:10 -07:00
---@param start_pos integer[]|nil {row,col} mark-indexed position.
2022-07-28 10:19:07 -07:00
--- Defaults to the start of the last visual selection.
2023-06-24 04:47:10 -07:00
---@param end_pos integer[]|nil {row,col} mark-indexed position.
2022-07-28 10:19:07 -07:00
--- Defaults to the end of the last visual selection.
2023-03-06 23:17:52 -07:00
---@param bufnr integer|nil buffer handle or 0 for current, defaults to current
2022-07-28 10:19:07 -07:00
---@param offset_encoding "utf-8"|"utf-16"|"utf-32"|nil defaults to `offset_encoding` of first client of `bufnr`
2023-07-01 03:42:37 -07:00
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
2020-09-24 12:53:08 -07:00
---`start_position`, end = `end_position` } }
2021-12-10 07:17:50 -07:00
function M . make_given_range_params ( start_pos , end_pos , bufnr , offset_encoding )
2020-09-24 12:53:08 -07:00
validate ( {
start_pos = { start_pos , ' t ' , true } ,
end_pos = { end_pos , ' t ' , true } ,
2021-12-10 07:17:50 -07:00
offset_encoding = { offset_encoding , ' s ' , true } ,
2020-09-24 12:53:08 -07:00
} )
2022-07-15 09:26:47 -07:00
bufnr = bufnr or api.nvim_get_current_buf ( )
2021-12-10 07:17:50 -07:00
offset_encoding = offset_encoding or M._get_offset_encoding ( bufnr )
2022-07-15 09:26:47 -07:00
local A = list_extend ( { } , start_pos or api.nvim_buf_get_mark ( bufnr , ' < ' ) )
local B = list_extend ( { } , end_pos or api.nvim_buf_get_mark ( bufnr , ' > ' ) )
2020-09-24 12:53:08 -07:00
-- convert to 0-index
A [ 1 ] = A [ 1 ] - 1
B [ 1 ] = B [ 1 ] - 1
2021-12-10 07:17:50 -07:00
-- account for offset_encoding.
2020-09-24 12:53:08 -07:00
if A [ 2 ] > 0 then
2021-12-10 07:17:50 -07:00
A = { A [ 1 ] , M.character_offset ( bufnr , A [ 1 ] , A [ 2 ] , offset_encoding ) }
2020-09-24 12:53:08 -07:00
end
if B [ 2 ] > 0 then
2021-12-10 07:17:50 -07:00
B = { B [ 1 ] , M.character_offset ( bufnr , B [ 1 ] , B [ 2 ] , offset_encoding ) }
2020-09-24 12:53:08 -07:00
end
2021-05-14 09:33:23 -07:00
-- we need to offset the end character position otherwise we loose the last
-- character of the selection, as LSP end position is exclusive
-- see https://microsoft.github.io/language-server-protocol/specification#range
if vim.o . selection ~= ' exclusive ' then
B [ 2 ] = B [ 2 ] + 1
end
2020-09-24 12:53:08 -07:00
return {
2021-12-10 07:17:50 -07:00
textDocument = M.make_text_document_params ( bufnr ) ,
2020-09-24 12:53:08 -07:00
range = {
start = { line = A [ 1 ] , character = A [ 2 ] } ,
[ ' end ' ] = { line = B [ 1 ] , character = B [ 2 ] } ,
} ,
}
end
2020-08-19 09:17:08 -07:00
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
2023-03-06 23:17:52 -07:00
---@param bufnr integer|nil: Buffer handle, defaults to current
2023-07-01 03:42:37 -07:00
---@return table `TextDocumentIdentifier`
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
2021-12-10 07:17:50 -07:00
function M . make_text_document_params ( bufnr )
2023-03-10 23:35:23 -07:00
return { uri = vim.uri_from_bufnr ( bufnr or 0 ) }
2020-02-22 05:20:38 -07:00
end
2020-12-08 19:09:33 -07:00
--- Create the workspace params
2023-07-01 03:42:37 -07:00
---@param added table
---@param removed table
2020-11-25 13:07:02 -07:00
function M . make_workspace_params ( added , removed )
return { event = { added = added , removed = removed } }
end
2023-08-31 01:00:24 -07:00
2022-03-20 10:41:46 -07:00
--- Returns indentation size.
2020-05-05 08:23:45 -07:00
---
2022-09-25 16:58:27 -07:00
---@see 'shiftwidth'
2023-03-06 23:17:52 -07:00
---@param bufnr (integer|nil): Buffer handle, defaults to current
2023-07-01 03:42:37 -07:00
---@return (integer) indentation size
2020-05-05 08:23:45 -07:00
function M . get_effective_tabstop ( bufnr )
validate ( { bufnr = { bufnr , ' n ' , true } } )
local bo = bufnr and vim.bo [ bufnr ] or vim.bo
2022-03-20 10:41:46 -07:00
local sw = bo.shiftwidth
return ( sw == 0 and bo.tabstop ) or sw
2020-05-05 08:23:45 -07:00
end
2021-07-17 02:15:57 -07:00
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
2020-08-19 09:17:08 -07:00
---
2022-04-30 06:36:40 -07:00
---@param options table|nil with valid `FormattingOptions` entries
2023-12-13 05:00:11 -07:00
---@return lsp.DocumentFormattingParams object
2021-08-22 13:55:28 -07:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
2020-05-05 08:23:45 -07:00
function M . make_formatting_params ( options )
validate ( { options = { options , ' t ' , true } } )
options = vim.tbl_extend ( ' keep ' , options or { } , {
tabSize = M.get_effective_tabstop ( ) ,
insertSpaces = vim.bo . expandtab ,
} )
return {
2023-03-10 23:35:23 -07:00
textDocument = { uri = vim.uri_from_bufnr ( 0 ) } ,
2020-05-05 08:23:45 -07:00
options = options ,
}
end
2020-08-19 09:17:08 -07:00
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
---
2023-03-06 23:17:52 -07:00
---@param buf integer buffer number (0 for current)
2024-01-09 10:36:46 -07:00
---@param row integer 0-indexed line
---@param col integer 0-indexed byte offset in line
2023-07-01 03:42:37 -07:00
---@param offset_encoding string utf-8|utf-16|utf-32 defaults to `offset_encoding` of first client of `buf`
---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
2021-12-10 07:17:50 -07:00
function M . character_offset ( buf , row , col , offset_encoding )
local line = get_line ( buf , row )
2022-01-13 03:34:04 -07:00
if offset_encoding == nil then
vim.notify_once (
' character_offset must be called with valid offset encoding ' ,
vim.log . levels.WARN
)
2023-07-17 09:27:16 -07:00
offset_encoding = vim.lsp . get_clients ( { bufnr = buf } ) [ 1 ] . offset_encoding
2022-01-13 03:34:04 -07:00
end
2019-11-22 00:58:32 -07:00
-- If the col is past the EOL, use the line length.
2019-11-22 01:31:10 -07:00
if col > # line then
2021-12-10 07:17:50 -07:00
return _str_utfindex_enc ( line , nil , offset_encoding )
2019-11-22 01:31:10 -07:00
end
2021-12-10 07:17:50 -07:00
return _str_utfindex_enc ( line , col , offset_encoding )
2019-11-21 17:23:12 -07:00
end
2019-11-20 16:35:18 -07:00
2020-12-30 17:05:10 -07:00
--- Helper function to return nested values in language server settings
---
2023-07-01 03:42:37 -07:00
---@param settings table language server settings
---@param section string indicating the field of the settings table
2024-01-14 20:37:07 -07:00
---@return table|string|vim.NIL The value of settings accessed via section. `vim.NIL` if not found.
2024-01-14 21:12:54 -07:00
---@deprecated
2020-12-30 17:05:10 -07:00
function M . lookup_section ( settings , section )
2024-01-14 21:12:54 -07:00
vim.deprecate ( ' vim.lsp.util.lookup_section() ' , ' vim.tbl_get() with `vim.split` ' , ' 0.12 ' )
2023-07-01 03:42:37 -07:00
for part in vim.gsplit ( section , ' . ' , { plain = true } ) do
2020-12-30 17:05:10 -07:00
settings = settings [ part ]
2022-04-15 02:12:41 -07:00
if settings == nil then
return vim.NIL
2020-12-30 17:05:10 -07:00
end
end
return settings
end
2023-09-21 03:06:40 -07:00
--- Converts line range (0-based, end-inclusive) to lsp range,
--- handles absence of a trailing newline
---
---@param bufnr integer
---@param start_line integer
---@param end_line integer
---@param offset_encoding lsp.PositionEncodingKind
---@return lsp.Range
local function make_line_range_params ( bufnr , start_line , end_line , offset_encoding )
local last_line = api.nvim_buf_line_count ( bufnr ) - 1
---@type lsp.Position
local end_pos
if end_line == last_line and not vim.api . nvim_get_option_value ( ' endofline ' , { buf = bufnr } ) then
end_pos = {
line = end_line ,
character = M.character_offset ( bufnr , end_line , # get_line ( bufnr , end_line ) , offset_encoding ) ,
}
else
end_pos = { line = end_line + 1 , character = 0 }
end
return {
start = { line = start_line , character = 0 } ,
[ ' end ' ] = end_pos ,
}
end
2024-02-27 08:20:32 -07:00
---@class (private) vim.lsp.util._refresh.Opts
2023-08-31 01:00:24 -07:00
---@field bufnr integer? Buffer to refresh (default: 0)
---@field only_visible? boolean Whether to only refresh for the visible regions of the buffer (default: false)
---@field client_id? integer Client ID to refresh (default: all clients)
2024-02-27 08:20:32 -07:00
---@private
--- Request updated LSP information for a buffer.
---
2023-07-20 00:03:48 -07:00
---@param method string LSP method to call
2024-02-27 08:20:32 -07:00
---@param opts? vim.lsp.util._refresh.Opts Options table
2023-07-20 00:03:48 -07:00
function M . _refresh ( method , opts )
opts = opts or { }
local bufnr = opts.bufnr
if bufnr == nil or bufnr == 0 then
bufnr = api.nvim_get_current_buf ( )
end
2023-08-31 01:00:24 -07:00
local clients = vim.lsp . get_clients ( { bufnr = bufnr , method = method , id = opts.client_id } )
if # clients == 0 then
return
end
2023-09-21 03:06:40 -07:00
local textDocument = M.make_text_document_params ( bufnr )
2023-07-20 00:03:48 -07:00
local only_visible = opts.only_visible or false
2023-08-31 01:00:24 -07:00
if only_visible then
for _ , window in ipairs ( api.nvim_list_wins ( ) ) do
if api.nvim_win_get_buf ( window ) == bufnr then
local first = vim.fn . line ( ' w0 ' , window )
local last = vim.fn . line ( ' w$ ' , window )
for _ , client in ipairs ( clients ) do
2023-09-21 03:06:40 -07:00
client.request ( method , {
textDocument = textDocument ,
range = make_line_range_params ( bufnr , first - 1 , last - 1 , client.offset_encoding ) ,
} , nil , bufnr )
2023-08-31 01:00:24 -07:00
end
end
2023-07-20 00:03:48 -07:00
end
2023-08-31 01:00:24 -07:00
else
for _ , client in ipairs ( clients ) do
2023-09-21 03:06:40 -07:00
client.request ( method , {
textDocument = textDocument ,
range = make_line_range_params (
bufnr ,
0 ,
api.nvim_buf_line_count ( bufnr ) - 1 ,
client.offset_encoding
) ,
} , nil , bufnr )
2023-08-31 01:00:24 -07:00
end
2023-07-20 00:03:48 -07:00
end
end
lsp: vim.lsp.diagnostic (#12655)
Breaking Changes:
- Deprecated all `vim.lsp.util.{*diagnostics*}()` functions.
- Instead, all functions must be found in vim.lsp.diagnostic
- For now, they issue a warning ONCE per neovim session. In a
"little while" we will remove them completely.
- `vim.lsp.callbacks` has moved to `vim.lsp.handlers`.
- For a "little while" we will just redirect `vim.lsp.callbacks` to
`vim.lsp.handlers`. However, we will remove this at some point, so
it is recommended that you change all of your references to
`callbacks` into `handlers`.
- This also means that for functions like |vim.lsp.start_client()|
and similar, keyword style arguments have moved from "callbacks"
to "handlers". Once again, these are currently being forward, but
will cease to be forwarded in a "little while".
- Changed the highlight groups for LspDiagnostic highlight as they were
inconsistently named.
- For more information, see |lsp-highlight-diagnostics|
- Changed the sign group names as well, to be consistent with
|lsp-highlight-diagnostics|
General Enhancements:
- Rewrote much of the getting started help document for lsp. It also
provides a much nicer configuration strategy, so as to not recommend
globally overwriting builtin neovim mappings.
LSP Enhancements:
- Introduced the concept of |lsp-handlers| which will allow much better
customization for users without having to copy & paste entire files /
functions / etc.
Diagnostic Enhancements:
- "goto next diagnostic" |vim.lsp.diagnostic.goto_next()|
- "goto prev diagnostic" |vim.lsp.diagnostic.goto_prev()|
- For each of the gotos, auto open diagnostics is available as a
configuration option
- Configurable diagnostic handling:
- See |vim.lsp.diagnostic.on_publish_diagnostics()|
- Delay display until after insert mode
- Configure signs
- Configure virtual text
- Configure underline
- Set the location list with the buffers diagnostics.
- See |vim.lsp.diagnostic.set_loclist()|
- Better performance for getting counts and line diagnostics
- They are now cached on save, to enhance lookups.
- Particularly useful for checking in statusline, etc.
- Actual testing :)
- See ./test/functional/plugin/lsp/diagnostic_spec.lua
- Added `guisp` for underline highlighting
NOTE: "a little while" means enough time to feel like most plugins and
plugin authors have had a chance to refactor their code to use the
updated calls. Then we will remove them completely. There is no need to
keep them, because we don't have any released version of neovim that
exposes these APIs. I'm trying to be nice to people following HEAD :)
Co-authored: [Twitch Chat 2020](https://twitch.tv/teej_dv)
2020-11-12 20:21:34 -07:00
M._get_line_byte_from_position = get_line_byte_from_position
2023-07-18 07:42:30 -07:00
---@nodoc
2023-12-13 05:00:11 -07:00
M.buf_versions = { } ---@type table<integer,integer>
2020-01-24 04:31:52 -07:00
2019-11-13 13:55:26 -07:00
return M