2019-11-13 12:55:26 -08:00
local protocol = require ( 'vim.lsp.protocol' )
local validate = vim.validate
2022-07-15 18:26:47 +02:00
local api = vim.api
2019-11-26 05:59:40 -08:00
local list_extend = vim.list_extend
2024-01-22 18:23:28 +01:00
local highlight = vim.highlight
2023-06-03 11:06:00 +01:00
local uv = vim.uv
2019-11-13 12:55:26 -08:00
local M = {}
2021-04-08 19:32:05 +02:00
local default_border = {
{ '' , 'NormalFloat' },
{ '' , 'NormalFloat' },
{ '' , 'NormalFloat' },
{ ' ' , 'NormalFloat' },
{ '' , 'NormalFloat' },
{ '' , 'NormalFloat' },
{ '' , 'NormalFloat' },
{ ' ' , 'NormalFloat' },
}
2024-10-16 12:49:14 +01:00
--- @param border string|(string|[string,string])[]
local function border_error ( border )
error (
string.format (
'invalid floating preview border: %s. :help vim.api.nvim_open_win()' ,
vim.inspect ( border )
),
2
)
end
local border_size = {
none = { 0 , 0 },
single = { 2 , 2 },
double = { 2 , 2 },
rounded = { 2 , 2 },
solid = { 2 , 2 },
shadow = { 1 , 1 },
}
2021-08-22 14:55:28 -06:00
--- Check the border given by opts or the default border for the additional
--- size it adds to a float.
2024-10-16 12:49:14 +01:00
--- @param opts? {border:string|(string|[string,string])[]}
--- @return integer height
--- @return integer width
2021-04-08 19:32:05 +02:00
local function get_border_size ( opts )
local border = opts and opts.border or default_border
if type ( border ) == 'string' then
2024-10-16 12:49:14 +01:00
if not border_size [ border ] then
border_error ( border )
2021-06-01 09:01:58 +02:00
end
2024-10-16 12:49:14 +01:00
return unpack ( border_size [ border ])
end
if 8 % # border ~= 0 then
border_error ( border )
end
--- @param id integer
--- @return string
local function elem ( id )
id = ( id - 1 ) % # border + 1
local e = border [ id ]
if type ( e ) == 'table' then
-- border specified as a table of <character, highlight group>
return e [ 1 ]
elseif type ( e ) == 'string' then
-- border specified as a list of border characters
return e
2021-06-01 09:01:58 +02:00
end
2024-10-16 12:49:14 +01:00
--- @diagnostic disable-next-line:missing-return
border_error ( border )
2021-04-08 19:32:05 +02:00
end
2024-10-16 12:49:14 +01:00
--- @param e string
local function border_height ( e )
return # e > 0 and 1 or 0
end
local top , bottom = elem ( 2 ), elem ( 6 )
local height = border_height ( top ) + border_height ( bottom )
local right , left = elem ( 4 ), elem ( 8 )
local width = vim.fn . strdisplaywidth ( right ) + vim.fn . strdisplaywidth ( left )
return height , width
2021-04-08 19:32:05 +02:00
end
2024-10-06 12:20:40 -07:00
--- Splits string at newlines, optionally removing unwanted blank lines.
---
--- @param s string Multiline string
--- @param no_blank boolean? Drop blank lines for each @param/@return (except one empty line
--- separating each). Workaround for https://github.com/LuaLS/lua-language-server/issues/2333
local function split_lines ( s , no_blank )
s = string.gsub ( s , ' \r\n ?' , ' \n ' )
local lines = {}
local in_desc = true -- Main description block, before seeing any @foo.
for line in vim.gsplit ( s , ' \n ' , { plain = true , trimempty = true }) do
local start_annotation = not not line : find ( '^ ?%@.?[pr]' )
in_desc = ( not start_annotation ) and in_desc or false
if start_annotation and no_blank and not ( lines [ # lines ] or '' ): find ( '^%s*$' ) then
table.insert ( lines , '' ) -- Separate each @foo with a blank line.
end
if in_desc or not no_blank or not line : find ( '^%s*$' ) then
table.insert ( lines , line )
end
end
return lines
2019-11-13 12:55:26 -08:00
end
2022-10-08 05:22:25 -03:00
local function create_window_without_focus ()
2024-10-16 09:45:12 +01:00
local prev = api.nvim_get_current_win ()
2022-10-08 05:22:25 -03:00
vim.cmd . new ()
2024-10-16 09:45:12 +01:00
local new = api.nvim_get_current_win ()
api.nvim_set_current_win ( prev )
2022-10-08 05:22:25 -03:00
return new
end
2021-12-10 06:17:50 -08:00
--- Convert byte index to `encoding` index.
--- Convenience wrapper around vim.str_utfindex
---@param line string line to be indexed
2024-10-15 17:27:27 +01:00
---@param index integer? byte index (utf-8), or `nil` for length
---@param encoding 'utf-8'|'utf-16'|'utf-32'? defaults to utf-16
2023-03-07 15:17:52 +09:00
---@return integer `encoding` index of `index` in `line`
2021-12-10 06:17:50 -08:00
function M . _str_utfindex_enc ( line , index , encoding )
2024-09-09 22:39:02 +01:00
local len32 , len16 = vim.str_utfindex ( line )
2021-12-20 08:54:05 -08: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
2024-09-09 22:39:02 +01:00
if not index or index > len16 then
return len16
end
2021-12-20 08:54:05 -08:00
local _ , col16 = vim.str_utfindex ( line , index )
return col16
elseif encoding == 'utf-32' then
2024-09-09 22:39:02 +01:00
if not index or index > len32 then
return len32
end
2021-12-20 08:54:05 -08:00
local col32 , _ = vim.str_utfindex ( line , index )
return col32
2021-12-10 06:17:50 -08:00
else
2021-12-20 08:54:05 -08:00
error ( 'Invalid encoding: ' .. vim.inspect ( encoding ))
2021-12-10 06:17:50 -08: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-07 15:17:52 +09:00
---@param index integer UTF index
2023-07-01 18:42:37 +08:00
---@param encoding string utf-8|utf-16|utf-32| defaults to utf-16
2023-03-07 15:17:52 +09:00
---@return integer byte (utf-8) index of `encoding` index `index` in `line`
2021-12-10 06:17:50 -08:00
function M . _str_byteindex_enc ( line , index , encoding )
2024-09-13 19:57:04 +02:00
local len = # line
2024-09-08 11:44:46 +01:00
if index > len then
-- LSP spec: if character > line length, default to the line length.
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#position
return len
end
2021-12-20 08:54:05 -08:00
if not encoding then
encoding = 'utf-16'
end
if encoding == 'utf-8' then
if index then
return index
else
2024-09-13 19:57:04 +02:00
return len
2021-12-20 08:54:05 -08:00
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 06:17:50 -08:00
else
2021-12-20 08:54:05 -08:00
error ( 'Invalid encoding: ' .. vim.inspect ( encoding ))
2021-12-10 06:17:50 -08:00
end
end
2020-08-19 18:17:08 +02:00
--- Replaces text in a range with new text.
---
--- CAUTION: Changes in-place!
---
2023-09-29 08:37:14 -07:00
---@deprecated
2024-05-31 08:41:10 -04:00
---@param lines string[] Original list of strings
---@param A [integer, integer] Start position; a 2-tuple of {line,col} numbers
---@param B [integer, integer] End position; a 2-tuple {line,col} numbers
---@param new_lines string[] list of strings to replace the original
---@return string[] The modified {lines} object
2019-11-21 15:19:06 -08:00
function M . set_lines ( lines , A , B , new_lines )
2023-12-25 21:28:28 +01:00
vim.deprecate ( 'vim.lsp.util.set_lines()' , 'nil' , '0.12' )
2019-11-21 15:19:06 -08:00
-- 0-indexing to 1-indexing
local i_0 = A [ 1 ] + 1
2019-12-20 02:50:37 -08: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 09:06:00 +02:00
if not ( i_0 >= 1 and i_0 <= # lines + 1 and i_n >= 1 and i_n <= # lines ) then
2019-11-21 15:19:06 -08:00
error ( 'Invalid range: ' .. vim.inspect ({ A = A , B = B , # lines , new_lines }))
2019-11-20 20:51:44 -08:00
end
local prefix = ''
2019-11-21 15:19:06 -08: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 20:51:44 -08:00
end
2019-11-21 23:58:32 -08: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 20:51:44 -08:00
if # suffix > 0 then
2019-11-21 23:58:32 -08:00
local i = i_0 + # new_lines - 1
lines [ i ] = lines [ i ] .. suffix
2019-11-20 20:51:44 -08:00
end
if # prefix > 0 then
2019-11-21 23:58:32 -08:00
lines [ i_0 ] = prefix .. lines [ i_0 ]
2019-11-20 20:51:44 -08:00
end
2019-11-21 23:58:32 -08:00
return lines
2019-11-21 15:19:06 -08:00
end
2024-10-15 17:27:27 +01:00
--- @param fn fun(x:any):any[]
--- @return function
2019-11-21 15:19:06 -08:00
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 20:51:44 -08:00
end
2019-11-21 15:19:06 -08:00
-- every value must have been equal here, which means it's not less than.
return false
2019-11-20 20:51:44 -08:00
end
end
2021-11-25 02:49:34 +01: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-07 15:17:52 +09:00
---@param bufnr integer bufnr to get the lines from
---@param rows integer[] zero-indexed line numbers
2023-07-01 18:42:37 +08:00
---@return table<integer, string>|string a table mapping rows to lines
2021-11-25 02:49:34 +01:00
local function get_lines ( bufnr , rows )
2024-06-11 12:05:18 +01:00
--- @type integer[]
2021-11-25 02:49:34 +01:00
rows = type ( rows ) == 'table' and rows or { rows }
2022-01-08 02:30:59 -08:00
-- This is needed for bufload and bufloaded
if bufnr == 0 then
2022-07-15 18:26:47 +02:00
bufnr = api.nvim_get_current_buf ()
2022-01-08 02:30:59 -08:00
end
2021-11-25 02:49:34 +01:00
local function buf_lines ()
2024-06-11 12:05:18 +01:00
local lines = {} --- @type table<integer,string>
2023-05-05 00:41:36 -05:00
for _ , row in ipairs ( rows ) do
2022-07-15 18:26:47 +02:00
lines [ row ] = ( api.nvim_buf_get_lines ( bufnr , row , row + 1 , false ) or { '' })[ 1 ]
2021-11-25 02:49:34 +01:00
end
return lines
end
2023-05-05 00:41:36 -05:00
-- use loaded buffers if available
if vim.fn . bufloaded ( bufnr ) == 1 then
return buf_lines ()
end
2021-11-25 02:49:34 +01: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 18:26:47 +02:00
local filename = api.nvim_buf_get_name ( bufnr )
2024-10-07 08:25:13 -07:00
if vim.fn . isdirectory ( filename ) ~= 0 then
return {}
end
2021-11-25 02:49:34 +01:00
-- get the data from the file
local fd = uv.fs_open ( filename , 'r' , 438 )
if not fd then
return ''
end
2024-06-11 12:05:18 +01:00
local stat = assert ( uv.fs_fstat ( fd ))
local data = assert ( uv.fs_read ( fd , stat.size , 0 ))
2021-11-25 02:49:34 +01:00
uv.fs_close ( fd )
2024-06-11 12:05:18 +01:00
local lines = {} --- @type table<integer,true|string> rows we need to retrieve
2021-11-25 02:49:34 +01:00
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
2024-06-11 12:05:18 +01:00
return lines --[[@as table<integer,string>]]
2021-11-25 02:49:34 +01:00
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-07 15:17:52 +09:00
---@param bufnr integer
---@param row integer zero-indexed line number
2021-11-25 02:49:34 +01:00
---@return string the line at row in filename
local function get_line ( bufnr , row )
return get_lines ( bufnr , { row })[ row ]
end
2020-05-19 08:49:13 +02:00
--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
2024-06-11 12:05:18 +01:00
---@param position lsp.Position
2024-10-16 13:17:10 +01:00
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'
2023-07-01 18:42:37 +08:00
---@return integer
2021-11-07 05:05:52 +07:00
local function get_line_byte_from_position ( bufnr , position , offset_encoding )
2020-05-19 08:49:13 +02: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 18:28:13 +09:00
local line = get_line ( bufnr , position.line ) or ''
2024-10-16 13:17:10 +01:00
return M._str_byteindex_enc ( line , col , offset_encoding )
2020-05-17 19:47:14 +02:00
end
2020-05-19 08:49:13 +02:00
return col
2020-05-17 19:47:14 +02:00
end
2020-08-19 18:17:08 +02:00
--- Applies a list of text edits to a buffer.
2024-05-31 08:41:10 -04:00
---@param text_edits lsp.TextEdit[]
2023-03-07 15:17:52 +09:00
---@param bufnr integer Buffer id
2022-03-30 21:04:17 +02:00
---@param offset_encoding string utf-8|utf-16|utf-32
2021-08-09 19:45:00 +02:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
2021-12-10 06:17:50 -08:00
function M . apply_text_edits ( text_edits , bufnr , offset_encoding )
2024-10-15 16:56:35 +01:00
validate ( 'text_edits' , text_edits , 'table' , false )
validate ( 'bufnr' , bufnr , 'number' , false )
validate ( 'offset_encoding' , offset_encoding , 'string' , false )
2019-11-20 20:51:44 -08:00
if not next ( text_edits ) then
return
end
2024-02-24 19:21:57 -06:00
assert ( bufnr ~= 0 , 'Explicit buffer number is required' )
2022-07-15 18:26:47 +02:00
if not api.nvim_buf_is_loaded ( bufnr ) then
2020-05-08 16:04:41 +02:00
vim.fn . bufload ( bufnr )
end
2022-12-19 16:37:45 +00:00
vim.bo [ bufnr ]. buflisted = true
2019-11-21 15:19:06 -08:00
2021-09-19 05:19:21 +09:00
-- Fix reversed range and indexing each text_edits
2024-10-15 17:27:27 +01:00
for index , text_edit in ipairs ( text_edits ) do
--- @cast text_edit lsp.TextEdit|{_index: integer}
2021-09-19 05:19:21 +09:00
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
2024-10-15 17:27:27 +01:00
end
2021-09-19 05:19:21 +09:00
-- Sort text_edits
2024-06-11 12:05:18 +01:00
---@param a lsp.TextEdit | { _index: integer }
---@param b lsp.TextEdit | { _index: integer }
---@return boolean
2021-09-19 05:19:21 +09:00
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
2024-07-27 22:30:14 +02:00
return a._index > b._index
2021-09-19 05:19:21 +09:00
end )
2023-06-05 01:45:01 +02:00
-- save and restore local marks since they get deleted by nvim_buf_set_lines
2024-06-11 12:05:18 +01:00
local marks = {} --- @type table<string,[integer,integer]>
2024-02-24 19:21:57 -06:00
for _ , m in pairs ( vim.fn . getmarklist ( bufnr )) do
2023-06-05 01:45:01 +02: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-19 05:19:21 +09:00
-- Apply text edits.
2022-01-13 18:28:13 +09:00
local has_eol_text_edit = false
2021-09-19 05:19:21 +09:00
for _ , text_edit in ipairs ( text_edits ) do
2022-01-14 08:02:44 -08:00
-- Normalize line ending
text_edit.newText , _ = string.gsub ( text_edit.newText , ' \r\n ?' , ' \n ' )
2022-01-13 18:28:13 +09:00
-- Convert from LSP style ranges to Neovim style ranges.
2024-10-16 13:32:12 +01:00
local start_row = text_edit.range . start.line
local start_col = get_line_byte_from_position ( bufnr , text_edit.range . start , offset_encoding )
local end_row = text_edit.range [ 'end' ]. line
local end_col = get_line_byte_from_position ( bufnr , text_edit.range [ 'end' ], offset_encoding )
2024-10-16 13:33:07 +01:00
local text = vim.split ( text_edit.newText , ' \n ' , { plain = true })
2022-01-13 18:28:13 +09:00
2022-07-15 18:26:47 +02:00
local max = api.nvim_buf_line_count ( bufnr )
2022-09-20 22:14:58 +02: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.
2024-10-16 13:32:12 +01:00
if max <= start_row then
api.nvim_buf_set_lines ( bufnr , max , max , false , text )
2022-09-20 22:14:58 +02:00
else
2024-10-16 13:32:12 +01:00
local last_line_len = # ( get_line ( bufnr , math.min ( end_row , max - 1 )) or '' )
2022-09-20 22:14:58 +02:00
-- 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.
2024-10-16 13:32:12 +01:00
if max <= end_row then
end_row = max - 1
end_col = last_line_len
2022-09-20 22:14:58 +02:00
has_eol_text_edit = true
else
2024-10-16 13:32:12 +01:00
-- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the
2022-09-20 22:14:58 +02:00
-- 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
2024-10-16 13:32:12 +01:00
end_col >= last_line_len
and text_edit.range [ 'end' ]. character > end_col
2022-09-20 22:14:58 +02:00
and # text_edit.newText > 0
and string.sub ( text_edit.newText , - 1 ) == ' \n '
then
2024-10-16 13:32:12 +01:00
table.remove ( text , # text )
2022-09-20 22:14:58 +02:00
end
2022-01-13 18:28:13 +09:00
end
2024-10-16 13:32:12 +01:00
-- Make sure we don't go out of bounds for end_col
end_col = math.min ( last_line_len , end_col )
2022-09-20 22:14:58 +02:00
2024-10-16 13:32:12 +01:00
api.nvim_buf_set_text ( bufnr , start_row , start_col , end_row , end_col , text )
2021-09-19 05:19:21 +09:00
end
2019-11-21 15:19:06 -08:00
end
2022-07-15 18:26:47 +02:00
local max = api.nvim_buf_line_count ( bufnr )
2022-01-13 18:28:13 +09:00
2023-06-05 01:45:01 +02:00
-- no need to restore marks that still exist
2024-02-24 19:21:57 -06:00
for _ , m in pairs ( vim.fn . getmarklist ( bufnr )) do
2023-06-05 01:45:01 +02: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 '' ))
2024-10-16 09:45:12 +01:00
api.nvim_buf_set_mark ( bufnr or 0 , mark , pos [ 1 ], pos [ 2 ], {})
2023-06-05 01:45:01 +02:00
end
end
2021-09-19 05:19:21 +09:00
-- Remove final line if needed
local fix_eol = has_eol_text_edit
2022-12-19 16:37:45 +00:00
fix_eol = fix_eol and ( vim.bo [ bufnr ]. eol or ( vim.bo [ bufnr ]. fixeol and not vim.bo [ bufnr ]. binary ))
2022-01-13 18:28:13 +09:00
fix_eol = fix_eol and get_line ( bufnr , max - 1 ) == ''
2021-09-19 05:19:21 +09:00
if fix_eol then
2022-07-15 18:26:47 +02:00
api.nvim_buf_set_lines ( bufnr , - 2 , - 1 , false , {})
2019-11-20 20:51:44 -08:00
end
end
2020-08-19 18:17:08 +02:00
--- Applies a `TextDocumentEdit`, which is a list of changes to a single
2021-01-11 11:39:11 -05:00
--- document.
2020-08-19 18:17:08 +02:00
---
2024-05-31 08:41:10 -04:00
---@param text_document_edit lsp.TextDocumentEdit
---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
2024-02-15 17:16:04 +00:00
---@param offset_encoding? string
2021-01-11 11:39:11 -05:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
2022-01-13 02:34:04 -08:00
function M . apply_text_document_edit ( text_document_edit , index , offset_encoding )
2019-11-20 20:51:44 -08:00
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr ( text_document.uri )
2022-01-13 02:34:04 -08:00
if offset_encoding == nil then
vim.notify_once (
'apply_text_document_edit must be called with valid offset encoding' ,
vim.log . levels.WARN
)
2024-10-16 13:17:10 +01:00
return
2022-01-13 02:34:04 -08:00
end
2020-11-02 08:50:44 -05:00
2021-01-11 11:39:11 -05: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 08:50:44 -05:00
-- `VersionedTextDocumentIdentifier`s version may be null
-- https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
2021-01-11 11:39:11 -05:00
if
should_check_version
and (
text_document.version
2021-04-14 21:29:32 +02:00
and text_document.version > 0
2024-06-07 11:36:46 +02:00
and M.buf_versions [ bufnr ] > text_document.version
2021-01-11 11:39:11 -05:00
)
then
2020-11-02 08:50:44 -05:00
print ( 'Buffer ' , text_document.uri , ' newer than edits.' )
return
2019-11-13 12:55:26 -08:00
end
2020-11-02 08:50:44 -05:00
2022-01-13 02:34:04 -08:00
M.apply_text_edits ( text_document_edit.edits , bufnr , offset_encoding )
2019-11-13 12:55:26 -08:00
end
2024-02-25 00:47:34 +09:00
local function path_components ( path )
return vim.split ( path , '/' , { plain = true })
end
2024-10-15 17:27:27 +01:00
--- @param path string[]
--- @param prefix string[]
--- @return boolean
2024-02-25 00:47:34 +09:00
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
2024-10-16 13:10:21 +01:00
--- Get list of loaded writable buffers whose filename matches the given path
--- prefix (normalized full path).
2024-05-31 08:41:10 -04:00
---@param prefix string
2024-02-25 00:47:34 +09:00
---@return integer[]
2024-10-16 13:10:21 +01:00
local function get_writable_bufs ( prefix )
2024-10-15 17:27:27 +01:00
local prefix_parts = path_components ( prefix )
2024-10-16 13:10:21 +01:00
local buffers = {} --- @type integer[]
for _ , buf in ipairs ( api.nvim_list_bufs ()) do
-- No need to care about unloaded or nofile buffers. Also :saveas won't work for them.
if
api.nvim_buf_is_loaded ( buf )
and not vim.list_contains ({ 'nofile' , 'nowrite' }, vim.bo [ buf ]. buftype )
then
local bname = api.nvim_buf_get_name ( buf )
local path = path_components ( vim.fs . normalize ( bname , { expand_env = false }))
if path_under_prefix ( path , prefix_parts ) then
buffers [ # buffers + 1 ] = buf
end
2023-03-14 13:08:37 +01:00
end
end
return buffers
end
2024-03-02 23:21:53 +09:00
local function escape_gsub_repl ( s )
return ( s : gsub ( '%%' , '%%%%' ))
end
2024-02-27 15:20:32 +00:00
--- @class vim.lsp.util.rename.Opts
--- @inlinedoc
--- @field overwrite? boolean
--- @field ignoreIfExists? boolean
2021-03-15 22:34:22 +01:00
--- Rename old_fname to new_fname
2024-03-02 23:21:53 +09: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-12 06:51:53 +01:00
--- * the conflicting buffers are not loaded, so that deleting them does not result in data loss.
2024-03-02 23:21:53 +09:00
---
2024-02-27 15:20:32 +00:00
--- @param old_fname string
--- @param new_fname string
--- @param opts? vim.lsp.util.rename.Opts Options:
2021-03-15 22:34:22 +01:00
function M . rename ( old_fname , new_fname , opts )
opts = opts or {}
2024-02-25 00:47:34 +09: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-10 01:57:35 +02:00
local target_exists = uv.fs_stat ( new_fname ) ~= nil
2024-02-25 00:47:34 +09:00
if target_exists and skip then
vim.notify ( new_fname .. ' already exists. Skipping rename.' , vim.log . levels.ERROR )
2021-03-15 22:34:22 +01:00
return
end
2021-11-14 12:55:16 +01:00
2024-03-02 23:21:53 +09:00
local buf_rename = {} ---@type table<integer, {from: string, to: string}>
local old_fname_pat = '^' .. vim.pesc ( old_fname_full )
2024-10-16 13:10:21 +01:00
for _ , b in ipairs ( get_writable_bufs ( old_fname_full )) do
2024-03-02 23:21:53 +09:00
-- 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.
2024-10-16 09:45:12 +01:00
local old_bname = api.nvim_buf_get_name ( b )
2024-03-02 23:21:53 +09:00
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-29 01:32:25 +09:00
end
2024-03-02 23:21:53 +09:00
buf_rename [ b ] = { from = old_bname , to = new_bname }
2023-02-18 09:43:59 +03:00
end
2021-11-14 12:55:16 +01:00
2024-05-25 20:35:37 +02:00
local newdir = vim.fs . dirname ( new_fname )
2024-02-08 22:06:54 +01:00
vim.fn . mkdir ( newdir , 'p' )
2024-02-25 00:47:34 +09:00
local ok , err = os.rename ( old_fname_full , new_fname )
2021-03-15 22:34:22 +01:00
assert ( ok , err )
2021-11-14 12:55:16 +01:00
2024-03-02 02:31:54 +09: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 )
2024-05-25 20:35:37 +02:00
vim.fn . mkdir ( vim.fs . dirname ( new_undofile ), 'p' )
2024-03-02 02:31:54 +09:00
os.rename ( old_undofile , new_undofile )
end
2024-03-02 23:21:53 +09: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.
2024-06-08 21:40:18 +02:00
vim._with ({ buf = b }, function ()
2024-03-02 23:21:53 +09:00
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 12:55:16 +01:00
end
2021-03-15 22:34:22 +01:00
end
2024-09-21 16:23:00 +01:00
--- @param change lsp.CreateFile
2021-03-18 11:07:06 +01: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 06:46:21 -04:00
vim.fn . mkdir ( vim.fs . dirname ( fname ), 'p' )
2021-03-18 11:07:06 +01:00
local file = io.open ( fname , 'w' )
2022-09-24 06:46:21 -04:00
if file then
file : close ()
end
2021-03-18 11:07:06 +01:00
end
vim.fn . bufadd ( fname )
end
2024-09-21 16:23:00 +01:00
--- @param change lsp.DeleteFile
2021-03-18 11:32:33 +01:00
local function delete_file ( change )
local opts = change.options or {}
local fname = vim.uri_to_fname ( change.uri )
local bufnr = vim.fn . bufadd ( fname )
2024-09-21 16:23:00 +01:00
vim.fs . rm ( fname , {
force = opts.ignoreIfNotExists ,
recursive = opts.recursive ,
})
2022-07-15 18:26:47 +02:00
api.nvim_buf_delete ( bufnr , { force = true })
2021-03-18 11:32:33 +01:00
end
2020-08-19 18:17:08 +02:00
--- Applies a `WorkspaceEdit`.
---
2024-05-31 08:41:10 -04:00
---@param workspace_edit lsp.WorkspaceEdit
2024-10-15 17:27:27 +01:00
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32' (required)
2024-02-15 17:16:04 +00:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
2022-01-13 02:34:04 -08: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
)
2024-10-16 13:17:10 +01:00
return
2022-01-13 02:34:04 -08:00
end
2019-11-13 12:55:26 -08:00
if workspace_edit.documentChanges then
2021-01-11 11:39:11 -05:00
for idx , change in ipairs ( workspace_edit.documentChanges ) do
2021-03-15 22:34:22 +01:00
if change.kind == 'rename' then
2024-10-15 17:27:27 +01:00
local options = change.options --[[@as vim.lsp.util.rename.Opts]]
M.rename ( vim.uri_to_fname ( change.oldUri ), vim.uri_to_fname ( change.newUri ), options )
2021-03-18 11:07:06 +01:00
elseif change.kind == 'create' then
create_file ( change )
2021-03-18 11:32:33 +01:00
elseif change.kind == 'delete' then
delete_file ( change )
2024-10-15 17:27:27 +01:00
elseif change.kind then --- @diagnostic disable-line:undefined-field
2019-11-13 12:55:26 -08:00
error ( string.format ( 'Unsupported change: %q' , vim.inspect ( change )))
else
2022-01-13 02:34:04 -08:00
M.apply_text_document_edit ( change , idx , offset_encoding )
2019-11-13 12:55:26 -08:00
end
end
return
end
2019-11-20 16:35:11 -08:00
local all_changes = workspace_edit.changes
if not ( all_changes and not vim.tbl_isempty ( all_changes )) then
2019-11-13 12:55:26 -08:00
return
end
2019-11-20 16:35:11 -08:00
for uri , changes in pairs ( all_changes ) do
local bufnr = vim.uri_to_bufnr ( uri )
2022-01-13 06:47:35 -08:00
M.apply_text_edits ( changes , bufnr , offset_encoding )
2019-11-13 12:55:26 -08:00
end
end
2020-08-19 18:17:08 +02: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-05-31 08:41:10 -04:00
---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent
2024-10-15 17:27:27 +01:00
---@param contents string[]? 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 14:55:28 -06:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
2019-11-13 12:55:26 -08:00
function M . convert_input_to_markdown_lines ( input , contents )
contents = contents or {}
-- MarkedString variation 1
if type ( input ) == 'string' then
2024-10-06 12:20:40 -07:00
list_extend ( contents , split_lines ( input , true ))
2019-11-13 12:55:26 -08:00
else
2023-09-12 20:51:21 -07:00
assert ( type ( input ) == 'table' , 'Expected a table for LSP input' )
2019-11-13 12:55:26 -08:00
-- MarkupContent
if input.kind then
2021-07-05 00:39:46 +02:00
local value = input.value or ''
2024-10-06 12:20:40 -07:00
list_extend ( contents , split_lines ( value , true ))
2019-11-13 12:55:26 -08: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-27 00:00:06 +01:00
if ( contents [ 1 ] == '' or contents [ 1 ] == nil ) and # contents == 1 then
2019-11-13 12:55:26 -08:00
return {}
end
return contents
end
2024-10-15 02:36:04 -07:00
--- Returns the line/column-based position in `contents` at the given offset.
---
---@param offset integer
---@param contents string[]
2024-10-15 17:27:27 +01:00
---@return { [1]: integer, [2]: integer }?
2024-10-15 02:36:04 -07:00
local function get_pos_from_offset ( offset , contents )
local i = 0
for l , line in ipairs ( contents ) do
if offset >= i and offset < i + # line then
return { l - 1 , offset - i + 1 }
else
i = i + # line + 1
end
end
end
2023-09-12 20:51:21 -07:00
--- Converts `textDocument/signatureHelp` response to markdown lines.
2020-08-19 18:17:08 +02:00
---
2024-05-31 08:41:10 -04:00
---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp`
2024-10-15 17:27:27 +01:00
---@param ft string? filetype that will be use as the `lang` for the label markdown code block
---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets
---@return string[]? # lines of converted markdown.
---@return Range4? # highlight range for the active parameter
2021-08-22 14:55:28 -06:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
2021-07-09 12:38:29 +02:00
function M . convert_signature_help_to_markdown_lines ( signature_help , ft , triggers )
2020-04-20 06:40:54 +09:00
--The active signature. If omitted or the value lies outside the range of
2022-09-25 02:20:47 +02: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-20 06:40:54 +09:00
--the active signature and shouldn't rely on a default value.
2024-10-15 17:27:27 +01:00
local contents = {} --- @type string[]
local active_offset ---@type [integer, integer]?
2020-11-02 08:50:44 -05:00
local active_signature = signature_help.activeSignature or 0
2020-04-20 06:40:54 +09:00
-- If the activeSignature is not inside the valid range, then clip it.
2022-01-13 10:47:36 +01: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-20 06:40:54 +09:00
active_signature = 0
end
local signature = signature_help.signatures [ active_signature + 1 ]
2021-05-21 19:19:17 +02: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 19:19:17 +02:00
label = ( '```%s \n %s \n ```' ): format ( ft , label )
end
2024-10-16 13:33:07 +01:00
list_extend ( contents , vim.split ( label , ' \n ' , { plain = true , trimempty = true }))
2024-10-15 17:27:27 +01:00
local doc = signature.documentation
if doc then
2023-07-25 08:40:13 -04: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
2024-10-15 17:27:27 +01:00
if type ( doc ) == 'string' then
signature.documentation = { kind = 'plaintext' , value = doc }
2023-07-25 08:40:13 -04:00
end
2020-04-20 06:40:54 +09:00
M.convert_input_to_markdown_lines ( signature.documentation , contents )
end
2020-09-13 00:33:31 +08:00
if signature.parameters and # signature.parameters > 0 then
2024-10-15 02:36:04 -07:00
-- First check if the signature has an activeParameter. If it doesn't check if the response
-- had that property instead. Else just default to 0.
2024-10-16 10:34:37 +01:00
local active_parameter =
math.max ( signature.activeParameter or signature_help.activeParameter or 0 , 0 )
2021-07-09 12:38:29 +02:00
-- 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
2024-10-16 10:34:37 +01:00
active_parameter = math.min ( active_parameter , # signature.parameters - 1 )
2021-07-09 12:38:29 +02:00
2020-09-13 00:33:31 +08:00
local parameter = signature.parameters [ active_parameter + 1 ]
2024-10-15 02:36:04 -07:00
local parameter_label = parameter.label
if type ( parameter_label ) == 'table' then
active_offset = parameter_label
else
2024-10-15 17:27:27 +01:00
local offset = 1 ---@type integer?
2024-10-15 02:36:04 -07:00
-- 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
2021-07-08 08:04:35 +02:00
end
end
2024-10-15 02:36:04 -07:00
for p , param in pairs ( signature.parameters ) do
2024-10-15 17:27:27 +01:00
local plabel = param.label
assert ( type ( plabel ) == 'string' , 'Expected label to be a string' )
offset = signature.label : find ( plabel , offset , true )
2024-10-15 02:36:04 -07:00
if not offset then
break
end
if p == active_parameter + 1 then
active_offset = { offset - 1 , offset + # parameter_label - 1 }
break
end
offset = offset + # param.label + 1
2020-04-20 06:40:54 +09:00
end
end
2024-10-15 02:36:04 -07:00
if parameter.documentation then
M.convert_input_to_markdown_lines ( parameter.documentation , contents )
end
2020-04-20 06:40:54 +09:00
end
2024-10-15 02:36:04 -07:00
local active_hl = nil
if active_offset then
-- Account for the start of the markdown block.
if ft then
2024-10-15 17:27:27 +01:00
active_offset [ 1 ] = active_offset [ 1 ] + # contents [ 1 ]
active_offset [ 2 ] = active_offset [ 2 ] + # contents [ 1 ]
2024-10-15 02:36:04 -07:00
end
2024-10-15 17:27:27 +01:00
active_hl = {}
list_extend ( active_hl , get_pos_from_offset ( active_offset [ 1 ], contents ) or {})
list_extend ( active_hl , get_pos_from_offset ( active_offset [ 2 ], contents ) or {})
2024-10-15 02:36:04 -07:00
end
2021-07-08 08:04:35 +02:00
return contents , active_hl
2020-04-20 06:40:54 +09:00
end
2020-08-19 18:17:08 +02:00
--- Creates a table with sensible default options for a floating window. The
--- table can be passed to |nvim_open_win()|.
---
2023-07-01 18:42:37 +08:00
---@param width integer window width (in character cells)
---@param height integer window height (in character cells)
2024-10-15 17:27:27 +01:00
---@param opts? vim.lsp.util.open_floating_preview.Opts
2023-07-01 18:42:37 +08:00
---@return table Options
2019-11-13 12:55:26 -08:00
function M . make_floating_popup_options ( width , height , opts )
2024-10-15 16:56:35 +01:00
validate ( 'opts' , opts , 'table' , true )
2019-11-13 12:55:26 -08:00
opts = opts or {}
2024-10-15 16:56:35 +01:00
validate ( 'opts.offset_x' , opts.offset_x , 'n' , true )
validate ( 'opts.offset_y' , opts.offset_y , 'n' , true )
2019-11-13 12:55:26 -08:00
local anchor = ''
2023-01-10 11:22:41 +01:00
local lines_above = opts.relative == 'mouse' and vim.fn . getmousepos (). line - 1
or vim.fn . winline () - 1
2020-01-03 14:39:25 +02: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'
2024-10-15 17:27:27 +01:00
local anchor_below --- @type boolean?
2023-09-10 01:02:23 -07:00
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
2024-10-16 12:49:14 +01:00
local border_height = get_border_size ( opts )
2024-10-15 17:27:27 +01:00
local row , col --- @type integer?, integer?
2023-09-10 01:02:23 -07:00
if anchor_below then
2019-11-13 12:55:26 -08: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 12:55:26 -08: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-03 08:36:24 +08:00
row = 0
2019-11-13 12:55:26 -08:00
end
2023-01-10 11:22:41 +01:00
local wincol = opts.relative == 'mouse' and vim.fn . getmousepos (). column or vim.fn . wincol ()
2022-12-19 16:37:45 +00:00
if wincol + width + ( opts.offset_x or 0 ) <= vim.o . columns then
2019-11-13 12:55:26 -08:00
anchor = anchor .. 'W'
col = 0
else
anchor = anchor .. 'E'
col = 1
end
2022-11-21 18:06:14 +08:00
local title = ( opts.border and opts.title ) and opts.title or nil
2024-10-15 17:27:27 +01:00
local title_pos --- @type 'left'|'center'|'right'?
2022-11-21 18:06:14 +08:00
if title then
title_pos = opts.title_pos or 'center'
end
2019-11-13 12:55:26 -08:00
return {
anchor = anchor ,
col = col + ( opts.offset_x or 0 ),
height = height ,
2021-06-01 13:33:07 +06:00
focusable = opts.focusable ,
2023-01-10 11:22:41 +01:00
relative = opts.relative == 'mouse' and 'mouse' or 'cursor' ,
2019-11-13 12:55:26 -08:00
row = row + ( opts.offset_y or 0 ),
style = 'minimal' ,
width = width ,
2021-04-08 19:32:05 +02:00
border = opts.border or default_border ,
2021-07-20 02:03:59 +10:00
zindex = opts.zindex or 50 ,
2022-11-21 18:06:14 +08:00
title = title ,
title_pos = title_pos ,
2019-11-13 12:55:26 -08:00
}
end
2024-10-15 17:27:27 +01:00
--- @class vim.lsp.util.show_document.Opts
--- @inlinedoc
---
--- Jump to existing window if buffer is already open.
--- @field reuse_win? boolean
---
--- Whether to focus/jump to location if possible.
--- (defaults: true)
--- @field focus? boolean
2022-10-08 05:22:25 -03:00
--- Shows document and optionally jumps to the location.
2020-08-19 18:17:08 +02:00
---
2024-05-31 08:41:10 -04:00
---@param location lsp.Location|lsp.LocationLink
2024-10-15 17:27:27 +01:00
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'?
---@param opts? vim.lsp.util.show_document.Opts
2022-10-08 05:22:25 -03:00
---@return boolean `true` if succeeded
function M . show_document ( location , offset_encoding , opts )
2020-05-02 15:21:07 +02:00
-- location may be Location or LocationLink
local uri = location.uri or location.targetUri
if uri == nil then
2022-10-08 05:22:25 -03:00
return false
2020-05-02 15:21:07 +02:00
end
2022-01-13 02:34:04 -08:00
if offset_encoding == nil then
2022-10-08 05:22:25 -03:00
vim.notify_once ( 'show_document must be called with valid offset encoding' , vim.log . levels.WARN )
2024-10-16 13:17:10 +01:00
return false
2022-01-13 02:34:04 -08:00
end
2021-05-19 15:33:32 +02:00
local bufnr = vim.uri_to_bufnr ( uri )
2020-04-28 16:47:22 +02:00
2022-10-08 05:22:25 -03: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 16:47:22 +02:00
2024-02-24 21:14:12 +09:00
local win = opts.reuse_win and vim.fn . win_findbuf ( bufnr )[ 1 ]
2022-10-08 05:22:25 -03:00
or focus and api.nvim_get_current_win ()
or create_window_without_focus ()
2022-12-19 16:37:45 +00:00
vim.bo [ bufnr ]. buflisted = true
2022-10-08 05:22:25 -03:00
api.nvim_win_set_buf ( win , bufnr )
if focus then
2022-07-15 18:26:47 +02:00
api.nvim_set_current_win ( win )
2022-05-18 20:03:24 +01:00
end
2022-10-08 05:22:25 -03:00
-- location may be Location or LocationLink
2020-05-02 15:21:07 +02:00
local range = location.range or location.targetSelectionRange
2022-10-08 05:22:25 -03:00
if range then
2023-12-28 18:00:30 -05:00
-- Jump to new location (adjusting for encoding of characters)
2022-10-08 05:22:25 -03: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 })
2024-06-08 21:40:18 +02:00
vim._with ({ win = win }, function ()
2022-10-08 05:22:25 -03:00
-- Open folds under the cursor
vim.cmd ( 'normal! zv' )
end )
end
2019-11-26 05:59:40 -08:00
return true
end
2022-10-08 05:22:25 -03:00
--- Jumps to a location.
---
2024-05-31 08:41:10 -04:00
---@param location lsp.Location|lsp.LocationLink
2024-10-15 17:27:27 +01:00
---@param offset_encoding string? utf-8|utf-16|utf-32
---@param reuse_win boolean? Jump to existing window if buffer is already open.
2022-10-08 05:22:25 -03: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 18:17:08 +02:00
--- Previews a location in a floating window
2020-05-26 15:07:10 +02: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)
---
2024-05-31 08:41:10 -04:00
---@param location lsp.Location|lsp.LocationLink
2024-10-15 17:27:27 +01:00
---@param opts? vim.lsp.util.open_floating_preview.Opts
---@return integer? buffer id of float window
---@return integer? window id of float window
2021-05-21 19:16:50 +02:00
function M . preview_location ( location , opts )
2020-05-26 15:07:10 +02: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 18:26:47 +02:00
if not api.nvim_buf_is_loaded ( bufnr ) then
2020-05-26 15:07:10 +02:00
vim.fn . bufload ( bufnr )
end
local range = location.targetRange or location.range
2022-07-15 18:26:47 +02:00
local contents = api.nvim_buf_get_lines ( bufnr , range.start . line , range [ 'end' ]. line + 1 , false )
2022-12-19 16:37:45 +00:00
local syntax = vim.bo [ bufnr ]. syntax
2021-05-21 19:16:50 +02: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 19:16:50 +02:00
-- An empty syntax is more common now with TreeSitter, since TS disables syntax.
2022-12-19 16:37:45 +00:00
syntax = vim.bo [ bufnr ]. filetype
2021-05-21 19:16:50 +02:00
end
2021-05-26 23:47:53 +02:00
opts = opts or {}
opts.focus_id = 'location'
2021-05-21 19:16:50 +02:00
return M.open_floating_preview ( contents , syntax , opts )
2020-05-26 15:07:10 +02:00
end
2019-11-26 05:59:40 -08:00
local function find_window_by_var ( name , value )
2022-07-15 18:26:47 +02:00
for _ , win in ipairs ( api.nvim_list_wins ()) do
2024-10-16 13:35:03 +01:00
if vim.w [ win ][ name ] == value then
2019-11-26 05:59:40 -08: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-06 03:09:52 +02:00
end
2023-09-12 20:51:21 -07:00
l = l + 1
2020-07-06 03:09:52 +02: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-06 03:09:52 +02:00
end
2023-09-12 20:51:21 -07:00
table.insert ( collapsed , line )
l = l + 1
2020-07-06 03:09:52 +02:00
end
2023-09-12 20:51:21 -07:00
return collapsed
2020-06-04 20:23:03 +02:00
end
2021-06-25 15:23:02 +02:00
local function get_markdown_fences ()
2024-10-15 17:27:27 +01:00
local fences = {} --- @type table<string,string>
for _ , fence in
pairs ( vim.g . markdown_fenced_languages or {} --[[@as string[] ]] )
do
2021-06-25 15:23:02 +02:00
local lang , syntax = fence : match ( '^(.*)=(.*)$' )
if lang then
fences [ lang ] = syntax
end
end
return fences
end
2020-08-19 18:17:08 +02:00
--- Converts markdown into syntax highlighted regions by stripping the code
2020-06-04 20:23:03 +02: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 23:47:53 +02: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 20:23:03 +02:00
---
2024-02-15 17:16:04 +00:00
---@param bufnr integer
2024-05-31 08:41:10 -04:00
---@param contents string[] of lines to show in window
2024-10-15 17:27:27 +01:00
---@param opts? table with optional fields
2020-11-12 22:21:34 -05: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 18:42:37 +08:00
---@return table stripped content
2021-05-26 23:47:53 +02:00
function M . stylize_markdown ( bufnr , contents , opts )
2024-10-15 16:56:35 +01:00
validate ( 'contents' , contents , 'table' )
validate ( 'opts' , opts , 'table' , true )
2020-06-04 20:23:03 +02:00
opts = opts or {}
2021-06-10 17:40:26 +02: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 06:11:45 -04:00
block = { nil , '```+%s*([a-zA-Z0-9_]*)' , '```+' },
2022-12-03 13:51:57 +01:00
pre = { nil , '<pre>([a-z0-9]*)' , '</pre>' },
2021-06-10 17:40:26 +02:00
code = { '' , '<code>' , '</code>' },
2022-03-29 01:16:11 +09:00
text = { 'text' , '<text>' , '</text>' },
2021-06-10 17:40:26 +02:00
}
2024-10-15 17:27:27 +01:00
--- @param line string
--- @return {type:string,ft:string}?
local function match_begin ( line )
2021-06-10 17:40:26 +02:00
for type , pattern in pairs ( matchers ) do
2024-10-15 17:27:27 +01:00
--- @type string?
2021-06-10 17:40:26 +02:00
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
2024-10-15 17:27:27 +01:00
--- @param line string
--- @param match {type:string,ft:string}
--- @return string
local function match_end ( line , match )
2021-06-10 17:40:26 +02:00
local pattern = matchers [ match.type ]
return line : match ( string.format ( '^%%s*%s%%s*$' , pattern [ 3 ]))
end
2021-06-15 00:41:07 +02:00
-- Clean up
2023-09-18 11:04:01 -07:00
contents = vim.split ( table.concat ( contents , ' \n ' ), ' \n ' , { trimempty = true })
2021-06-15 00:41:07 +02:00
2019-12-20 02:50:37 -08:00
local stripped = {}
2024-10-15 17:27:27 +01:00
local highlights = {} --- @type {ft:string,start:integer,finish:integer}[]
2021-06-15 00:41:07 +02:00
-- keep track of lnums that contain markdown
2024-10-15 17:27:27 +01:00
local markdown_lines = {} --- @type table<integer,boolean>
2024-10-16 09:48:27 +01:00
local i = 1
while i <= # contents do
local line = contents [ i ]
local match = match_begin ( line )
if match then
local start = # stripped
i = i + 1
while i <= # contents do
line = contents [ i ]
if match_end ( line , match ) then
2019-12-20 02:50:37 -08:00
i = i + 1
2024-10-16 09:48:27 +01:00
break
2019-12-20 02:50:37 -08:00
end
2024-10-16 09:48:27 +01:00
table.insert ( stripped , line )
2019-12-20 02:50:37 -08:00
i = i + 1
end
2024-10-16 09:48:27 +01:00
table.insert ( highlights , {
ft = match.ft ,
start = start + 1 ,
finish = # stripped ,
})
-- add a separator, but not on the last line
if opts.separator and i < # contents then
table.insert ( stripped , '---' )
markdown_lines [ # stripped ] = true
end
else
-- 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 ( '^---+$' )
)
then
table.insert ( stripped , line )
markdown_lines [ # stripped ] = true
end
i = i + 1
2019-12-20 02:50:37 -08:00
end
end
2020-06-04 20:23:03 +02:00
2023-03-25 13:46:07 -03:00
-- Handle some common html escape sequences
2024-10-15 17:27:27 +01:00
--- @type string[]
stripped = vim.tbl_map (
--- @param line string
function ( line )
local escapes = {
[ '>' ] = '>' ,
[ '<' ] = '<' ,
[ '"' ] = '"' ,
[ ''' ] = "'" ,
[ ' ' ] = ' ' ,
[ ' ' ] = ' ' ,
[ '&' ] = '&' ,
}
return ( line : gsub ( '&[^ ;]+;' , escapes ))
end ,
stripped
)
2023-03-25 13:46:07 -03:00
2020-06-04 20:23:03 +02:00
-- Compute size of float needed to show (wrapped) lines
2022-07-15 18:26:47 +02:00
opts.wrap_at = opts.wrap_at or ( vim.wo [ 'wrap' ] and api.nvim_win_get_width ( 0 ))
2021-08-06 23:15:49 +02:00
local width = M._make_floating_popup_size ( stripped , opts )
2020-06-04 20:23:03 +02:00
2021-06-15 00:41:07 +02: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 18:26:47 +02:00
api.nvim_buf_set_lines ( bufnr , 0 , - 1 , false , stripped )
2021-05-21 19:18:22 +02:00
2019-12-20 02:50:37 -08:00
local idx = 1
2021-05-26 23:47:53 +02:00
-- keep track of syntaxes we already included.
-- no need to include the same syntax more than once
2024-10-15 17:27:27 +01:00
local langs = {} --- @type table<string,boolean>
2021-06-25 15:23:02 +02:00
local fences = get_markdown_fences ()
2020-05-31 20:56:00 +02:00
local function apply_syntax_to_region ( ft , start , finish )
2021-05-20 10:42:08 +02:00
if ft == '' then
2021-05-21 19:18:22 +02:00
vim.cmd (
string.format (
'syntax region markdownCode start=+ \\ %%%dl+ end=+ \\ %%%dl+ keepend extend' ,
start ,
finish + 1
)
2022-05-09 11:23:51 +02:00
)
2021-05-20 10:42:08 +02:00
return
end
2021-06-25 15:23:02 +02:00
ft = fences [ ft ] or ft
2019-12-20 02:50:37 -08:00
local name = ft .. idx
idx = idx + 1
local lang = '@' .. ft : upper ()
2021-05-26 23:47:53 +02: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 18:26:47 +02:00
pcall ( api.nvim_buf_del_var , bufnr , 'current_syntax' )
2023-07-01 18:42:37 +08:00
if # api.nvim_get_runtime_file (( 'syntax/%s.vim' ): format ( ft ), true ) == 0 then
2021-05-26 23:47:53 +02:00
return
end
2024-10-15 17:27:27 +01:00
--- @diagnostic disable-next-line:param-type-mismatch
2023-07-25 19:38:48 +08:00
pcall ( vim.cmd , string.format ( 'syntax include %s syntax/%s.vim' , lang , ft ))
2021-05-26 23:47:53 +02:00
langs [ lang ] = true
2019-12-20 02:50:37 -08:00
end
2021-05-20 10:42:08 +02:00
vim.cmd (
string.format (
'syntax region %s start=+ \\ %%%dl+ end=+ \\ %%%dl+ contains=%s keepend' ,
name ,
start ,
finish + 1 ,
lang
)
2022-05-09 11:23:51 +02:00
)
2019-12-20 02:50:37 -08:00
end
2021-05-21 19:18:22 +02:00
2021-05-26 23:47:53 +02:00
-- needs to run in the buffer for the regions to work
2024-06-08 21:40:18 +02:00
vim._with ({ buf = bufnr }, function ()
2021-05-26 23:47:53 +02: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 20:45:42 +02:00
if last <= # stripped then
2021-05-26 23:47:53 +02:00
apply_syntax_to_region ( 'lsp_markdown' , last , # stripped )
end
end )
2019-12-20 02:50:37 -08:00
2021-05-26 23:47:53 +02:00
return stripped
2019-12-20 02:50:37 -08:00
end
2024-02-27 15:20:32 +00: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 15:20:32 +00: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 )
2024-10-15 16:56:35 +01:00
validate ( 'contents' , contents , 'table' )
validate ( 'opts' , opts , 'table' , true )
2023-09-12 20:51:21 -07:00
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 15:53:09 +01:00
--- Closes the preview window
---
2023-03-07 15:17:52 +09:00
---@param winnr integer window id of preview window
2024-10-15 17:27:27 +01:00
---@param bufnrs table? optional list of ignored buffers
2022-07-10 00:40:32 +08:00
local function close_preview_window ( winnr , bufnrs )
2021-12-15 15:53:09 +01:00
vim.schedule ( function ()
-- exit if we are in one of ignored buffers
2023-04-14 10:39:57 +02:00
if bufnrs and vim.list_contains ( bufnrs , api.nvim_get_current_buf ()) then
2021-12-15 15:53:09 +01:00
return
end
local augroup = 'preview_window_' .. winnr
2022-07-15 18:26:47 +02:00
pcall ( api.nvim_del_augroup_by_name , augroup )
pcall ( api.nvim_win_close , winnr , true )
2021-12-15 15:53:09 +01:00
end )
end
2022-07-10 00:40:32 +08:00
--- Creates autocommands to close a preview window when events happen.
---
---@param events table list of events
2023-03-07 15:17:52 +09:00
---@param winnr integer window id of preview window
2022-07-10 00:40:32 +08:00
---@param bufnrs table list of buffers where the preview window will remain visible
2023-07-01 18:42:37 +08:00
---@see autocmd-events
2022-07-10 00:40:32 +08:00
local function close_preview_autocmd ( events , winnr , bufnrs )
2022-07-15 18:26:47 +02:00
local augroup = api.nvim_create_augroup ( 'preview_window_' .. winnr , {
2022-07-10 00:40:32 +08: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 18:26:47 +02:00
api.nvim_create_autocmd ( 'BufEnter' , {
2022-07-10 00:40:32 +08:00
group = augroup ,
callback = function ()
close_preview_window ( winnr , bufnrs )
end ,
})
if # events > 0 then
2022-07-15 18:26:47 +02:00
api.nvim_create_autocmd ( events , {
2022-07-18 07:11:41 +08:00
group = augroup ,
2022-07-10 00:40:32 +08:00
buffer = bufnrs [ 2 ],
callback = function ()
close_preview_window ( winnr )
end ,
})
end
end
2024-02-27 15:20:32 +00:00
---@private
2020-08-19 18:17:08 +02:00
--- Computes size of float needed to show contents (with optional wrapping)
2020-06-04 20:23:03 +02:00
---
2024-10-15 17:27:27 +01:00
---@param contents string[] of lines to show in window
---@param opts? vim.lsp.util.open_floating_preview.Opts
2023-07-01 18:42:37 +08:00
---@return integer width size of float
---@return integer height size of float
2020-06-04 20:23:03 +02:00
function M . _make_floating_popup_size ( contents , opts )
2024-10-15 16:56:35 +01:00
validate ( 'contents' , contents , 'table' )
validate ( 'opts' , opts , 'table' , true )
2019-11-20 15:35:18 -08:00
opts = opts or {}
2019-11-20 11:34:10 -08:00
2019-11-20 15:35:18 -08:00
local width = opts.width
2020-06-04 20:23:03 +02:00
local height = opts.height
2020-07-06 03:09:52 +02:00
local wrap_at = opts.wrap_at
local max_width = opts.max_width
local max_height = opts.max_height
2024-10-15 17:27:27 +01:00
local line_widths = {} --- @type table<integer,integer>
2020-06-04 20:23:03 +02:00
2019-11-20 15:35:18 -08: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 15:39:39 +08:00
line_widths [ i ] = vim.fn . strdisplaywidth ( line : gsub ( '%z' , ' \n ' ))
2020-06-04 20:23:03 +02:00
width = math.max ( line_widths [ i ], width )
2019-11-13 12:55:26 -08:00
end
end
2021-04-08 19:32:05 +02:00
2024-10-16 12:49:14 +01:00
local _ , border_width = get_border_size ( opts )
2022-07-15 18:26:47 +02:00
local screen_width = api.nvim_win_get_width ( 0 )
2021-04-08 19:32:05 +02:00
width = math.min ( width , screen_width )
-- make sure borders are always inside the screen
2024-10-15 17:27:27 +01:00
width = math.min ( width , screen_width - border_width )
2021-04-08 19:32:05 +02:00
2024-10-15 17:27:27 +01:00
if wrap_at then
wrap_at = math.min ( wrap_at , width )
2021-04-08 19:32:05 +02:00
end
2020-07-06 03:09:52 +02: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 12:55:26 -08:00
2020-06-04 20:23:03 +02:00
if not height then
height = # contents
2020-07-06 03:09:52 +02:00
if wrap_at and width >= wrap_at then
2020-06-04 20:23:03 +02:00
height = 0
if vim.tbl_isempty ( line_widths ) then
for _ , line in ipairs ( contents ) do
2023-10-12 15:39:39 +08:00
local line_width = vim.fn . strdisplaywidth ( line : gsub ( '%z' , ' \n ' ))
2020-06-04 20:23:03 +02:00
height = height + math.ceil ( line_width / wrap_at )
end
else
for i = 1 , # contents do
2020-07-06 03:09:52 +02:00
height = height + math.max ( 1 , math.ceil ( line_widths [ i ] / wrap_at ))
2020-06-04 20:23:03 +02:00
end
end
end
end
2020-07-06 03:09:52 +02:00
if max_height then
height = math.min ( height , max_height )
end
2020-06-04 20:23:03 +02:00
return width , height
end
2024-02-27 15:20:32 +00:00
--- @class vim.lsp.util.open_floating_preview.Opts
---
--- 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
2024-10-15 17:27:27 +01:00
---
--- offset to add to `col`
--- @field offset_x? integer
---
--- offset to add to `row`
--- @field offset_y? integer
--- @field border? (string|[string,string])[] override `border`
--- @field zindex? integer override `zindex`, defaults to 50
--- @field title? string
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
--- @field relative? 'mouse'|'cursor'
---
--- - "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.
--- (default: `'auto'`)
--- @field anchor_bias? 'auto'|'above'|'below'
2024-02-27 15:20:32 +00:00
2020-08-19 18:17:08 +02:00
--- Shows contents in a floating window.
2020-06-04 20:23:03 +02:00
---
2021-08-22 14:55:28 -06:00
---@param contents table of lines to show in window
---@param syntax string of syntax to set for opened buffer
2024-02-27 15:20:32 +00: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 18:42:37 +08: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 )
2024-10-15 16:56:35 +01:00
validate ( 'contents' , contents , 'table' )
validate ( 'syntax' , syntax , 'string' , true )
validate ( 'opts' , opts , 'table' , true )
2020-06-04 20:23:03 +02:00
opts = opts or {}
2021-05-26 23:47:53 +02: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 15:53:09 +01:00
opts.close_events = opts.close_events or { 'CursorMoved' , 'CursorMovedI' , 'InsertCharPre' }
2021-05-26 23:47:53 +02:00
2022-07-15 18:26:47 +02:00
local bufnr = api.nvim_get_current_buf ()
2021-05-26 23:47:53 +02: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 23:47:53 +02:00
-- Go back to previous window if we are in a focusable one
2022-07-15 18:26:47 +02:00
local current_winnr = api.nvim_get_current_win ()
2024-10-16 13:35:03 +01:00
if vim.w [ current_winnr ][ opts.focus_id ] then
2022-07-15 18:26:47 +02:00
api.nvim_command ( 'wincmd p' )
2021-05-26 23:47:53 +02:00
return bufnr , current_winnr
end
do
local win = find_window_by_var ( opts.focus_id , bufnr )
2022-07-15 18:26:47 +02:00
if win and api.nvim_win_is_valid ( win ) and vim.fn . pumvisible () == 0 then
2021-05-26 23:47:53 +02:00
-- focus and return the existing buf, win
2022-07-15 18:26:47 +02:00
api.nvim_set_current_win ( win )
api.nvim_command ( 'stopinsert' )
return api.nvim_win_get_buf ( win ), win
2021-05-26 23:47:53 +02:00
end
end
end
2021-06-05 10:13:01 +02:00
-- check if another floating preview already exists for this buffer
-- and close it if needed
2024-10-16 13:35:03 +01:00
local existing_float = vim.b [ bufnr ]. lsp_floating_preview
2022-07-15 18:26:47 +02:00
if existing_float and api.nvim_win_is_valid ( existing_float ) then
api.nvim_win_close ( existing_float , true )
2021-06-05 10:13:01 +02:00
end
2023-09-12 20:51:21 -07:00
-- Create the buffer
2022-07-15 18:26:47 +02:00
local floating_bufnr = api.nvim_create_buf ( false , true )
2020-06-04 20:23:03 +02: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 23:47:53 +02: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 23:47:53 +02: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 23:47:53 +02:00
if syntax then
2022-12-19 16:37:45 +00:00
vim.bo [ floating_bufnr ]. syntax = syntax
2021-05-26 23:47:53 +02:00
end
2022-07-15 18:26:47 +02:00
api.nvim_buf_set_lines ( floating_bufnr , 0 , - 1 , true , contents )
2021-05-26 23:47:53 +02:00
end
2020-06-04 20:23:03 +02:00
-- Compute size of float needed to show (wrapped) lines
2021-05-26 23:47:53 +02:00
if opts.wrap then
2022-07-15 18:26:47 +02:00
opts.wrap_at = opts.wrap_at or api.nvim_win_get_width ( 0 )
2021-05-26 23:47:53 +02:00
else
opts.wrap_at = nil
end
2020-06-04 20:23:03 +02:00
local width , height = M._make_floating_popup_size ( contents , opts )
2019-11-13 12:55:26 -08:00
local float_option = M.make_floating_popup_options ( width , height , opts )
2022-07-15 18:26:47 +02:00
local floating_winnr = api.nvim_open_win ( floating_bufnr , false , float_option )
2023-09-12 20:51:21 -07:00
2021-05-26 23:47:53 +02:00
if do_stylize then
2022-12-19 16:37:45 +00:00
vim.wo [ floating_winnr ]. conceallevel = 2
2019-11-13 12:55:26 -08:00
end
2024-10-06 12:20:40 -07:00
vim.wo [ floating_winnr ]. foldenable = false -- Disable folding.
vim.wo [ floating_winnr ]. wrap = opts.wrap -- Soft wrapping.
vim.wo [ floating_winnr ]. breakindent = true -- Slightly better list presentation.
2024-10-11 00:50:09 -07:00
vim.wo [ floating_winnr ]. smoothscroll = true -- Scroll by screen-line instead of buffer-line.
2021-05-26 23:47:53 +02:00
2022-12-19 16:37:45 +00:00
vim.bo [ floating_bufnr ]. modifiable = false
vim.bo [ floating_bufnr ]. bufhidden = 'wipe'
2023-09-12 20:51:21 -07:00
2022-07-15 18:26:47 +02:00
api.nvim_buf_set_keymap (
2021-11-24 23:57:33 +01:00
floating_bufnr ,
'n' ,
'q' ,
'<cmd>bdelete<cr>' ,
{ silent = true , noremap = true , nowait = true }
)
2021-12-15 15:53:09 +01:00
close_preview_autocmd ( opts.close_events , floating_winnr , { floating_bufnr , bufnr })
2021-05-26 23:47:53 +02:00
-- save focus_id
if opts.focus_id then
2022-07-15 18:26:47 +02:00
api.nvim_win_set_var ( floating_winnr , opts.focus_id , bufnr )
2021-05-26 23:47:53 +02:00
end
2022-07-15 18:26:47 +02:00
api.nvim_buf_set_var ( bufnr , 'lsp_floating_preview' , floating_winnr )
2021-05-26 23:47:53 +02:00
2019-11-13 12:55:26 -08:00
return floating_bufnr , floating_winnr
end
2020-11-12 22:21:34 -05:00
do --[[ References ]]
2022-07-15 18:26:47 +02:00
local reference_ns = api.nvim_create_namespace ( 'vim_lsp_references' )
2019-11-13 12:55:26 -08:00
2020-08-19 18:17:08 +02:00
--- Removes document highlights from a buffer.
---
2024-10-15 17:27:27 +01:00
---@param bufnr integer? Buffer id
2020-02-26 20:10:16 +01:00
function M . buf_clear_references ( bufnr )
2022-07-15 18:26:47 +02:00
api.nvim_buf_clear_namespace ( bufnr or 0 , reference_ns , 0 , - 1 )
2020-02-26 20:10:16 +01:00
end
2020-08-19 18:17:08 +02:00
--- Shows a list of document highlights for a certain buffer.
---
2023-03-07 15:17:52 +09:00
---@param bufnr integer Buffer id
2024-05-31 08:41:10 -04:00
---@param references lsp.DocumentHighlight[] objects to highlight
2022-01-13 02:34:04 -08: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 12:14:45 -08:00
function M . buf_highlight_references ( bufnr , references , offset_encoding )
2024-10-15 16:56:35 +01:00
validate ( 'bufnr' , bufnr , 'number' , true )
validate ( 'offset_encoding' , offset_encoding , 'string' , false )
2020-02-26 20:10:16 +01:00
for _ , reference in ipairs ( references ) do
2021-11-07 05: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 12:14:45 -08: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-07 05:05:52 +07:00
2020-02-26 20:10:16 +01:00
local document_highlight_kind = {
[ protocol.DocumentHighlightKind . Text ] = 'LspReferenceText' ,
[ protocol.DocumentHighlightKind . Read ] = 'LspReferenceRead' ,
[ protocol.DocumentHighlightKind . Write ] = 'LspReferenceWrite' ,
}
2020-04-17 00:30:03 +08:00
local kind = reference [ 'kind' ] or protocol.DocumentHighlightKind . Text
2021-11-07 05:05:52 +07:00
highlight.range (
bufnr ,
reference_ns ,
document_highlight_kind [ kind ],
{ start_line , start_idx },
2022-01-15 14:19:20 -08:00
{ end_line , end_idx },
2022-02-21 21:21:42 +01:00
{ priority = vim.highlight . priorities.user }
)
2020-02-26 20:10:16 +01:00
end
end
2019-11-13 12:55:26 -08:00
end
2019-11-24 03:01:18 -08:00
local position_sort = sort_by_key ( function ( v )
2020-01-28 10:45:25 +01:00
return { v.start . line , v.start . character }
2019-11-24 03:01:18 -08:00
end )
2019-11-13 12:55:26 -08:00
2020-08-19 18:17:08 +02:00
--- Returns the items with the byte position calculated correctly and in sorted
--- order, for display in quickfix and location lists.
---
2023-09-07 10:12:02 +02:00
--- The `user_data` field of each resulting item will contain the original
--- `Location` or `LocationLink` it was computed from.
---
2021-09-06 20:21:18 -06:00
--- The result can be passed to the {list} argument of |setqflist()| or
--- |setloclist()|.
---
2023-12-22 11:38:02 +01:00
---@param locations lsp.Location[]|lsp.LocationLink[]
2022-01-13 02:34:04 -08:00
---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32
2023-07-01 18:42:37 +08:00
--- default to first client of buffer
2024-10-15 17:27:27 +01:00
---@return vim.quickfix.entry[] # See |setqflist()| for the format
2022-01-13 02:34:04 -08: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 18:27:16 +02:00
offset_encoding = vim.lsp . get_clients ({ bufnr = 0 })[ 1 ]. offset_encoding
2022-01-13 02:34:04 -08:00
end
2024-10-15 17:27:27 +01:00
local items = {} --- @type vim.quickfix.entry[]
2024-10-16 10:06:59 +01:00
2024-06-10 03:14:55 +02:00
---@type table<string, {start: lsp.Position, end: lsp.Position, location: lsp.Location|lsp.LocationLink}[]>
2024-10-16 10:06:59 +01:00
local grouped = {}
2019-11-13 12:55:26 -08:00
for _ , d in ipairs ( locations ) do
2020-05-02 15:21:07 +02:00
-- locations may be Location or LocationLink
local uri = d.uri or d.targetUri
local range = d.range or d.targetSelectionRange
2024-10-16 10:06:59 +01:00
grouped [ uri ] = grouped [ uri ] or {}
2024-06-10 03:14:55 +02:00
table.insert ( grouped [ uri ], { start = range.start , [ 'end' ] = range [ 'end' ], location = d })
2019-11-24 03:01:18 -08:00
end
2020-01-28 10:45:25 +01:00
2024-10-15 17:27:27 +01:00
for uri in vim.spairs ( grouped ) do
2020-06-30 17:48:04 +02:00
local rows = grouped [ uri ]
2019-11-24 03:01:18 -08:00
table.sort ( rows , position_sort )
2020-06-30 17:48:04 +02:00
local filename = vim.uri_to_fname ( uri )
2021-05-25 12:09:37 +02:00
2023-12-22 11:38:02 +01:00
local line_numbers = {}
2021-05-25 12:09:37 +02:00
for _ , temp in ipairs ( rows ) do
2023-12-22 11:38:02 +01:00
table.insert ( line_numbers , temp.start . line )
2024-06-10 03:14:55 +02:00
if temp.start . line ~= temp [ 'end' ]. line then
table.insert ( line_numbers , temp [ 'end' ]. line )
end
2021-05-25 12:09:37 +02:00
end
-- get all the lines for this uri
2023-12-22 11:38:02 +01:00
local lines = get_lines ( vim.uri_to_bufnr ( uri ), line_numbers )
2021-05-25 12:09:37 +02:00
2020-06-30 17:48:04 +02:00
for _ , temp in ipairs ( rows ) do
local pos = temp.start
2024-06-10 03:14:55 +02:00
local end_pos = temp [ 'end' ]
2020-06-30 17:48:04 +02:00
local row = pos.line
2024-06-10 03:14:55 +02:00
local end_row = end_pos.line
2021-05-25 12:09:37 +02:00
local line = lines [ row ] or ''
2024-09-05 08:23:11 +01:00
local end_line = lines [ end_row ] or ''
2024-09-08 11:44:46 +01:00
local col = M._str_byteindex_enc ( line , pos.character , offset_encoding )
local end_col = M._str_byteindex_enc ( end_line , end_pos.character , offset_encoding )
2024-09-05 08:23:11 +01:00
2024-10-15 17:27:27 +01:00
items [ # items + 1 ] = {
2020-06-30 17:48:04 +02:00
filename = filename ,
lnum = row + 1 ,
2024-06-10 03:14:55 +02:00
end_lnum = end_row + 1 ,
2020-06-30 17:48:04 +02:00
col = col + 1 ,
2024-06-10 03:14:55 +02:00
end_col = end_col + 1 ,
2020-06-30 17:48:04 +02:00
text = line ,
2023-09-07 10:12:02 +02:00
user_data = temp.location ,
2024-10-15 17:27:27 +01:00
}
2019-11-24 03:01:18 -08:00
end
2019-11-13 12:55:26 -08:00
end
2019-11-24 03:01:18 -08:00
return items
end
2020-08-19 18:17:08 +02:00
--- Converts symbols to quickfix list items.
2020-02-22 21:20:38 +09:00
---
2024-10-15 17:27:27 +01:00
---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[]
2024-08-11 11:58:15 +01:00
---@param bufnr? integer
2024-10-15 17:27:27 +01:00
---@return vim.quickfix.entry[] # See |setqflist()| for the format
2020-02-22 21:20:38 +09:00
function M . symbols_to_items ( symbols , bufnr )
2024-10-16 09:58:10 +01:00
bufnr = bufnr or 0
local items = {} --- @type vim.quickfix.entry[]
for _ , symbol in ipairs ( symbols ) do
--- @type string?, lsp.Position?
local filename , pos
if symbol.location then
--- @cast symbol lsp.SymbolInformation
filename = vim.uri_to_fname ( symbol.location . uri )
pos = symbol.location . range.start
elseif symbol.selectionRange then
--- @cast symbol lsp.DocumentSymbol
filename = api.nvim_buf_get_name ( bufnr )
pos = symbol.selectionRange . start
end
if filename and pos then
local kind = protocol.SymbolKind [ symbol.kind ] or 'Unknown'
items [ # items + 1 ] = {
filename = filename ,
lnum = pos.line + 1 ,
col = pos.character + 1 ,
kind = kind ,
text = '[' .. kind .. '] ' .. symbol.name ,
}
end
if symbol.children then
list_extend ( items , M.symbols_to_items ( symbol.children , bufnr ))
2020-02-22 21:20:38 +09:00
end
end
2024-10-16 09:58:10 +01:00
return items
2020-02-22 21:20:38 +09:00
end
2020-08-19 18:17:08 +02: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 18:42:37 +08:00
---@param lines table list of lines to trim
---@return table trimmed list of lines
2019-11-20 15:35:18 -08:00
function M . trim_empty_lines ( lines )
2023-12-25 21:28:28 +01:00
vim.deprecate ( 'vim.lsp.util.trim_empty_lines()' , 'vim.split() with `trimempty`' , '0.12' )
2019-11-20 15:35:18 -08:00
local start = 1
for i = 1 , # lines do
2021-08-10 20:52:30 +02:00
if lines [ i ] ~= nil and # lines [ i ] > 0 then
2019-11-20 15:35:18 -08:00
start = i
break
end
end
local finish = 1
for i = # lines , 1 , - 1 do
2021-08-10 20:52:30 +02:00
if lines [ i ] ~= nil and # lines [ i ] > 0 then
2019-11-20 15:35:18 -08:00
finish = i
break
end
end
2024-10-15 17:27:27 +01:00
return vim.list_slice ( lines , start , finish )
2019-11-20 15:35:18 -08:00
end
2020-08-19 18:17:08 +02: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
2024-05-31 08:41:10 -04:00
---@param lines string[] list of lines
2023-07-01 18:42:37 +08:00
---@return string filetype or "markdown" if it was unchanged.
2019-11-20 15:35:18 -08:00
function M . try_trim_markdown_code_blocks ( lines )
2023-12-25 21:28:28 +01:00
vim.deprecate ( 'vim.lsp.util.try_trim_markdown_code_blocks()' , 'nil' , '0.12' )
2019-11-20 15:35:18 -08: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
2024-10-15 17:27:27 +01:00
---@param window integer?: window handle or 0 for current, defaults to current
---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
2021-12-10 06:17:50 -08:00
local function make_position_param ( window , offset_encoding )
window = window or 0
2022-07-15 18:26:47 +02:00
local buf = api.nvim_win_get_buf ( window )
local row , col = unpack ( api.nvim_win_get_cursor ( window ))
2021-12-10 06:17:50 -08:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
2019-11-21 15:41:32 -08:00
row = row - 1
2022-07-15 18:26:47 +02:00
local line = api.nvim_buf_get_lines ( buf , row , row + 1 , true )[ 1 ]
2020-09-02 06:45:47 +03:00
if not line then
return { line = 0 , character = 0 }
end
2021-12-10 06:17:50 -08:00
2024-09-09 22:39:02 +01:00
col = M._str_utfindex_enc ( line , col , offset_encoding )
2021-12-10 06:17:50 -08:00
2020-05-16 01:18:59 +02:00
return { line = row , character = col }
end
2020-08-19 18:17:08 +02:00
--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
---
2024-10-15 17:27:27 +01:00
---@param window integer?: window handle or 0 for current, defaults to current
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of buffer of `window`
2024-05-31 08:41:10 -04:00
---@return lsp.TextDocumentPositionParams
2021-08-22 14:55:28 -06:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
2021-12-10 06:17:50 -08:00
function M . make_position_params ( window , offset_encoding )
window = window or 0
2022-07-15 18:26:47 +02:00
local buf = api.nvim_win_get_buf ( window )
2021-12-10 06:17:50 -08:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
2019-11-21 15:41:32 -08:00
return {
2021-12-10 06:17:50 -08: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.
2024-05-31 08:41:10 -04:00
---@param bufnr integer buffer handle or 0 for current, defaults to current
2023-07-01 18:42:37 +08:00
---@return string encoding first client if there is one, nil otherwise
2021-12-10 06:17:50 -08:00
function M . _get_offset_encoding ( bufnr )
2024-10-15 16:56:35 +01:00
validate ( 'bufnr' , bufnr , 'number' , true )
2021-12-10 06:17:50 -08:00
2024-10-15 17:27:27 +01:00
local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'?
2021-12-10 06:17:50 -08:00
2023-07-17 18:27:16 +02:00
for _ , client in pairs ( vim.lsp . get_clients ({ bufnr = bufnr })) do
2022-01-13 02:34:04 -08: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 06:17:50 -08: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 06:17:50 -08: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-16 01:18:59 +02:00
end
2020-08-19 18:17:08 +02: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`.
---
2024-10-15 17:27:27 +01:00
---@param window integer? window handle or 0 for current, defaults to current
---@param offset_encoding "utf-8"|"utf-16"|"utf-32"? defaults to `offset_encoding` of first client of buffer of `window`
2023-07-01 18:42:37 +08:00
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
2020-08-19 18:17:08 +02:00
---`current_position`, end = `current_position` } }
2021-12-10 06:17:50 -08:00
function M . make_range_params ( window , offset_encoding )
2022-07-15 18:26:47 +02:00
local buf = api.nvim_win_get_buf ( window or 0 )
2021-12-10 06:17:50 -08:00
offset_encoding = offset_encoding or M._get_offset_encoding ( buf )
local position = make_position_param ( window , offset_encoding )
2020-05-16 01:18:59 +02:00
return {
2021-12-10 06:17:50 -08:00
textDocument = M.make_text_document_params ( buf ),
2020-05-16 01:18:59 +02:00
range = { start = position , [ 'end' ] = position },
2019-11-21 15:41:32 -08:00
}
end
2020-09-25 04:53:08 +09:00
--- Using the given range in the current buffer, creates an object that
--- is similar to |vim.lsp.util.make_range_params()|.
---
2024-10-15 17:27:27 +01:00
---@param start_pos [integer,integer]? {row,col} mark-indexed position.
2022-07-28 19:19:07 +02:00
--- Defaults to the start of the last visual selection.
2024-10-15 17:27:27 +01:00
---@param end_pos [integer,integer]? {row,col} mark-indexed position.
2022-07-28 19:19:07 +02:00
--- Defaults to the end of the last visual selection.
2024-10-15 17:27:27 +01:00
---@param bufnr integer? buffer handle or 0 for current, defaults to current
---@param offset_encoding 'utf-8'|'utf-16'|'utf-32'? defaults to `offset_encoding` of first client of `bufnr`
2023-07-01 18:42:37 +08:00
---@return table { textDocument = { uri = `current_file_uri` }, range = { start =
2020-09-25 04:53:08 +09:00
---`start_position`, end = `end_position` } }
2021-12-10 06:17:50 -08:00
function M . make_given_range_params ( start_pos , end_pos , bufnr , offset_encoding )
2024-10-15 16:56:35 +01:00
validate ( 'start_pos' , start_pos , 'table' , true )
validate ( 'end_pos' , end_pos , 'table' , true )
validate ( 'offset_encoding' , offset_encoding , 'string' , true )
2022-07-15 18:26:47 +02:00
bufnr = bufnr or api.nvim_get_current_buf ()
2021-12-10 06:17:50 -08:00
offset_encoding = offset_encoding or M._get_offset_encoding ( bufnr )
2024-10-15 17:27:27 +01:00
--- @type integer[]
2022-07-15 18:26:47 +02:00
local A = list_extend ({}, start_pos or api.nvim_buf_get_mark ( bufnr , '<' ))
2024-10-15 17:27:27 +01:00
--- @type integer[]
2022-07-15 18:26:47 +02:00
local B = list_extend ({}, end_pos or api.nvim_buf_get_mark ( bufnr , '>' ))
2020-09-25 04:53:08 +09:00
-- convert to 0-index
A [ 1 ] = A [ 1 ] - 1
B [ 1 ] = B [ 1 ] - 1
2021-12-10 06:17:50 -08:00
-- account for offset_encoding.
2020-09-25 04:53:08 +09:00
if A [ 2 ] > 0 then
2021-12-10 06:17:50 -08:00
A = { A [ 1 ], M.character_offset ( bufnr , A [ 1 ], A [ 2 ], offset_encoding ) }
2020-09-25 04:53:08 +09:00
end
if B [ 2 ] > 0 then
2021-12-10 06:17:50 -08:00
B = { B [ 1 ], M.character_offset ( bufnr , B [ 1 ], B [ 2 ], offset_encoding ) }
2020-09-25 04:53:08 +09:00
end
2021-05-14 18:33:23 +02: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-25 04:53:08 +09:00
return {
2021-12-10 06:17:50 -08:00
textDocument = M.make_text_document_params ( bufnr ),
2020-09-25 04:53:08 +09:00
range = {
start = { line = A [ 1 ], character = A [ 2 ] },
[ 'end' ] = { line = B [ 1 ], character = B [ 2 ] },
},
}
end
2020-08-19 18:17:08 +02:00
--- Creates a `TextDocumentIdentifier` object for the current buffer.
---
2024-10-15 17:27:27 +01:00
---@param bufnr integer?: Buffer handle, defaults to current
2024-05-31 08:41:10 -04:00
---@return lsp.TextDocumentIdentifier
2021-08-22 14:55:28 -06:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
2021-12-10 06:17:50 -08:00
function M . make_text_document_params ( bufnr )
2023-03-11 07:35:23 +01:00
return { uri = vim.uri_from_bufnr ( bufnr or 0 ) }
2020-02-22 21:20:38 +09:00
end
2020-12-08 21:09:33 -05:00
--- Create the workspace params
2024-05-31 08:41:10 -04:00
---@param added lsp.WorkspaceFolder[]
---@param removed lsp.WorkspaceFolder[]
---@return lsp.WorkspaceFoldersChangeEvent
2020-11-25 12:07:02 -08:00
function M . make_workspace_params ( added , removed )
return { event = { added = added , removed = removed } }
end
2023-08-31 04:00:24 -04:00
2022-03-20 13:41:46 -04:00
--- Returns indentation size.
2020-05-05 17:23:45 +02:00
---
2022-09-25 19:58:27 -04:00
---@see 'shiftwidth'
2024-10-15 17:27:27 +01:00
---@param bufnr integer?: Buffer handle, defaults to current
2024-05-31 08:41:10 -04:00
---@return integer indentation size
2020-05-05 17:23:45 +02:00
function M . get_effective_tabstop ( bufnr )
2024-10-15 16:56:35 +01:00
validate ( 'bufnr' , bufnr , 'number' , true )
2020-05-05 17:23:45 +02:00
local bo = bufnr and vim.bo [ bufnr ] or vim.bo
2022-03-20 13:41:46 -04:00
local sw = bo.shiftwidth
return ( sw == 0 and bo.tabstop ) or sw
2020-05-05 17:23:45 +02:00
end
2021-07-17 12:15:57 +03:00
--- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
2020-08-19 18:17:08 +02:00
---
2024-10-15 17:27:27 +01:00
---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries
2023-12-13 12:00:11 +00:00
---@return lsp.DocumentFormattingParams object
2021-08-22 14:55:28 -06:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
2020-05-05 17:23:45 +02:00
function M . make_formatting_params ( options )
2024-10-15 16:56:35 +01:00
validate ( 'options' , options , 'table' , true )
2020-05-05 17:23:45 +02:00
options = vim.tbl_extend ( 'keep' , options or {}, {
tabSize = M.get_effective_tabstop (),
insertSpaces = vim.bo . expandtab ,
})
return {
2023-03-11 07:35:23 +01:00
textDocument = { uri = vim.uri_from_bufnr ( 0 ) },
2020-05-05 17:23:45 +02:00
options = options ,
}
end
2020-08-19 18:17:08 +02:00
--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
---
2023-03-07 15:17:52 +09:00
---@param buf integer buffer number (0 for current)
2024-01-09 17:36:46 +00:00
---@param row integer 0-indexed line
---@param col integer 0-indexed byte offset in line
2023-07-01 18:42:37 +08: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 06:17:50 -08:00
function M . character_offset ( buf , row , col , offset_encoding )
local line = get_line ( buf , row )
2022-01-13 02:34:04 -08: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 18:27:16 +02:00
offset_encoding = vim.lsp . get_clients ({ bufnr = buf })[ 1 ]. offset_encoding
2022-01-13 02:34:04 -08:00
end
2024-09-09 22:39:02 +01:00
return M._str_utfindex_enc ( line , col , offset_encoding )
2019-11-21 16:23:12 -08:00
end
2019-11-20 15:35:18 -08:00
2020-12-30 16:05:10 -08:00
--- Helper function to return nested values in language server settings
---
2023-07-01 18:42:37 +08:00
---@param settings table language server settings
---@param section string indicating the field of the settings table
2024-01-14 22:37:07 -05:00
---@return table|string|vim.NIL The value of settings accessed via section. `vim.NIL` if not found.
2024-01-14 23:12:54 -05:00
---@deprecated
2020-12-30 16:05:10 -08:00
function M . lookup_section ( settings , section )
2024-01-14 23:12:54 -05:00
vim.deprecate ( 'vim.lsp.util.lookup_section()' , 'vim.tbl_get() with `vim.split`' , '0.12' )
2023-07-01 18:42:37 +08:00
for part in vim.gsplit ( section , '.' , { plain = true }) do
2024-10-15 17:27:27 +01:00
--- @diagnostic disable-next-line:no-unknown
2020-12-30 16:05:10 -08:00
settings = settings [ part ]
2022-04-15 11:12:41 +02:00
if settings == nil then
return vim.NIL
2020-12-30 16:05:10 -08:00
end
end
return settings
end
2023-09-21 14:06:40 +04: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
2024-10-16 09:45:12 +01:00
if end_line == last_line and not vim.bo [ bufnr ]. endofline then
2023-09-21 14:06:40 +04:00
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 15:20:32 +00:00
---@class (private) vim.lsp.util._refresh.Opts
2023-08-31 04:00:24 -04: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 15:20:32 +00:00
---@private
--- Request updated LSP information for a buffer.
---
2023-07-20 03:03:48 -04:00
---@param method string LSP method to call
2024-02-27 15:20:32 +00:00
---@param opts? vim.lsp.util._refresh.Opts Options table
2023-07-20 03:03:48 -04: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 04:00:24 -04:00
local clients = vim.lsp . get_clients ({ bufnr = bufnr , method = method , id = opts.client_id })
if # clients == 0 then
return
end
2023-09-21 14:06:40 +04:00
local textDocument = M.make_text_document_params ( bufnr )
2023-07-20 03:03:48 -04:00
local only_visible = opts.only_visible or false
2023-08-31 04:00:24 -04: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 14:06:40 +04:00
client.request ( method , {
textDocument = textDocument ,
range = make_line_range_params ( bufnr , first - 1 , last - 1 , client.offset_encoding ),
}, nil , bufnr )
2023-08-31 04:00:24 -04:00
end
end
2023-07-20 03:03:48 -04:00
end
2023-08-31 04:00:24 -04:00
else
for _ , client in ipairs ( clients ) do
2023-09-21 14:06:40 +04: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 04:00:24 -04:00
end
2023-07-20 03:03:48 -04:00
end
end
2024-05-30 10:46:26 +02:00
M._get_line_byte_from_position = get_line_byte_from_position
2020-01-24 12:31:52 +01:00
2024-06-07 11:36:46 +02:00
---@nodoc
2024-06-14 05:03:58 -04:00
---@type table<integer,integer>
M.buf_versions = setmetatable ({}, {
__index = function ( t , bufnr )
return rawget ( t , bufnr ) or 0
end ,
})
2024-06-07 11:36:46 +02:00
2019-11-13 12:55:26 -08:00
return M