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
2023-06-03 11:06:00 +01:00
local uv = vim.uv
2026-04-29 15:13:47 -05:00
local isnil = require ( 'vim._core.util' ). isnil
2019-11-13 12:55:26 -08:00
local M = {}
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 },
2025-04-09 16:45:33 +05:30
bold = { 2 , 2 },
2024-10-16 12:49:14 +01:00
}
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 )
2025-03-19 05:05:35 +08:00
local border = opts and opts.border or vim.o . winborder
if border == '' then
border = 'none'
end
2021-04-08 19:32:05 +02:00
2025-07-10 14:51:13 +02:00
-- Convert winborder string option with custom characters into a table
if type ( border ) == 'string' and border : find ( ',' ) then
border = vim.split ( border , ',' )
end
2021-04-08 19:32:05 +02:00
if type ( border ) == 'string' then
2024-10-16 12:49:14 +01:00
if not border_size [ border ] then
border_error ( border )
2021-08-27 19:19:17 +08:00
end
2025-01-24 13:01:25 +00:00
local r = border_size [ border ]
return r [ 1 ], r [ 2 ]
2024-10-16 12:49:14 +01:00
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
2025-06-05 11:31:51 +01:00
--- @return integer
2024-10-16 12:49:14 +01:00
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
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 )
2025-10-05 14:39:23 -07: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 = ''
2025-06-05 11:31:51 +01:00
local suffix = assert ( lines [ i_n ]): sub ( B [ 2 ] + 1 )
2019-11-21 15:19:06 -08:00
if A [ 2 ] > 0 then
2025-06-05 11:31:51 +01:00
prefix = assert ( 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
2025-06-05 11:31:51 +01:00
---@return table<integer, 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
2024-12-07 13:05:05 +00:00
bufnr = vim._resolve_bufnr ( bufnr )
2022-01-08 02:30:59 -08:00
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
2025-06-05 11:31:51 +01:00
return {}
2021-11-25 02:49:34 +01:00
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-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2023-07-01 18:42:37 +08:00
---@return integer
2024-11-26 00:06:05 +08:00
local function get_line_byte_from_position ( bufnr , position , position_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-11-26 00:06:05 +08:00
return vim.str_byteindex ( line , position_encoding , col , false )
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
2026-04-23 19:24:04 -04:00
--- Applies a list of text edits to a buffer. Note: this mutates `text_edits` (sorts in-place and
--- adds `_index` fields).
---
2025-06-23 06:30:49 -07:00
---@param text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit)[]
2023-03-07 15:17:52 +09:00
---@param bufnr integer Buffer id
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2025-06-23 06:30:49 -07:00
---@param change_annotations? table<string, lsp.ChangeAnnotation>
2021-08-09 19:45:00 +02:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
2025-06-23 06:30:49 -07:00
function M . apply_text_edits ( text_edits , bufnr , position_encoding , change_annotations )
2024-10-15 16:56:35 +01:00
validate ( 'text_edits' , text_edits , 'table' , false )
validate ( 'bufnr' , bufnr , 'number' , false )
2024-11-26 00:06:05 +08:00
validate ( 'position_encoding' , position_encoding , 'string' , false )
2025-06-23 06:30:49 -07:00
validate ( 'change_annotations' , change_annotations , 'table' , true )
2024-10-15 16:56:35 +01:00
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
2019-11-21 15:19:06 -08:00
2025-06-23 06:30:49 -07:00
local marks = {} --- @type table<string,[integer,integer]>
local has_eol_text_edit = false
2021-09-19 05:19:21 +09:00
2025-06-23 06:30:49 -07:00
local function apply_text_edits ()
-- Fix reversed range and indexing each text_edits
for index , text_edit in ipairs ( text_edits ) do
--- @cast text_edit lsp.TextEdit|{_index: integer}
2026-04-23 19:24:04 -04:00
-- XXX: Preserve existing _index to avoid surprises if the same edit is reapplied. #39344
if text_edit._index == nil then
text_edit._index = index
end
2025-06-23 06:30:49 -07:00
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
2021-09-19 05:19:21 +09:00
end
2025-06-23 06:30:49 -07:00
--- @cast text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})[]
2025-06-05 11:31:51 +01:00
2025-06-23 06:30:49 -07:00
-- Sort text_edits
---@param a (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})
---@param b (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})
---@return boolean
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
return a._index > b._index
end )
-- save and restore local marks since they get deleted by nvim_buf_set_lines
for _ , m in pairs ( vim.fn . getmarklist ( bufnr )) do
if m.mark : match ( "^'[a-z]$" ) then
marks [ m.mark : sub ( 2 , 2 )] = { m.pos [ 2 ], m.pos [ 3 ] - 1 } -- api-indexed
end
2021-09-19 05:19:21 +09:00
end
2025-06-23 06:30:49 -07:00
for _ , text_edit in ipairs ( text_edits ) do
-- Normalize line ending
text_edit.newText , _ = string.gsub ( text_edit.newText , ' \r\n ?' , ' \n ' )
-- Convert from LSP style ranges to Neovim style ranges.
local start_row = text_edit.range . start.line
local start_col = get_line_byte_from_position ( bufnr , text_edit.range . start , position_encoding )
local end_row = text_edit.range [ 'end' ]. line
local end_col = get_line_byte_from_position ( bufnr , text_edit.range [ 'end' ], position_encoding )
local text = vim.split ( text_edit.newText , ' \n ' , { plain = true })
local max = api.nvim_buf_line_count ( bufnr )
-- If the whole edit is after the lines in the buffer we can simply add the new text to the end
-- of the buffer.
if max <= start_row then
api.nvim_buf_set_lines ( bufnr , max , max , false , text )
else
local last_line_len = # ( get_line ( bufnr , math.min ( end_row , max - 1 )) or '' )
-- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
-- accept it so we should fix it here.
if max <= end_row then
end_row = max - 1
end_col = last_line_len
has_eol_text_edit = true
else
-- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the
-- replacement text ends with a newline We can likely assume that the replacement is assumed
-- to be meant to replace the newline with another newline and we need to make sure this
-- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
-- in the file some servers (clangd on windows) will include that character in the line
-- while nvim_buf_set_text doesn't count it as part of the line.
if
end_col >= last_line_len
and text_edit.range [ 'end' ]. character > end_col
and # text_edit.newText > 0
and string.sub ( text_edit.newText , - 1 ) == ' \n '
then
table.remove ( text , # text )
end
end
-- Make sure we don't go out of bounds for end_col
end_col = math.min ( last_line_len , end_col )
api.nvim_buf_set_text ( bufnr , start_row , start_col , end_row , end_col , text )
end
2023-06-05 01:45:01 +02:00
end
end
2025-06-23 06:30:49 -07:00
--- Track how many times each change annotation is applied to build up the final description.
---@type table<string, integer>
local change_count = {}
-- If there are any annotated text edits, we need to confirm them before applying the edits.
local confirmations = {} ---@type table<string, integer>
2021-09-19 05:19:21 +09:00
for _ , text_edit in ipairs ( text_edits ) do
2025-06-23 06:30:49 -07:00
if text_edit.annotationId then
assert (
change_annotations ~= nil ,
'change_annotations must be provided for annotated text edits'
)
local annotation = assert (
change_annotations [ text_edit.annotationId ],
string.format ( 'No change annotation found for ID: %s' , text_edit.annotationId )
)
if annotation.needsConfirmation then
confirmations [ text_edit.annotationId ] = ( confirmations [ text_edit.annotationId ] or 0 ) + 1
2022-01-13 18:28:13 +09:00
end
2022-09-20 22:14:58 +02:00
2025-06-23 06:30:49 -07:00
change_count [ text_edit.annotationId ] = ( change_count [ text_edit.annotationId ] or 0 ) + 1
end
end
if next ( confirmations ) then
local message = { 'Apply all changes?' }
for id , count in pairs ( confirmations ) do
local annotation = assert ( change_annotations )[ id ]
message [ # message + 1 ] = annotation.label
.. ( annotation.description and ( string.format ( ': %s' , annotation.description )) or '' )
.. ( count > 1 and string.format ( ' (%d)' , count ) or '' )
end
local response = vim.fn . confirm ( table.concat ( message , ' \n ' ), '&Yes \n &No' , 1 , 'Question' )
if response == 1 then
-- Proceed with applying text edits.
apply_text_edits ()
else
-- Don't apply any text edits.
return
end
else
-- No confirmations needed, apply text edits directly.
apply_text_edits ()
end
if change_annotations ~= nil and next ( change_count ) then
local change_message = { 'Applied changes:' }
for id , count in pairs ( change_count ) do
local annotation = change_annotations [ id ]
change_message [ # change_message + 1 ] = annotation.label
.. ( annotation.description and ( ': ' .. annotation.description ) or '' )
.. ( count > 1 and string.format ( ' (%d)' , count ) or '' )
2021-09-19 05:19:21 +09:00
end
2025-06-23 06:30:49 -07:00
vim.notify ( table.concat ( change_message , ' \n ' ), vim.log . levels.INFO )
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-11-26 00:06:05 +08:00
---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
2025-06-23 06:30:49 -07:00
---@param change_annotations? table<string, lsp.ChangeAnnotation>
2021-01-11 11:39:11 -05:00
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
2025-06-23 06:30:49 -07:00
function M . apply_text_document_edit (
text_document_edit ,
index ,
position_encoding ,
change_annotations
)
2019-11-20 20:51:44 -08:00
local text_document = text_document_edit.textDocument
local bufnr = vim.uri_to_bufnr ( text_document.uri )
2024-11-26 00:06:05 +08:00
if position_encoding == nil then
2022-01-13 02:34:04 -08:00
vim.notify_once (
2024-11-26 00:06:05 +08:00
'apply_text_document_edit must be called with valid position encoding' ,
2022-01-13 02:34:04 -08:00
vim.log . levels.WARN
)
2024-10-16 13:17:10 +01:00
return
2021-01-11 11:39:11 -05:00
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
2024-10-16 13:38:29 +01:00
-- For lists of text document edits,
-- do not check the version after the first edit.
not ( index and index > 1 )
2021-01-11 11:39:11 -05:00
and (
2025-08-03 17:42:44 +03:00
text_document.version ~= vim.NIL
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
2025-06-23 06:30:49 -07:00
M.apply_text_edits ( text_document_edit.edits , bufnr , position_encoding , change_annotations )
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
2025-08-26 15:31:16 +02:00
local old_fname_full = uv.fs_realpath ( vim.fs . normalize ( old_fname , { expand_env = false }))
2024-02-25 00:47:34 +09:00
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' )
2026-03-12 15:04:05 +00:00
assert ( os.rename ( old_fname_full , new_fname ))
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-11-26 00:06:05 +08:00
---@param position_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
2024-11-26 00:06:05 +08:00
function M . apply_workspace_edit ( workspace_edit , position_encoding )
if position_encoding == nil then
2022-01-13 02:34:04 -08:00
vim.notify_once (
2024-11-26 00:06:05 +08:00
'apply_workspace_edit must be called with valid position encoding' ,
2022-01-13 02:34:04 -08:00
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
2026-04-09 05:10:52 +08:00
local bufnr = vim.uri_to_bufnr ( change.textDocument . uri )
2025-06-23 06:30:49 -07:00
M.apply_text_document_edit ( change , idx , position_encoding , workspace_edit.changeAnnotations )
2026-04-09 05:10:52 +08:00
-- avoid triggering OptionSet
if not vim.bo [ bufnr ]. buflisted then
vim.bo [ bufnr ]. buflisted = true
end
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 )
2025-06-23 06:30:49 -07:00
M.apply_text_edits ( changes , bufnr , position_encoding , workspace_edit.changeAnnotations )
2026-04-09 05:10:52 +08:00
-- avoid triggering OptionSet
if not vim.bo [ bufnr ]. buflisted then
vim.bo [ bufnr ]. buflisted = true
end
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
2024-11-01 12:30:36 +00:00
local signature = vim.deepcopy ( 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
2025-08-07 05:56:02 +03:00
-- Add delimiter if there is documentation to display
if signature.documentation . value ~= '' then
contents [ # contents + 1 ] = '---'
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
2025-08-12 00:31:57 +03:00
local active_parameter = signature.activeParameter or signature_help.activeParameter
2021-07-09 12:38:29 +02:00
2025-08-12 00:31:57 +03:00
-- NOTE: We intentionally violate the LSP spec, which states that if `activeParameter`
-- is not provided or is out-of-bounds, it should default to 0.
-- Instead, we default to `nil`, as most clients do. In practice, 'no active parameter'
-- is better default than 'first parameter' and aligns better with user expectations.
-- Related discussion: https://github.com/microsoft/language-server-protocol/issues/1271
if
not active_parameter
or active_parameter == vim.NIL
or active_parameter < 0
or active_parameter >= # signature.parameters
then
return contents , nil
end
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
2025-08-26 15:31:16 +02:00
offset = offset + # plabel + 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-11-01 12:30:36 +00:00
local a_start = get_pos_from_offset ( active_offset [ 1 ], contents )
local a_end = get_pos_from_offset ( active_offset [ 2 ], contents )
if a_start and a_end then
active_hl = { a_start [ 1 ], a_start [ 2 ], a_end [ 1 ], a_end [ 2 ] }
end
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
2024-11-01 12:30:36 +00:00
---@return vim.api.keyset.win_config
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-18 03:20:58 +03:00
validate ( 'opts.offset_x' , opts.offset_x , 'number' , true )
validate ( 'opts.offset_y' , opts.offset_y , 'number' , 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
2025-08-06 21:25:55 +08:00
local title = (( opts.border or vim.o . winborder ~= '' ) 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 ,
2025-01-08 00:09:01 +08:00
row = row + ( opts.offset_y or 0 ),
2019-11-13 12:55:26 -08:00
col = col + ( opts.offset_x or 0 ),
height = height ,
2021-06-01 13:33:07 +06:00
focusable = opts.focusable ,
2025-01-09 09:26:45 -08:00
relative = ( opts.relative == 'mouse' or opts.relative == 'editor' ) and opts.relative
2025-01-08 00:09:01 +08:00
or 'cursor' ,
2019-11-13 12:55:26 -08:00
style = 'minimal' ,
width = width ,
2025-03-19 05:05:35 +08:00
border = opts.border ,
2025-01-06 15:05:50 +01:00
zindex = opts.zindex or ( api.nvim_win_get_config ( 0 ). zindex or 49 ) + 1 ,
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-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
2024-10-15 17:27:27 +01:00
---@param opts? vim.lsp.util.show_document.Opts
2022-10-08 05:22:25 -03:00
---@return boolean `true` if succeeded
2024-11-26 00:06:05 +08:00
function M . show_document ( location , position_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
2024-11-26 00:06:05 +08:00
if position_encoding == nil then
vim.notify_once (
'show_document must be called with valid position 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
2024-11-26 00:06:05 +08:00
local col = get_line_byte_from_position ( bufnr , range.start , position_encoding )
2022-10-08 05:22:25 -03:00
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 )
2026-04-12 17:46:24 +02:00
-- nvim_win_set_cursor clamps to last char at EOL. In insert mode the cursor
-- should be past the last char (append position).
if vim.api . nvim_get_mode (). mode == 'i' then
local line = api.nvim_buf_get_lines ( bufnr , row , row + 1 , false )[ 1 ] or ''
if col >= # line then
2026-04-14 06:09:54 -04:00
vim.api . nvim_feedkeys ( vim.keycode ( '<End>' ), 'n' , false )
2026-04-12 17:46:24 +02:00
end
end
2022-10-08 05:22:25 -03:00
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-10-26 14:06:15 -07:00
---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead
2024-05-31 08:41:10 -04:00
---@param location lsp.Location|lsp.LocationLink
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
2024-10-15 17:27:27 +01:00
---@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
2024-11-26 00:06:05 +08:00
function M . jump_to_location ( location , position_encoding , reuse_win )
2024-10-20 23:40:44 +02:00
vim.deprecate ( 'vim.lsp.util.jump_to_location' , nil , '0.12' )
2024-11-26 00:06:05 +08:00
return M.show_document ( location , position_encoding , { reuse_win = reuse_win , focus = true })
2022-10-08 05:22:25 -03:00
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.
2025-08-05 23:34:14 +03:00
---@see https://github.github.com/gfm/#thematic-break
2023-09-12 20:51:21 -07:00
---@param line string
---@return boolean
local function is_separator_line ( line )
2025-08-05 23:34:14 +03:00
local i = 1
-- 1. Skip up to 3 leading spaces
local leading_spaces = 3
while i <= # line and line : byte ( i ) == string.byte ( ' ' ) and leading_spaces > 0 do
i = i + 1
leading_spaces = leading_spaces - 1
end
-- 2. Determine the delimiter character
local delimiter = line : byte ( i ) -- nil if i > #line
if
delimiter ~= string.byte ( '-' )
and delimiter ~= string.byte ( '_' )
and delimiter ~= string.byte ( '*' )
then
return false
end
local ndelimiters = 1
i = i + 1
-- 3. Iterate until found non-whitespace or other than expected delimiter
while i <= # line do
local char = line : byte ( i )
if char == delimiter then
ndelimiters = ndelimiters + 1
elseif not ( char == string.byte ( ' ' ) or char == string.byte ( ' \t ' )) then
return false
end
i = i + 1
end
return ndelimiters >= 3
2023-09-12 20:51:21 -07:00
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
2025-04-02 11:00:50 +01:00
--- @deprecated
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
--- - max_height maximal height of floating window
2026-04-26 17:07:32 +02:00
--- - max_width maximal width of floating window
2020-11-12 22:21:34 -05:00
--- - separator insert separator after code block
2026-04-26 17:07:32 +02:00
--- - width of floating window
--- - wrap_at character to wrap at for computing height
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 )
2025-04-02 11:00:50 +01:00
vim.deprecate ( 'vim.lsp.util.stylize_markdown' , nil , '0.14' )
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
2025-04-02 11:24:38 +01:00
local stripped = {} --- @type string[]
2024-10-15 17:27:27 +01:00
local highlights = {} --- @type {ft:string,start:integer,finish:integer}[]
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
2025-04-02 11:24:38 +01:00
stripped [ # stripped + 1 ] = 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
2025-04-02 11:24:38 +01:00
stripped [ # stripped + 1 ] = '---'
2024-10-16 09:48:27 +01:00
end
else
-- strip any empty lines or separators prior to this separator in actual markdown
if line : match ( '^---+$' ) then
while
2025-04-02 11:24:38 +01:00
stripped [ # stripped ]
2024-10-16 09:48:27 +01:00
and ( stripped [ # stripped ]: match ( '^%s*$' ) or stripped [ # stripped ]: match ( '^---+$' ))
do
2025-04-02 11:24:38 +01:00
stripped [ # stripped ] = nil
2024-10-16 09:48:27 +01:00
end
end
-- add the line if its not an empty line following a separator
if
2025-04-02 11:24:38 +01:00
not ( line : match ( '^%s*$' ) and stripped [ # stripped ] and stripped [ # stripped ]: match ( '^---+$' ))
2024-10-16 09:48:27 +01:00
then
2025-04-02 11:24:38 +01:00
stripped [ # stripped + 1 ] = line
2024-10-16 09:48:27 +01:00
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 ))
2025-04-02 11:24:38 +01:00
for l in ipairs ( stripped ) do
2021-06-15 00:41:07 +02:00
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)
2025-08-05 23:34:14 +03:00
--- specification, as required by the LSP.
2023-09-12 20:51:21 -07:00
---
--- 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
---
---@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
2025-08-05 23:34:14 +03:00
---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#markupContent
2023-09-12 20:51:21 -07:00
---@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
2025-03-14 15:23:36 -04:00
local augroup = 'nvim.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
2025-07-16 06:12:45 +03:00
---@param floating_bufnr integer floating preview buffer
---@param bufnr integer buffer that opened the floating preview buffer
2023-07-01 18:42:37 +08:00
---@see autocmd-events
2025-07-16 06:12:45 +03:00
local function close_preview_autocmd ( events , winnr , floating_bufnr , bufnr )
2025-01-13 19:45:11 -08:00
local augroup = api.nvim_create_augroup ( 'nvim.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
2025-07-08 20:21:09 +08:00
api.nvim_create_autocmd ( 'BufLeave' , {
2022-07-10 00:40:32 +08:00
group = augroup ,
2026-04-08 21:23:13 -04:00
buf = bufnr ,
2022-07-10 00:40:32 +08:00
callback = function ()
2025-07-08 20:21:09 +08:00
vim.schedule ( function ()
-- When jumping to the quickfix window from the preview window,
-- do not close the preview window.
if api.nvim_get_option_value ( 'filetype' , { buf = 0 }) ~= 'qf' then
2025-07-16 06:12:45 +03:00
close_preview_window ( winnr , { floating_bufnr , bufnr })
2025-07-08 20:21:09 +08:00
end
end )
2022-07-10 00:40:32 +08:00
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 ,
2026-04-08 21:23:13 -04:00
buf = bufnr ,
2022-07-10 00:40:32 +08:00
callback = function ()
close_preview_window ( winnr )
end ,
})
end
end
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
2025-02-22 13:19:00 -08:00
-- Make sure that the width is large enough to fit the title.
2025-03-22 10:43:06 +08:00
local title_length = 0
local chunks = type ( opts.title ) == 'string' and { { opts.title } } or opts.title or {}
for _ , chunk in
ipairs ( chunks --[=[@as [string, string][]]=] )
do
title_length = title_length + vim.fn . strdisplaywidth ( chunk [ 1 ])
2025-02-22 13:19:00 -08:00
end
2025-03-22 10:43:06 +08:00
width = math.max ( width , title_length )
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 ' ))
2025-01-14 07:17:23 +08:00
height = height + math.max ( 1 , math.ceil ( line_width / wrap_at ))
2020-06-04 20:23:03 +02:00
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
2024-11-01 10:03:09 +01:00
--- @field border? string|(string|[string,string])[] override `border`
2024-10-15 17:27:27 +01:00
--- @field zindex? integer override `zindex`, defaults to 50
2025-03-22 10:43:06 +08:00
--- @field title? string|[string,string][]
2024-10-15 17:27:27 +01:00
--- @field title_pos? 'left'|'center'|'right'
---
--- (default: `'cursor'`)
2025-01-08 00:09:01 +08:00
--- @field relative? 'mouse'|'cursor'|'editor'
2024-10-15 17:27:27 +01:00
---
2025-03-02 14:27:52 -08:00
--- Adjusts placement relative to cursor.
2024-10-15 17:27:27 +01:00
--- - "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-11-01 12:30:36 +00:00
---
--- @field _update_win? integer
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
2024-11-01 12:30:36 +00:00
local floating_winnr = opts._update_win
-- Create/get the buffer
local floating_bufnr --- @type integer
if floating_winnr then
floating_bufnr = api.nvim_win_get_buf ( floating_winnr )
else
-- check if this popup is focusable and we need to focus
if opts.focus_id and opts.focusable ~= false and opts.focus then
-- Go back to previous window if we are in a focusable one
local current_winnr = api.nvim_get_current_win ()
if vim.w [ current_winnr ][ opts.focus_id ] then
api.nvim_command ( 'wincmd p' )
return bufnr , current_winnr
end
do
local win = find_window_by_var ( opts.focus_id , bufnr )
if win and api.nvim_win_is_valid ( win ) and vim.fn . pumvisible () == 0 then
-- focus and return the existing buf, win
api.nvim_set_current_win ( win )
api.nvim_command ( 'stopinsert' )
return api.nvim_win_get_buf ( win ), win
end
2021-05-26 23:47:53 +02:00
end
end
2024-11-01 12:30:36 +00:00
-- check if another floating preview already exists for this buffer
-- and close it if needed
local existing_float = vim.b [ bufnr ]. lsp_floating_preview
if existing_float and api.nvim_win_is_valid ( existing_float ) then
api.nvim_win_close ( existing_float , true )
end
floating_bufnr = api.nvim_create_buf ( false , true )
2021-06-05 10:13:01 +02:00
end
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
2024-11-01 12:30:36 +00:00
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 })
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
end
2024-11-01 12:30:36 +00:00
vim.bo [ floating_bufnr ]. modifiable = true
api.nvim_buf_set_lines ( floating_bufnr , 0 , - 1 , false , contents )
if floating_winnr then
api.nvim_win_set_config ( floating_winnr , {
border = opts.border ,
title = opts.title ,
})
2021-05-26 23:47:53 +02:00
else
2024-11-01 12:30:36 +00:00
-- Compute size of float needed to show (wrapped) lines
if opts.wrap then
opts.wrap_at = opts.wrap_at or api.nvim_win_get_width ( 0 )
else
opts.wrap_at = nil
end
-- TODO(lewis6991): These function assume the current window to determine options,
-- therefore it won't work for opts._update_win and the current window if the floating
-- window
local width , height = M._make_floating_popup_size ( contents , opts )
local float_option = M.make_floating_popup_options ( width , height , opts )
2020-06-04 20:23:03 +02:00
2024-11-01 12:30:36 +00:00
floating_winnr = api.nvim_open_win ( floating_bufnr , false , float_option )
api.nvim_buf_set_keymap (
floating_bufnr ,
'n' ,
'q' ,
'<cmd>bdelete<cr>' ,
{ silent = true , noremap = true , nowait = true }
)
2025-07-16 06:12:45 +03:00
close_preview_autocmd ( opts.close_events , floating_winnr , floating_bufnr , bufnr )
2024-11-01 12:30:36 +00:00
-- save focus_id
if opts.focus_id then
api.nvim_win_set_var ( floating_winnr , opts.focus_id , bufnr )
end
api.nvim_buf_set_var ( bufnr , 'lsp_floating_preview' , floating_winnr )
2025-03-11 20:10:09 +08:00
api.nvim_win_set_var ( floating_winnr , 'lsp_floating_bufnr' , bufnr )
2024-11-01 12:30:36 +00:00
end
2023-09-12 20:51:21 -07:00
2025-03-11 20:10:09 +08:00
api.nvim_create_autocmd ( 'WinClosed' , {
group = api.nvim_create_augroup ( 'nvim.closing_floating_preview' , { clear = true }),
callback = function ( args )
2026-03-12 15:04:05 +00:00
local winid = vim._tointeger ( args.match )
local preview_bufnr = vim.w [ winid ]. lsp_floating_bufnr
2025-03-11 20:10:09 +08:00
if
2026-03-12 15:04:05 +00:00
preview_bufnr
2025-03-11 20:10:09 +08:00
and api.nvim_buf_is_valid ( preview_bufnr )
and winid == vim.b [ preview_bufnr ]. lsp_floating_preview
then
vim.b [ bufnr ]. lsp_floating_preview = nil
return true
end
end ,
})
2024-11-23 19:11:30 +08:00
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.
2025-10-22 19:38:47 +02:00
vim.wo [ floating_winnr ]. linebreak = true -- Break lines a bit nicer
2024-10-06 12:20:40 -07:00
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
2025-01-20 13:00:13 +00:00
if do_stylize then
vim.wo [ floating_winnr ]. conceallevel = 2
2025-11-18 04:49:14 +03:00
vim.wo [ floating_winnr ]. concealcursor = ''
2025-01-20 13:00:13 +00:00
vim.bo [ floating_bufnr ]. filetype = 'markdown'
vim.treesitter . start ( floating_bufnr )
2025-02-24 09:20:55 +01:00
if not opts.height then
-- Reduce window height if TS highlighter conceals code block backticks.
2025-04-21 13:48:26 +02:00
local win_height = api.nvim_win_get_height ( floating_winnr )
local text_height = api.nvim_win_text_height ( floating_winnr , { max_height = win_height }). all
if text_height < win_height then
api.nvim_win_set_height ( floating_winnr , text_height )
2025-03-04 13:36:57 -05:00
end
2025-02-24 09:20:55 +01:00
end
2025-01-20 13:00:13 +00:00
end
2019-11-13 12:55:26 -08:00
return floating_bufnr , floating_winnr
end
2020-11-12 22:21:34 -05:00
do --[[ References ]]
2025-01-13 19:39:03 -08:00
local reference_ns = api.nvim_create_namespace ( 'nvim.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
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2023-10-16 08:13:37 -07:00
---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
2024-11-26 00:06:05 +08:00
function M . buf_highlight_references ( bufnr , references , position_encoding )
2024-10-15 16:56:35 +01:00
validate ( 'bufnr' , bufnr , 'number' , true )
2024-11-26 00:06:05 +08:00
validate ( 'position_encoding' , position_encoding , 'string' , false )
2020-02-26 20:10:16 +01:00
for _ , reference in ipairs ( references ) do
2024-11-09 13:00:37 -08:00
local range = reference.range
local start_line = range.start . line
local end_line = range [ 'end' ]. line
2021-11-07 05:05:52 +07:00
2024-11-26 00:06:05 +08:00
local start_idx = get_line_byte_from_position ( bufnr , range.start , position_encoding )
local end_idx = get_line_byte_from_position ( bufnr , range [ 'end' ], position_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
2024-10-17 11:16:16 +02:00
vim.hl . range (
2021-11-07 05:05:52 +07:00
bufnr ,
reference_ns ,
document_highlight_kind [ kind ],
{ start_line , start_idx },
2022-01-15 14:19:20 -08:00
{ end_line , end_idx },
2024-10-17 11:16:16 +02:00
{ priority = vim.hl . priorities.user }
2022-02-21 21:21:42 +01:00
)
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[]
2024-11-26 00:06:05 +08:00
---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
2024-10-16 15:48:30 +01:00
--- default to first client of buffer
2024-10-15 17:27:27 +01:00
---@return vim.quickfix.entry[] # See |setqflist()| for the format
2024-11-26 00:06:05 +08:00
function M . locations_to_items ( locations , position_encoding )
if position_encoding == nil then
2022-01-13 02:34:04 -08:00
vim.notify_once (
2024-11-26 00:06:05 +08:00
'locations_to_items must be called with valid position encoding' ,
2022-01-13 02:34:04 -08:00
vim.log . levels.WARN
)
2024-11-26 00:06:05 +08:00
position_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-16 16:11:48 +01:00
for uri , rows in vim.spairs ( grouped ) do
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-11-26 00:06:05 +08:00
local col = vim.str_byteindex ( line , position_encoding , pos.character , false )
local end_col = vim.str_byteindex ( end_line , position_encoding , end_pos.character , false )
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
---
2025-07-22 10:30:16 -07:00
---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[]|lsp.WorkspaceSymbol[] list of symbols
2025-02-11 17:09:39 +08:00
---@param bufnr? integer buffer handle or 0 for current, defaults to current
2025-02-11 17:19:44 +08:00
---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
--- default to first client of buffer
2024-10-15 17:27:27 +01:00
---@return vim.quickfix.entry[] # See |setqflist()| for the format
2025-02-11 17:19:44 +08:00
function M . symbols_to_items ( symbols , bufnr , position_encoding )
2025-02-11 17:09:39 +08:00
bufnr = vim._resolve_bufnr ( bufnr )
2025-02-11 17:19:44 +08:00
if position_encoding == nil then
vim.notify_once (
'symbols_to_items must be called with valid position encoding' ,
vim.log . levels.WARN
)
2025-06-05 11:31:51 +01:00
position_encoding = assert ( vim.lsp . get_clients ({ bufnr = bufnr })[ 1 ]). offset_encoding
2025-02-11 17:19:44 +08:00
end
2025-02-11 17:09:39 +08:00
2024-10-16 09:58:10 +01:00
local items = {} --- @type vim.quickfix.entry[]
for _ , symbol in ipairs ( symbols ) do
2025-02-11 17:09:39 +08:00
--- @type string?, lsp.Range?
local filename , range
2024-10-16 09:58:10 +01:00
if symbol.location then
--- @cast symbol lsp.SymbolInformation
filename = vim.uri_to_fname ( symbol.location . uri )
2025-02-11 17:09:39 +08:00
range = symbol.location . range
2024-10-16 09:58:10 +01:00
elseif symbol.selectionRange then
--- @cast symbol lsp.DocumentSymbol
filename = api.nvim_buf_get_name ( bufnr )
2025-02-11 17:09:39 +08:00
range = symbol.selectionRange
2024-10-16 09:58:10 +01:00
end
2025-02-11 17:09:39 +08:00
if filename and range then
2024-10-16 09:58:10 +01:00
local kind = protocol.SymbolKind [ symbol.kind ] or 'Unknown'
2025-02-11 17:09:39 +08:00
local lnum = range [ 'start' ]. line + 1
2025-02-11 17:19:44 +08:00
local col = get_line_byte_from_position ( bufnr , range [ 'start' ], position_encoding ) + 1
2025-02-11 17:09:39 +08:00
local end_lnum = range [ 'end' ]. line + 1
2025-02-11 17:19:44 +08:00
local end_col = get_line_byte_from_position ( bufnr , range [ 'end' ], position_encoding ) + 1
2025-02-11 17:09:39 +08:00
2026-04-29 15:13:47 -05:00
local is_deprecated = not isnil ( symbol.deprecated or nil )
or ( not isnil ( symbol.tags ) and vim.tbl_contains ( symbol.tags , protocol.SymbolTag . Deprecated ))
2025-07-22 10:30:16 -07:00
local text = string.format (
'[%s] %s%s%s' ,
kind ,
symbol.name ,
2026-04-29 15:13:47 -05:00
not isnil ( symbol.containerName ) and ' in ' .. symbol.containerName or '' ,
2025-07-22 10:30:16 -07:00
is_deprecated and ' (deprecated)' or ''
)
2025-07-06 09:39:37 -07:00
2024-10-16 09:58:10 +01:00
items [ # items + 1 ] = {
filename = filename ,
2025-02-11 17:09:39 +08:00
lnum = lnum ,
col = col ,
end_lnum = end_lnum ,
end_col = end_col ,
2024-10-16 09:58:10 +01:00
kind = kind ,
2025-07-06 09:39:37 -07:00
text = text ,
2024-10-16 09:58:10 +01:00
}
end
if symbol.children then
2025-02-11 17:19:44 +08:00
list_extend ( items , M.symbols_to_items ( symbol.children , bufnr , position_encoding ))
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 )
2025-10-05 14:39:23 -07:00
vim.deprecate ( 'vim.lsp.util.try_trim_markdown_code_blocks()' , nil , '0.12' )
2025-06-05 11:31:51 +01:00
local language_id = assert ( lines [ 1 ]): match ( '^```(.*)' )
2019-11-20 15:35:18 -08:00
if language_id then
local has_inner_code_fence = false
for i = 2 , ( # lines - 1 ) do
2025-06-05 11:31:51 +01:00
local line = lines [ i ] --[[@as string]]
2019-11-20 15:35:18 -08:00
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
2026-04-15 13:31:17 -04:00
---@param win integer?: |window-ID| or 0 for current, defaults to current
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2026-04-15 13:31:17 -04:00
local function make_position_param ( win , position_encoding )
win = win or 0
local buf = api.nvim_win_get_buf ( win )
local row , col = unpack ( api.nvim_win_get_cursor ( win ))
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-11-26 00:06:05 +08:00
col = vim.str_utfindex ( line , position_encoding , col , false )
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.
---
2026-04-15 13:31:17 -04:00
---@param win integer?: |window-ID| or 0 for current, defaults to current
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
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
2026-04-15 13:31:17 -04:00
function M . make_position_params ( win , position_encoding )
win = win or 0
local buf = api.nvim_win_get_buf ( win )
2024-11-26 00:06:05 +08:00
if position_encoding == nil then
2024-11-21 04:19:07 +08:00
vim.notify_once (
2024-11-27 21:44:29 +08:00
'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.' ,
2024-11-21 04:19:07 +08:00
vim.log . levels.WARN
)
2025-01-24 13:01:25 +00:00
--- @diagnostic disable-next-line: deprecated
2024-11-26 00:06:05 +08:00
position_encoding = M._get_offset_encoding ( buf )
2024-11-21 04:19:07 +08:00
end
2019-11-21 15:41:32 -08:00
return {
2021-12-10 06:17:50 -08:00
textDocument = M.make_text_document_params ( buf ),
2026-04-15 13:31:17 -04:00
position = make_position_param ( win , position_encoding ),
2021-12-10 06:17:50 -08:00
}
end
--- Utility function for getting the encoding of the first LSP client on the given buffer.
2024-11-21 04:19:07 +08:00
---@deprecated
2024-05-31 08:41:10 -04:00
---@param bufnr integer buffer handle or 0 for current, defaults to current
2025-06-05 11:31:51 +01:00
---@return 'utf-8'|'utf-16'|'utf-32' 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 (
2024-11-21 04:19:07 +08:00
'warning: multiple different client offset_encodings detected for buffer, vim.lsp.util._get_offset_encoding() uses the offset_encoding from the first client' ,
2021-12-10 06:17:50 -08:00
vim.log . levels.WARN
)
end
end
2025-06-05 11:31:51 +01:00
--- @cast offset_encoding -? hack - not safe
2021-12-10 06:17:50 -08:00
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`.
---
2026-04-15 13:31:17 -04:00
---@param win integer?: |window-ID| or 0 for current, defaults to current
2024-11-26 00:06:05 +08:00
---@param position_encoding "utf-8"|"utf-16"|"utf-32"
2024-11-21 04:19:07 +08:00
---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
2026-04-15 13:31:17 -04:00
function M . make_range_params ( win , position_encoding )
local buf = api.nvim_win_get_buf ( win or 0 )
2024-11-26 00:06:05 +08:00
if position_encoding == nil then
2024-11-21 04:19:07 +08:00
vim.notify_once (
2024-11-27 21:44:29 +08:00
'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.' ,
2024-11-21 04:19:07 +08:00
vim.log . levels.WARN
)
2025-01-24 13:01:25 +00:00
--- @diagnostic disable-next-line: deprecated
2024-11-26 00:06:05 +08:00
position_encoding = M._get_offset_encoding ( buf )
2024-11-21 04:19:07 +08:00
end
2026-04-15 13:31:17 -04:00
local position = make_position_param ( win , position_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
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2024-11-21 04:19:07 +08:00
---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
2024-11-26 00:06:05 +08:00
function M . make_given_range_params ( start_pos , end_pos , bufnr , position_encoding )
2024-10-15 16:56:35 +01:00
validate ( 'start_pos' , start_pos , 'table' , true )
validate ( 'end_pos' , end_pos , 'table' , true )
2024-11-26 00:06:05 +08:00
validate ( 'position_encoding' , position_encoding , 'string' , true )
2024-12-07 13:05:05 +00:00
bufnr = vim._resolve_bufnr ( bufnr )
2024-11-26 00:06:05 +08:00
if position_encoding == nil then
2024-11-21 04:19:07 +08:00
vim.notify_once (
2024-11-27 21:44:29 +08:00
'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.' ,
2024-11-21 04:19:07 +08:00
vim.log . levels.WARN
)
2025-01-24 13:01:25 +00:00
--- @diagnostic disable-next-line: deprecated
2024-11-26 00:06:05 +08:00
position_encoding = M._get_offset_encoding ( bufnr )
2024-11-21 04:19:07 +08:00
end
2024-10-16 16:11:48 +01:00
--- @type [integer, integer]
local A = { unpack ( start_pos or api.nvim_buf_get_mark ( bufnr , '<' )) }
--- @type [integer, integer]
local B = { unpack ( 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
2024-11-26 00:06:05 +08:00
-- account for position_encoding.
2020-09-25 04:53:08 +09:00
if A [ 2 ] > 0 then
2024-11-26 00:06:05 +08:00
A [ 2 ] = M.character_offset ( bufnr , A [ 1 ], A [ 2 ], position_encoding )
2020-09-25 04:53:08 +09:00
end
if B [ 2 ] > 0 then
2024-11-26 00:06:05 +08:00
B [ 2 ] = M.character_offset ( bufnr , B [ 1 ], B [ 2 ], position_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[]
2025-06-05 11:31:51 +01:00
---@return lsp.DidChangeWorkspaceFoldersParams
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
2024-10-16 15:48:30 +01:00
---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
--- defaults to `offset_encoding` of first client of `buf`
2023-07-01 18:42:37 +08:00
---@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
)
2025-06-05 11:31:51 +01:00
offset_encoding = assert ( vim.lsp . get_clients ({ bufnr = buf })[ 1 ]). offset_encoding
2022-01-13 02:34:04 -08:00
end
2024-10-26 15:38:25 +01:00
return vim.str_utfindex ( line , offset_encoding , col , false )
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
2024-11-26 00:06:05 +08:00
---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
2023-09-21 14:06:40 +04:00
---@return lsp.Range
2025-07-13 13:00:10 +08:00
function M . _make_line_range_params ( bufnr , start_line , end_line , position_encoding )
2023-09-21 14:06:40 +04:00
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 ,
2024-11-26 00:06:05 +08:00
character = M.character_offset (
bufnr ,
end_line ,
# get_line ( bufnr , end_line ),
position_encoding
),
2023-09-21 14:06:40 +04:00
}
else
end_pos = { line = end_line + 1 , character = 0 }
end
return {
start = { line = start_line , character = 0 },
[ 'end' ] = end_pos ,
}
end
2025-02-12 22:22:59 +08:00
---@class (private) vim.lsp.util._cancel_requests.Filter
---@field bufnr? integer
---@field clients? vim.lsp.Client[]
2025-04-17 10:21:41 +01:00
---@field method? vim.lsp.protocol.Method.ClientToServer.Request
2025-02-12 22:22:59 +08:00
---@field type? string
--- Cancel all {filter}ed requests.
---
---@param filter? vim.lsp.util._cancel_requests.Filter
function M . _cancel_requests ( filter )
filter = filter or {}
local bufnr = filter.bufnr and vim._resolve_bufnr ( filter.bufnr ) or nil
local clients = filter.clients
local method = filter.method
local type = filter.type
for _ , client in
ipairs ( clients or vim.lsp . get_clients ({
bufnr = bufnr ,
method = method ,
}))
do
for id , request in pairs ( client.requests ) do
if
( bufnr == nil or bufnr == request.bufnr )
and ( method == nil or method == request.method )
and ( type == nil or type == request.type )
then
client : cancel_request ( id )
end
end
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