neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

util.lua (80067B)


      1 local protocol = require('vim.lsp.protocol')
      2 local validate = vim.validate
      3 local api = vim.api
      4 local list_extend = vim.list_extend
      5 local uv = vim.uv
      6 
      7 local M = {}
      8 
      9 --- @param border string|(string|[string,string])[]
     10 local function border_error(border)
     11  error(
     12    string.format(
     13      'invalid floating preview border: %s. :help vim.api.nvim_open_win()',
     14      vim.inspect(border)
     15    ),
     16    2
     17  )
     18 end
     19 
     20 local border_size = {
     21  none = { 0, 0 },
     22  single = { 2, 2 },
     23  double = { 2, 2 },
     24  rounded = { 2, 2 },
     25  solid = { 2, 2 },
     26  shadow = { 1, 1 },
     27  bold = { 2, 2 },
     28 }
     29 
     30 --- Check the border given by opts or the default border for the additional
     31 --- size it adds to a float.
     32 --- @param opts? {border:string|(string|[string,string])[]}
     33 --- @return integer height
     34 --- @return integer width
     35 local function get_border_size(opts)
     36  local border = opts and opts.border or vim.o.winborder
     37 
     38  if border == '' then
     39    border = 'none'
     40  end
     41 
     42  -- Convert winborder string option with custom characters into a table
     43  if type(border) == 'string' and border:find(',') then
     44    border = vim.split(border, ',')
     45  end
     46 
     47  if type(border) == 'string' then
     48    if not border_size[border] then
     49      border_error(border)
     50    end
     51    local r = border_size[border]
     52    return r[1], r[2]
     53  end
     54 
     55  if 8 % #border ~= 0 then
     56    border_error(border)
     57  end
     58 
     59  --- @param id integer
     60  --- @return string
     61  local function elem(id)
     62    id = (id - 1) % #border + 1
     63    local e = border[id]
     64    if type(e) == 'table' then
     65      -- border specified as a table of <character, highlight group>
     66      return e[1]
     67    elseif type(e) == 'string' then
     68      -- border specified as a list of border characters
     69      return e
     70    end
     71    --- @diagnostic disable-next-line:missing-return
     72    border_error(border)
     73  end
     74 
     75  --- @param e string
     76  --- @return integer
     77  local function border_height(e)
     78    return #e > 0 and 1 or 0
     79  end
     80 
     81  local top, bottom = elem(2), elem(6)
     82  local height = border_height(top) + border_height(bottom)
     83 
     84  local right, left = elem(4), elem(8)
     85  local width = vim.fn.strdisplaywidth(right) + vim.fn.strdisplaywidth(left)
     86 
     87  return height, width
     88 end
     89 
     90 --- Splits string at newlines, optionally removing unwanted blank lines.
     91 ---
     92 --- @param s string Multiline string
     93 --- @param no_blank boolean? Drop blank lines for each @param/@return (except one empty line
     94 --- separating each). Workaround for https://github.com/LuaLS/lua-language-server/issues/2333
     95 local function split_lines(s, no_blank)
     96  s = string.gsub(s, '\r\n?', '\n')
     97  local lines = {}
     98  local in_desc = true -- Main description block, before seeing any @foo.
     99  for line in vim.gsplit(s, '\n', { plain = true, trimempty = true }) do
    100    local start_annotation = not not line:find('^ ?%@.?[pr]')
    101    in_desc = (not start_annotation) and in_desc or false
    102    if start_annotation and no_blank and not (lines[#lines] or ''):find('^%s*$') then
    103      table.insert(lines, '') -- Separate each @foo with a blank line.
    104    end
    105    if in_desc or not no_blank or not line:find('^%s*$') then
    106      table.insert(lines, line)
    107    end
    108  end
    109  return lines
    110 end
    111 
    112 local function create_window_without_focus()
    113  local prev = api.nvim_get_current_win()
    114  vim.cmd.new()
    115  local new = api.nvim_get_current_win()
    116  api.nvim_set_current_win(prev)
    117  return new
    118 end
    119 
    120 --- Replaces text in a range with new text.
    121 ---
    122 --- CAUTION: Changes in-place!
    123 ---
    124 ---@deprecated
    125 ---@param lines string[] Original list of strings
    126 ---@param A [integer, integer] Start position; a 2-tuple of {line,col} numbers
    127 ---@param B [integer, integer] End position; a 2-tuple {line,col} numbers
    128 ---@param new_lines string[] list of strings to replace the original
    129 ---@return string[] The modified {lines} object
    130 function M.set_lines(lines, A, B, new_lines)
    131  vim.deprecate('vim.lsp.util.set_lines()', nil, '0.12')
    132  -- 0-indexing to 1-indexing
    133  local i_0 = A[1] + 1
    134  -- If it extends past the end, truncate it to the end. This is because the
    135  -- way the LSP describes the range including the last newline is by
    136  -- specifying a line number after what we would call the last line.
    137  local i_n = math.min(B[1] + 1, #lines)
    138  if not (i_0 >= 1 and i_0 <= #lines + 1 and i_n >= 1 and i_n <= #lines) then
    139    error('Invalid range: ' .. vim.inspect({ A = A, B = B, #lines, new_lines }))
    140  end
    141  local prefix = ''
    142  local suffix = assert(lines[i_n]):sub(B[2] + 1)
    143  if A[2] > 0 then
    144    prefix = assert(lines[i_0]):sub(1, A[2])
    145  end
    146  local n = i_n - i_0 + 1
    147  if n ~= #new_lines then
    148    for _ = 1, n - #new_lines do
    149      table.remove(lines, i_0)
    150    end
    151    for _ = 1, #new_lines - n do
    152      table.insert(lines, i_0, '')
    153    end
    154  end
    155  for i = 1, #new_lines do
    156    lines[i - 1 + i_0] = new_lines[i]
    157  end
    158  if #suffix > 0 then
    159    local i = i_0 + #new_lines - 1
    160    lines[i] = lines[i] .. suffix
    161  end
    162  if #prefix > 0 then
    163    lines[i_0] = prefix .. lines[i_0]
    164  end
    165  return lines
    166 end
    167 
    168 --- @param fn fun(x:any):any[]
    169 --- @return function
    170 local function sort_by_key(fn)
    171  return function(a, b)
    172    local ka, kb = fn(a), fn(b)
    173    assert(#ka == #kb)
    174    for i = 1, #ka do
    175      if ka[i] ~= kb[i] then
    176        return ka[i] < kb[i]
    177      end
    178    end
    179    -- every value must have been equal here, which means it's not less than.
    180    return false
    181  end
    182 end
    183 
    184 --- Gets the zero-indexed lines from the given buffer.
    185 --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
    186 --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
    187 ---
    188 ---@param bufnr integer bufnr to get the lines from
    189 ---@param rows integer[] zero-indexed line numbers
    190 ---@return table<integer, string> # a table mapping rows to lines
    191 local function get_lines(bufnr, rows)
    192  --- @type integer[]
    193  rows = type(rows) == 'table' and rows or { rows }
    194 
    195  -- This is needed for bufload and bufloaded
    196  bufnr = vim._resolve_bufnr(bufnr)
    197 
    198  local function buf_lines()
    199    local lines = {} --- @type table<integer,string>
    200    for _, row in ipairs(rows) do
    201      lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1]
    202    end
    203    return lines
    204  end
    205 
    206  -- use loaded buffers if available
    207  if vim.fn.bufloaded(bufnr) == 1 then
    208    return buf_lines()
    209  end
    210 
    211  local uri = vim.uri_from_bufnr(bufnr)
    212 
    213  -- load the buffer if this is not a file uri
    214  -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds.
    215  if uri:sub(1, 4) ~= 'file' then
    216    vim.fn.bufload(bufnr)
    217    return buf_lines()
    218  end
    219 
    220  local filename = api.nvim_buf_get_name(bufnr)
    221  if vim.fn.isdirectory(filename) ~= 0 then
    222    return {}
    223  end
    224 
    225  -- get the data from the file
    226  local fd = uv.fs_open(filename, 'r', 438)
    227  if not fd then
    228    return {}
    229  end
    230  local stat = assert(uv.fs_fstat(fd))
    231  local data = assert(uv.fs_read(fd, stat.size, 0))
    232  uv.fs_close(fd)
    233 
    234  local lines = {} --- @type table<integer,true|string> rows we need to retrieve
    235  local need = 0 -- keep track of how many unique rows we need
    236  for _, row in pairs(rows) do
    237    if not lines[row] then
    238      need = need + 1
    239    end
    240    lines[row] = true
    241  end
    242 
    243  local found = 0
    244  local lnum = 0
    245 
    246  for line in string.gmatch(data, '([^\n]*)\n?') do
    247    if lines[lnum] == true then
    248      lines[lnum] = line
    249      found = found + 1
    250      if found == need then
    251        break
    252      end
    253    end
    254    lnum = lnum + 1
    255  end
    256 
    257  -- change any lines we didn't find to the empty string
    258  for i, line in pairs(lines) do
    259    if line == true then
    260      lines[i] = ''
    261    end
    262  end
    263  return lines --[[@as table<integer,string>]]
    264 end
    265 
    266 --- Gets the zero-indexed line from the given buffer.
    267 --- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
    268 --- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
    269 ---
    270 ---@param bufnr integer
    271 ---@param row integer zero-indexed line number
    272 ---@return string the line at row in filename
    273 local function get_line(bufnr, row)
    274  return get_lines(bufnr, { row })[row]
    275 end
    276 
    277 --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
    278 ---@param position lsp.Position
    279 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
    280 ---@return integer
    281 local function get_line_byte_from_position(bufnr, position, position_encoding)
    282  -- LSP's line and characters are 0-indexed
    283  -- Vim's line and columns are 1-indexed
    284  local col = position.character
    285  -- When on the first character, we can ignore the difference between byte and
    286  -- character
    287  if col > 0 then
    288    local line = get_line(bufnr, position.line) or ''
    289    return vim.str_byteindex(line, position_encoding, col, false)
    290  end
    291  return col
    292 end
    293 
    294 --- Applies a list of text edits to a buffer.
    295 ---@param text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit)[]
    296 ---@param bufnr integer Buffer id
    297 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
    298 ---@param change_annotations? table<string, lsp.ChangeAnnotation>
    299 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
    300 function M.apply_text_edits(text_edits, bufnr, position_encoding, change_annotations)
    301  validate('text_edits', text_edits, 'table', false)
    302  validate('bufnr', bufnr, 'number', false)
    303  validate('position_encoding', position_encoding, 'string', false)
    304  validate('change_annotations', change_annotations, 'table', true)
    305 
    306  if not next(text_edits) then
    307    return
    308  end
    309 
    310  assert(bufnr ~= 0, 'Explicit buffer number is required')
    311 
    312  if not api.nvim_buf_is_loaded(bufnr) then
    313    vim.fn.bufload(bufnr)
    314  end
    315  vim.bo[bufnr].buflisted = true
    316 
    317  local marks = {} --- @type table<string,[integer,integer]>
    318  local has_eol_text_edit = false
    319 
    320  local function apply_text_edits()
    321    -- Fix reversed range and indexing each text_edits
    322    for index, text_edit in ipairs(text_edits) do
    323      --- @cast text_edit lsp.TextEdit|{_index: integer}
    324      text_edit._index = index
    325 
    326      if
    327        text_edit.range.start.line > text_edit.range['end'].line
    328        or text_edit.range.start.line == text_edit.range['end'].line
    329          and text_edit.range.start.character > text_edit.range['end'].character
    330      then
    331        local start = text_edit.range.start
    332        text_edit.range.start = text_edit.range['end']
    333        text_edit.range['end'] = start
    334      end
    335    end
    336 
    337    --- @cast text_edits (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})[]
    338 
    339    -- Sort text_edits
    340    ---@param a (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})
    341    ---@param b (lsp.TextEdit|lsp.AnnotatedTextEdit|{_index: integer})
    342    ---@return boolean
    343    table.sort(text_edits, function(a, b)
    344      if a.range.start.line ~= b.range.start.line then
    345        return a.range.start.line > b.range.start.line
    346      end
    347      if a.range.start.character ~= b.range.start.character then
    348        return a.range.start.character > b.range.start.character
    349      end
    350      return a._index > b._index
    351    end)
    352 
    353    -- save and restore local marks since they get deleted by nvim_buf_set_lines
    354    for _, m in pairs(vim.fn.getmarklist(bufnr)) do
    355      if m.mark:match("^'[a-z]$") then
    356        marks[m.mark:sub(2, 2)] = { m.pos[2], m.pos[3] - 1 } -- api-indexed
    357      end
    358    end
    359 
    360    for _, text_edit in ipairs(text_edits) do
    361      -- Normalize line ending
    362      text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n')
    363 
    364      -- Convert from LSP style ranges to Neovim style ranges.
    365      local start_row = text_edit.range.start.line
    366      local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, position_encoding)
    367      local end_row = text_edit.range['end'].line
    368      local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], position_encoding)
    369      local text = vim.split(text_edit.newText, '\n', { plain = true })
    370 
    371      local max = api.nvim_buf_line_count(bufnr)
    372      -- If the whole edit is after the lines in the buffer we can simply add the new text to the end
    373      -- of the buffer.
    374      if max <= start_row then
    375        api.nvim_buf_set_lines(bufnr, max, max, false, text)
    376      else
    377        local last_line_len = #(get_line(bufnr, math.min(end_row, max - 1)) or '')
    378        -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
    379        -- accept it so we should fix it here.
    380        if max <= end_row then
    381          end_row = max - 1
    382          end_col = last_line_len
    383          has_eol_text_edit = true
    384        else
    385          -- If the replacement is over the end of a line (i.e. end_col is equal to the line length and the
    386          -- replacement text ends with a newline We can likely assume that the replacement is assumed
    387          -- to be meant to replace the newline with another newline and we need to make sure this
    388          -- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
    389          -- in the file some servers (clangd on windows) will include that character in the line
    390          -- while nvim_buf_set_text doesn't count it as part of the line.
    391          if
    392            end_col >= last_line_len
    393            and text_edit.range['end'].character > end_col
    394            and #text_edit.newText > 0
    395            and string.sub(text_edit.newText, -1) == '\n'
    396          then
    397            table.remove(text, #text)
    398          end
    399        end
    400        -- Make sure we don't go out of bounds for end_col
    401        end_col = math.min(last_line_len, end_col)
    402 
    403        api.nvim_buf_set_text(bufnr, start_row, start_col, end_row, end_col, text)
    404      end
    405    end
    406  end
    407 
    408  --- Track how many times each change annotation is applied to build up the final description.
    409  ---@type table<string, integer>
    410  local change_count = {}
    411 
    412  -- If there are any annotated text edits, we need to confirm them before applying the edits.
    413  local confirmations = {} ---@type table<string, integer>
    414  for _, text_edit in ipairs(text_edits) do
    415    if text_edit.annotationId then
    416      assert(
    417        change_annotations ~= nil,
    418        'change_annotations must be provided for annotated text edits'
    419      )
    420 
    421      local annotation = assert(
    422        change_annotations[text_edit.annotationId],
    423        string.format('No change annotation found for ID: %s', text_edit.annotationId)
    424      )
    425 
    426      if annotation.needsConfirmation then
    427        confirmations[text_edit.annotationId] = (confirmations[text_edit.annotationId] or 0) + 1
    428      end
    429 
    430      change_count[text_edit.annotationId] = (change_count[text_edit.annotationId] or 0) + 1
    431    end
    432  end
    433 
    434  if next(confirmations) then
    435    local message = { 'Apply all changes?' }
    436    for id, count in pairs(confirmations) do
    437      local annotation = assert(change_annotations)[id]
    438      message[#message + 1] = annotation.label
    439        .. (annotation.description and (string.format(': %s', annotation.description)) or '')
    440        .. (count > 1 and string.format(' (%d)', count) or '')
    441    end
    442 
    443    local response = vim.fn.confirm(table.concat(message, '\n'), '&Yes\n&No', 1, 'Question')
    444    if response == 1 then
    445      -- Proceed with applying text edits.
    446      apply_text_edits()
    447    else
    448      -- Don't apply any text edits.
    449      return
    450    end
    451  else
    452    -- No confirmations needed, apply text edits directly.
    453    apply_text_edits()
    454  end
    455 
    456  if change_annotations ~= nil and next(change_count) then
    457    local change_message = { 'Applied changes:' }
    458    for id, count in pairs(change_count) do
    459      local annotation = change_annotations[id]
    460      change_message[#change_message + 1] = annotation.label
    461        .. (annotation.description and (': ' .. annotation.description) or '')
    462        .. (count > 1 and string.format(' (%d)', count) or '')
    463    end
    464    vim.notify(table.concat(change_message, '\n'), vim.log.levels.INFO)
    465  end
    466 
    467  local max = api.nvim_buf_line_count(bufnr)
    468 
    469  -- no need to restore marks that still exist
    470  for _, m in pairs(vim.fn.getmarklist(bufnr)) do
    471    marks[m.mark:sub(2, 2)] = nil
    472  end
    473  -- restore marks
    474  for mark, pos in pairs(marks) do
    475    if pos then
    476      -- make sure we don't go out of bounds
    477      pos[1] = math.min(pos[1], max)
    478      pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or ''))
    479      api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {})
    480    end
    481  end
    482 
    483  -- Remove final line if needed
    484  local fix_eol = has_eol_text_edit
    485  fix_eol = fix_eol and (vim.bo[bufnr].eol or (vim.bo[bufnr].fixeol and not vim.bo[bufnr].binary))
    486  fix_eol = fix_eol and get_line(bufnr, max - 1) == ''
    487  if fix_eol then
    488    api.nvim_buf_set_lines(bufnr, -2, -1, false, {})
    489  end
    490 end
    491 
    492 --- Applies a `TextDocumentEdit`, which is a list of changes to a single
    493 --- document.
    494 ---
    495 ---@param text_document_edit lsp.TextDocumentEdit
    496 ---@param index? integer: Optional index of the edit, if from a list of edits (or nil, if not from a list)
    497 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
    498 ---@param change_annotations? table<string, lsp.ChangeAnnotation>
    499 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit
    500 function M.apply_text_document_edit(
    501  text_document_edit,
    502  index,
    503  position_encoding,
    504  change_annotations
    505 )
    506  local text_document = text_document_edit.textDocument
    507  local bufnr = vim.uri_to_bufnr(text_document.uri)
    508  if position_encoding == nil then
    509    vim.notify_once(
    510      'apply_text_document_edit must be called with valid position encoding',
    511      vim.log.levels.WARN
    512    )
    513    return
    514  end
    515 
    516  -- `VersionedTextDocumentIdentifier`s version may be null
    517  --  https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
    518  if
    519    -- For lists of text document edits,
    520    -- do not check the version after the first edit.
    521    not (index and index > 1)
    522    and (
    523      text_document.version ~= vim.NIL
    524      and text_document.version > 0
    525      and M.buf_versions[bufnr] > text_document.version
    526    )
    527  then
    528    print('Buffer ', text_document.uri, ' newer than edits.')
    529    return
    530  end
    531 
    532  M.apply_text_edits(text_document_edit.edits, bufnr, position_encoding, change_annotations)
    533 end
    534 
    535 local function path_components(path)
    536  return vim.split(path, '/', { plain = true })
    537 end
    538 
    539 --- @param path string[]
    540 --- @param prefix string[]
    541 --- @return boolean
    542 local function path_under_prefix(path, prefix)
    543  for i, c in ipairs(prefix) do
    544    if c ~= path[i] then
    545      return false
    546    end
    547  end
    548  return true
    549 end
    550 
    551 --- Get list of loaded writable buffers whose filename matches the given path
    552 --- prefix (normalized full path).
    553 ---@param prefix string
    554 ---@return integer[]
    555 local function get_writable_bufs(prefix)
    556  local prefix_parts = path_components(prefix)
    557  local buffers = {} --- @type integer[]
    558  for _, buf in ipairs(api.nvim_list_bufs()) do
    559    -- No need to care about unloaded or nofile buffers. Also :saveas won't work for them.
    560    if
    561      api.nvim_buf_is_loaded(buf)
    562      and not vim.list_contains({ 'nofile', 'nowrite' }, vim.bo[buf].buftype)
    563    then
    564      local bname = api.nvim_buf_get_name(buf)
    565      local path = path_components(vim.fs.normalize(bname, { expand_env = false }))
    566      if path_under_prefix(path, prefix_parts) then
    567        buffers[#buffers + 1] = buf
    568      end
    569    end
    570  end
    571  return buffers
    572 end
    573 
    574 local function escape_gsub_repl(s)
    575  return (s:gsub('%%', '%%%%'))
    576 end
    577 
    578 --- @class vim.lsp.util.rename.Opts
    579 --- @inlinedoc
    580 --- @field overwrite? boolean
    581 --- @field ignoreIfExists? boolean
    582 
    583 --- Rename old_fname to new_fname
    584 ---
    585 --- Existing buffers are renamed as well, while maintaining their bufnr.
    586 ---
    587 --- It deletes existing buffers that conflict with the renamed file name only when
    588 --- * `opts` requests overwriting; or
    589 --- * the conflicting buffers are not loaded, so that deleting them does not result in data loss.
    590 ---
    591 --- @param old_fname string
    592 --- @param new_fname string
    593 --- @param opts? vim.lsp.util.rename.Opts Options:
    594 function M.rename(old_fname, new_fname, opts)
    595  opts = opts or {}
    596  local skip = not opts.overwrite or opts.ignoreIfExists
    597 
    598  local old_fname_full = uv.fs_realpath(vim.fs.normalize(old_fname, { expand_env = false }))
    599  if not old_fname_full then
    600    vim.notify('Invalid path: ' .. old_fname, vim.log.levels.ERROR)
    601    return
    602  end
    603 
    604  local target_exists = uv.fs_stat(new_fname) ~= nil
    605  if target_exists and skip then
    606    vim.notify(new_fname .. ' already exists. Skipping rename.', vim.log.levels.ERROR)
    607    return
    608  end
    609 
    610  local buf_rename = {} ---@type table<integer, {from: string, to: string}>
    611  local old_fname_pat = '^' .. vim.pesc(old_fname_full)
    612  for _, b in ipairs(get_writable_bufs(old_fname_full)) do
    613    -- Renaming a buffer may conflict with another buffer that happens to have the same name. In
    614    -- most cases, this would have been already detected by the file conflict check above, but the
    615    -- conflicting buffer may not be associated with a file. For example, 'buftype' can be "nofile"
    616    -- or "nowrite", or the buffer can be a normal buffer but has not been written to the file yet.
    617    -- Renaming should fail in such cases to avoid losing the contents of the conflicting buffer.
    618    local old_bname = api.nvim_buf_get_name(b)
    619    local new_bname = old_bname:gsub(old_fname_pat, escape_gsub_repl(new_fname))
    620    if vim.fn.bufexists(new_bname) == 1 then
    621      local existing_buf = vim.fn.bufnr(new_bname)
    622      if api.nvim_buf_is_loaded(existing_buf) and skip then
    623        vim.notify(
    624          new_bname .. ' already exists in the buffer list. Skipping rename.',
    625          vim.log.levels.ERROR
    626        )
    627        return
    628      end
    629      -- no need to preserve if such a buffer is empty
    630      api.nvim_buf_delete(existing_buf, {})
    631    end
    632 
    633    buf_rename[b] = { from = old_bname, to = new_bname }
    634  end
    635 
    636  local newdir = vim.fs.dirname(new_fname)
    637  vim.fn.mkdir(newdir, 'p')
    638 
    639  local ok, err = os.rename(old_fname_full, new_fname)
    640  assert(ok, err)
    641 
    642  local old_undofile = vim.fn.undofile(old_fname_full)
    643  if uv.fs_stat(old_undofile) ~= nil then
    644    local new_undofile = vim.fn.undofile(new_fname)
    645    vim.fn.mkdir(vim.fs.dirname(new_undofile), 'p')
    646    os.rename(old_undofile, new_undofile)
    647  end
    648 
    649  for b, rename in pairs(buf_rename) do
    650    -- Rename with :saveas. This does two things:
    651    -- * Unset BF_WRITE_MASK, so that users don't get E13 when they do :write.
    652    -- * Send didClose and didOpen via textDocument/didSave handler.
    653    vim._with({ buf = b }, function()
    654      vim.cmd('keepalt saveas! ' .. vim.fn.fnameescape(rename.to))
    655    end)
    656    -- Delete the new buffer with the old name created by :saveas. nvim_buf_delete and
    657    -- :bwipeout are futile because the buffer will be added again somewhere else.
    658    vim.cmd('bdelete! ' .. vim.fn.bufnr(rename.from))
    659  end
    660 end
    661 
    662 --- @param change lsp.CreateFile
    663 local function create_file(change)
    664  local opts = change.options or {}
    665  -- from spec: Overwrite wins over `ignoreIfExists`
    666  local fname = vim.uri_to_fname(change.uri)
    667  if not opts.ignoreIfExists or opts.overwrite then
    668    vim.fn.mkdir(vim.fs.dirname(fname), 'p')
    669    local file = io.open(fname, 'w')
    670    if file then
    671      file:close()
    672    end
    673  end
    674  vim.fn.bufadd(fname)
    675 end
    676 
    677 --- @param change lsp.DeleteFile
    678 local function delete_file(change)
    679  local opts = change.options or {}
    680  local fname = vim.uri_to_fname(change.uri)
    681  local bufnr = vim.fn.bufadd(fname)
    682  vim.fs.rm(fname, {
    683    force = opts.ignoreIfNotExists,
    684    recursive = opts.recursive,
    685  })
    686  api.nvim_buf_delete(bufnr, { force = true })
    687 end
    688 
    689 --- Applies a `WorkspaceEdit`.
    690 ---
    691 ---@param workspace_edit lsp.WorkspaceEdit
    692 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32' (required)
    693 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit
    694 function M.apply_workspace_edit(workspace_edit, position_encoding)
    695  if position_encoding == nil then
    696    vim.notify_once(
    697      'apply_workspace_edit must be called with valid position encoding',
    698      vim.log.levels.WARN
    699    )
    700    return
    701  end
    702  if workspace_edit.documentChanges then
    703    for idx, change in ipairs(workspace_edit.documentChanges) do
    704      if change.kind == 'rename' then
    705        local options = change.options --[[@as vim.lsp.util.rename.Opts]]
    706        M.rename(vim.uri_to_fname(change.oldUri), vim.uri_to_fname(change.newUri), options)
    707      elseif change.kind == 'create' then
    708        create_file(change)
    709      elseif change.kind == 'delete' then
    710        delete_file(change)
    711      elseif change.kind then --- @diagnostic disable-line:undefined-field
    712        error(string.format('Unsupported change: %q', vim.inspect(change)))
    713      else
    714        M.apply_text_document_edit(change, idx, position_encoding, workspace_edit.changeAnnotations)
    715      end
    716    end
    717    return
    718  end
    719 
    720  local all_changes = workspace_edit.changes
    721  if not (all_changes and not vim.tbl_isempty(all_changes)) then
    722    return
    723  end
    724 
    725  for uri, changes in pairs(all_changes) do
    726    local bufnr = vim.uri_to_bufnr(uri)
    727    M.apply_text_edits(changes, bufnr, position_encoding, workspace_edit.changeAnnotations)
    728  end
    729 end
    730 
    731 --- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into
    732 --- a list of lines containing valid markdown. Useful to populate the hover
    733 --- window for `textDocument/hover`, for parsing the result of
    734 --- `textDocument/signatureHelp`, and potentially others.
    735 ---
    736 --- Note that if the input is of type `MarkupContent` and its kind is `plaintext`,
    737 --- then the corresponding value is returned without further modifications.
    738 ---
    739 ---@param input lsp.MarkedString|lsp.MarkedString[]|lsp.MarkupContent
    740 ---@param contents string[]? List of strings to extend with converted lines. Defaults to {}.
    741 ---@return string[] extended with lines of converted markdown.
    742 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover
    743 function M.convert_input_to_markdown_lines(input, contents)
    744  contents = contents or {}
    745  -- MarkedString variation 1
    746  if type(input) == 'string' then
    747    list_extend(contents, split_lines(input, true))
    748  else
    749    assert(type(input) == 'table', 'Expected a table for LSP input')
    750    -- MarkupContent
    751    if input.kind then
    752      local value = input.value or ''
    753      list_extend(contents, split_lines(value, true))
    754      -- MarkupString variation 2
    755    elseif input.language then
    756      table.insert(contents, '```' .. input.language)
    757      list_extend(contents, split_lines(input.value or ''))
    758      table.insert(contents, '```')
    759      -- By deduction, this must be MarkedString[]
    760    else
    761      -- Use our existing logic to handle MarkedString
    762      for _, marked_string in ipairs(input) do
    763        M.convert_input_to_markdown_lines(marked_string, contents)
    764      end
    765    end
    766  end
    767  if (contents[1] == '' or contents[1] == nil) and #contents == 1 then
    768    return {}
    769  end
    770  return contents
    771 end
    772 
    773 --- Returns the line/column-based position in `contents` at the given offset.
    774 ---
    775 ---@param offset integer
    776 ---@param contents string[]
    777 ---@return { [1]: integer, [2]: integer }?
    778 local function get_pos_from_offset(offset, contents)
    779  local i = 0
    780  for l, line in ipairs(contents) do
    781    if offset >= i and offset < i + #line then
    782      return { l - 1, offset - i + 1 }
    783    else
    784      i = i + #line + 1
    785    end
    786  end
    787 end
    788 
    789 --- Converts `textDocument/signatureHelp` response to markdown lines.
    790 ---
    791 ---@param signature_help lsp.SignatureHelp Response of `textDocument/SignatureHelp`
    792 ---@param ft string? filetype that will be use as the `lang` for the label markdown code block
    793 ---@param triggers string[]? list of trigger characters from the lsp server. used to better determine parameter offsets
    794 ---@return string[]? # lines of converted markdown.
    795 ---@return Range4? # highlight range for the active parameter
    796 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp
    797 function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers)
    798  --The active signature. If omitted or the value lies outside the range of
    799  --`signatures` the value defaults to zero or is ignored if `signatures.length == 0`.
    800  --Whenever possible implementors should make an active decision about
    801  --the active signature and shouldn't rely on a default value.
    802  local contents = {} --- @type string[]
    803  local active_offset ---@type [integer, integer]?
    804  local active_signature = signature_help.activeSignature or 0
    805  -- If the activeSignature is not inside the valid range, then clip it.
    806  -- In 3.15 of the protocol, activeSignature was allowed to be negative
    807  if active_signature >= #signature_help.signatures or active_signature < 0 then
    808    active_signature = 0
    809  end
    810  local signature = vim.deepcopy(signature_help.signatures[active_signature + 1])
    811  local label = signature.label
    812  if ft then
    813    -- wrap inside a code block for proper rendering
    814    label = ('```%s\n%s\n```'):format(ft, label)
    815  end
    816  list_extend(contents, vim.split(label, '\n', { plain = true, trimempty = true }))
    817  local doc = signature.documentation
    818  if doc then
    819    -- if LSP returns plain string, we treat it as plaintext. This avoids
    820    -- special characters like underscore or similar from being interpreted
    821    -- as markdown font modifiers
    822    if type(doc) == 'string' then
    823      signature.documentation = { kind = 'plaintext', value = doc }
    824    end
    825    -- Add delimiter if there is documentation to display
    826    if signature.documentation.value ~= '' then
    827      contents[#contents + 1] = '---'
    828    end
    829    M.convert_input_to_markdown_lines(signature.documentation, contents)
    830  end
    831  if signature.parameters and #signature.parameters > 0 then
    832    local active_parameter = signature.activeParameter or signature_help.activeParameter
    833 
    834    -- NOTE: We intentionally violate the LSP spec, which states that if `activeParameter`
    835    -- is not provided or is out-of-bounds, it should default to 0.
    836    -- Instead, we default to `nil`, as most clients do. In practice, 'no active parameter'
    837    -- is better default than 'first parameter' and aligns better with user expectations.
    838    -- Related discussion: https://github.com/microsoft/language-server-protocol/issues/1271
    839    if
    840      not active_parameter
    841      or active_parameter == vim.NIL
    842      or active_parameter < 0
    843      or active_parameter >= #signature.parameters
    844    then
    845      return contents, nil
    846    end
    847 
    848    local parameter = signature.parameters[active_parameter + 1]
    849    local parameter_label = parameter.label
    850    if type(parameter_label) == 'table' then
    851      active_offset = parameter_label
    852    else
    853      local offset = 1 ---@type integer?
    854      -- try to set the initial offset to the first found trigger character
    855      for _, t in ipairs(triggers or {}) do
    856        local trigger_offset = signature.label:find(t, 1, true)
    857        if trigger_offset and (offset == 1 or trigger_offset < offset) then
    858          offset = trigger_offset
    859        end
    860      end
    861      for p, param in pairs(signature.parameters) do
    862        local plabel = param.label
    863        assert(type(plabel) == 'string', 'Expected label to be a string')
    864        offset = signature.label:find(plabel, offset, true)
    865        if not offset then
    866          break
    867        end
    868        if p == active_parameter + 1 then
    869          active_offset = { offset - 1, offset + #parameter_label - 1 }
    870          break
    871        end
    872        offset = offset + #plabel + 1
    873      end
    874    end
    875    if parameter.documentation then
    876      M.convert_input_to_markdown_lines(parameter.documentation, contents)
    877    end
    878  end
    879 
    880  local active_hl = nil
    881  if active_offset then
    882    -- Account for the start of the markdown block.
    883    if ft then
    884      active_offset[1] = active_offset[1] + #contents[1]
    885      active_offset[2] = active_offset[2] + #contents[1]
    886    end
    887 
    888    local a_start = get_pos_from_offset(active_offset[1], contents)
    889    local a_end = get_pos_from_offset(active_offset[2], contents)
    890    if a_start and a_end then
    891      active_hl = { a_start[1], a_start[2], a_end[1], a_end[2] }
    892    end
    893  end
    894 
    895  return contents, active_hl
    896 end
    897 
    898 --- Creates a table with sensible default options for a floating window. The
    899 --- table can be passed to |nvim_open_win()|.
    900 ---
    901 ---@param width integer window width (in character cells)
    902 ---@param height integer window height (in character cells)
    903 ---@param opts? vim.lsp.util.open_floating_preview.Opts
    904 ---@return vim.api.keyset.win_config
    905 function M.make_floating_popup_options(width, height, opts)
    906  validate('opts', opts, 'table', true)
    907  opts = opts or {}
    908  validate('opts.offset_x', opts.offset_x, 'number', true)
    909  validate('opts.offset_y', opts.offset_y, 'number', true)
    910 
    911  local anchor = ''
    912 
    913  local lines_above = opts.relative == 'mouse' and vim.fn.getmousepos().line - 1
    914    or vim.fn.winline() - 1
    915  local lines_below = vim.fn.winheight(0) - lines_above
    916 
    917  local anchor_bias = opts.anchor_bias or 'auto'
    918 
    919  local anchor_below --- @type boolean?
    920 
    921  if anchor_bias == 'below' then
    922    anchor_below = (lines_below > lines_above) or (height <= lines_below)
    923  elseif anchor_bias == 'above' then
    924    local anchor_above = (lines_above > lines_below) or (height <= lines_above)
    925    anchor_below = not anchor_above
    926  else
    927    anchor_below = lines_below > lines_above
    928  end
    929 
    930  local border_height = get_border_size(opts)
    931  local row, col --- @type integer?, integer?
    932  if anchor_below then
    933    anchor = anchor .. 'N'
    934    height = math.max(math.min(lines_below - border_height, height), 0)
    935    row = 1
    936  else
    937    anchor = anchor .. 'S'
    938    height = math.max(math.min(lines_above - border_height, height), 0)
    939    row = 0
    940  end
    941 
    942  local wincol = opts.relative == 'mouse' and vim.fn.getmousepos().column or vim.fn.wincol()
    943 
    944  if wincol + width + (opts.offset_x or 0) <= vim.o.columns then
    945    anchor = anchor .. 'W'
    946    col = 0
    947  else
    948    anchor = anchor .. 'E'
    949    col = 1
    950  end
    951 
    952  local title = ((opts.border or vim.o.winborder ~= '') and opts.title) and opts.title or nil
    953  local title_pos --- @type 'left'|'center'|'right'?
    954 
    955  if title then
    956    title_pos = opts.title_pos or 'center'
    957  end
    958 
    959  return {
    960    anchor = anchor,
    961    row = row + (opts.offset_y or 0),
    962    col = col + (opts.offset_x or 0),
    963    height = height,
    964    focusable = opts.focusable,
    965    relative = (opts.relative == 'mouse' or opts.relative == 'editor') and opts.relative
    966      or 'cursor',
    967    style = 'minimal',
    968    width = width,
    969    border = opts.border,
    970    zindex = opts.zindex or (api.nvim_win_get_config(0).zindex or 49) + 1,
    971    title = title,
    972    title_pos = title_pos,
    973  }
    974 end
    975 
    976 --- @class vim.lsp.util.show_document.Opts
    977 --- @inlinedoc
    978 ---
    979 --- Jump to existing window if buffer is already open.
    980 --- @field reuse_win? boolean
    981 ---
    982 --- Whether to focus/jump to location if possible.
    983 --- (defaults: true)
    984 --- @field focus? boolean
    985 
    986 --- Shows document and optionally jumps to the location.
    987 ---
    988 ---@param location lsp.Location|lsp.LocationLink
    989 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
    990 ---@param opts? vim.lsp.util.show_document.Opts
    991 ---@return boolean `true` if succeeded
    992 function M.show_document(location, position_encoding, opts)
    993  -- location may be Location or LocationLink
    994  local uri = location.uri or location.targetUri
    995  if uri == nil then
    996    return false
    997  end
    998  if position_encoding == nil then
    999    vim.notify_once(
   1000      'show_document must be called with valid position encoding',
   1001      vim.log.levels.WARN
   1002    )
   1003    return false
   1004  end
   1005  local bufnr = vim.uri_to_bufnr(uri)
   1006 
   1007  opts = opts or {}
   1008  local focus = vim.F.if_nil(opts.focus, true)
   1009  if focus then
   1010    -- Save position in jumplist
   1011    vim.cmd("normal! m'")
   1012 
   1013    -- Push a new item into tagstack
   1014    local from = { vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0 }
   1015    local items = { { tagname = vim.fn.expand('<cword>'), from = from } }
   1016    vim.fn.settagstack(vim.fn.win_getid(), { items = items }, 't')
   1017  end
   1018 
   1019  local win = opts.reuse_win and vim.fn.win_findbuf(bufnr)[1]
   1020    or focus and api.nvim_get_current_win()
   1021    or create_window_without_focus()
   1022 
   1023  vim.bo[bufnr].buflisted = true
   1024  api.nvim_win_set_buf(win, bufnr)
   1025  if focus then
   1026    api.nvim_set_current_win(win)
   1027  end
   1028 
   1029  -- location may be Location or LocationLink
   1030  local range = location.range or location.targetSelectionRange
   1031  if range then
   1032    -- Jump to new location (adjusting for encoding of characters)
   1033    local row = range.start.line
   1034    local col = get_line_byte_from_position(bufnr, range.start, position_encoding)
   1035    api.nvim_win_set_cursor(win, { row + 1, col })
   1036    vim._with({ win = win }, function()
   1037      -- Open folds under the cursor
   1038      vim.cmd('normal! zv')
   1039    end)
   1040  end
   1041 
   1042  return true
   1043 end
   1044 
   1045 --- Jumps to a location.
   1046 ---
   1047 ---@deprecated use `vim.lsp.util.show_document` with `{focus=true}` instead
   1048 ---@param location lsp.Location|lsp.LocationLink
   1049 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'?
   1050 ---@param reuse_win boolean? Jump to existing window if buffer is already open.
   1051 ---@return boolean `true` if the jump succeeded
   1052 function M.jump_to_location(location, position_encoding, reuse_win)
   1053  vim.deprecate('vim.lsp.util.jump_to_location', nil, '0.12')
   1054  return M.show_document(location, position_encoding, { reuse_win = reuse_win, focus = true })
   1055 end
   1056 
   1057 --- Previews a location in a floating window
   1058 ---
   1059 --- behavior depends on type of location:
   1060 ---   - for Location, range is shown (e.g., function definition)
   1061 ---   - for LocationLink, targetRange is shown (e.g., body of function definition)
   1062 ---
   1063 ---@param location lsp.Location|lsp.LocationLink
   1064 ---@param opts? vim.lsp.util.open_floating_preview.Opts
   1065 ---@return integer? buffer id of float window
   1066 ---@return integer? window id of float window
   1067 function M.preview_location(location, opts)
   1068  -- location may be LocationLink or Location (more useful for the former)
   1069  local uri = location.targetUri or location.uri
   1070  if uri == nil then
   1071    return
   1072  end
   1073  local bufnr = vim.uri_to_bufnr(uri)
   1074  if not api.nvim_buf_is_loaded(bufnr) then
   1075    vim.fn.bufload(bufnr)
   1076  end
   1077  local range = location.targetRange or location.range
   1078  local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range['end'].line + 1, false)
   1079  local syntax = vim.bo[bufnr].syntax
   1080  if syntax == '' then
   1081    -- When no syntax is set, we use filetype as fallback. This might not result
   1082    -- in a valid syntax definition.
   1083    -- An empty syntax is more common now with TreeSitter, since TS disables syntax.
   1084    syntax = vim.bo[bufnr].filetype
   1085  end
   1086  opts = opts or {}
   1087  opts.focus_id = 'location'
   1088  return M.open_floating_preview(contents, syntax, opts)
   1089 end
   1090 
   1091 local function find_window_by_var(name, value)
   1092  for _, win in ipairs(api.nvim_list_wins()) do
   1093    if vim.w[win][name] == value then
   1094      return win
   1095    end
   1096  end
   1097 end
   1098 
   1099 ---Returns true if the line is empty or only contains whitespace.
   1100 ---@param line string
   1101 ---@return boolean
   1102 local function is_blank_line(line)
   1103  return line and line:match('^%s*$')
   1104 end
   1105 
   1106 ---Returns true if the line corresponds to a Markdown thematic break.
   1107 ---@see https://github.github.com/gfm/#thematic-break
   1108 ---@param line string
   1109 ---@return boolean
   1110 local function is_separator_line(line)
   1111  local i = 1
   1112  -- 1. Skip up to 3 leading spaces
   1113  local leading_spaces = 3
   1114  while i <= #line and line:byte(i) == string.byte(' ') and leading_spaces > 0 do
   1115    i = i + 1
   1116    leading_spaces = leading_spaces - 1
   1117  end
   1118  -- 2. Determine the delimiter character
   1119  local delimiter = line:byte(i) -- nil if i > #line
   1120  if
   1121    delimiter ~= string.byte('-')
   1122    and delimiter ~= string.byte('_')
   1123    and delimiter ~= string.byte('*')
   1124  then
   1125    return false
   1126  end
   1127  local ndelimiters = 1
   1128  i = i + 1
   1129  -- 3. Iterate until found non-whitespace or other than expected delimiter
   1130  while i <= #line do
   1131    local char = line:byte(i)
   1132    if char == delimiter then
   1133      ndelimiters = ndelimiters + 1
   1134    elseif not (char == string.byte(' ') or char == string.byte('\t')) then
   1135      return false
   1136    end
   1137    i = i + 1
   1138  end
   1139  return ndelimiters >= 3
   1140 end
   1141 
   1142 ---Replaces separator lines by the given divider and removing surrounding blank lines.
   1143 ---@param contents string[]
   1144 ---@param divider string
   1145 ---@return string[]
   1146 local function replace_separators(contents, divider)
   1147  local trimmed = {}
   1148  local l = 1
   1149  while l <= #contents do
   1150    local line = contents[l]
   1151    if is_separator_line(line) then
   1152      if l > 1 and is_blank_line(contents[l - 1]) then
   1153        table.remove(trimmed)
   1154      end
   1155      table.insert(trimmed, divider)
   1156      if is_blank_line(contents[l + 1]) then
   1157        l = l + 1
   1158      end
   1159    else
   1160      table.insert(trimmed, line)
   1161    end
   1162    l = l + 1
   1163  end
   1164 
   1165  return trimmed
   1166 end
   1167 
   1168 ---Collapses successive blank lines in the input table into a single one.
   1169 ---@param contents string[]
   1170 ---@return string[]
   1171 local function collapse_blank_lines(contents)
   1172  local collapsed = {}
   1173  local l = 1
   1174  while l <= #contents do
   1175    local line = contents[l]
   1176    if is_blank_line(line) then
   1177      while is_blank_line(contents[l + 1]) do
   1178        l = l + 1
   1179      end
   1180    end
   1181    table.insert(collapsed, line)
   1182    l = l + 1
   1183  end
   1184  return collapsed
   1185 end
   1186 
   1187 local function get_markdown_fences()
   1188  local fences = {} --- @type table<string,string>
   1189  for _, fence in
   1190    pairs(vim.g.markdown_fenced_languages or {} --[[@as string[] ]])
   1191  do
   1192    local lang, syntax = fence:match('^(.*)=(.*)$')
   1193    if lang then
   1194      fences[lang] = syntax
   1195    end
   1196  end
   1197  return fences
   1198 end
   1199 
   1200 --- @deprecated
   1201 --- Converts markdown into syntax highlighted regions by stripping the code
   1202 --- blocks and converting them into highlighted code.
   1203 --- This will by default insert a blank line separator after those code block
   1204 --- regions to improve readability.
   1205 ---
   1206 --- This method configures the given buffer and returns the lines to set.
   1207 ---
   1208 --- If you want to open a popup with fancy markdown, use `open_floating_preview` instead
   1209 ---
   1210 ---@param bufnr integer
   1211 ---@param contents string[] of lines to show in window
   1212 ---@param opts? table with optional fields
   1213 ---  - height    of floating window
   1214 ---  - width     of floating window
   1215 ---  - wrap_at   character to wrap at for computing height
   1216 ---  - max_width  maximal width of floating window
   1217 ---  - max_height maximal height of floating window
   1218 ---  - separator insert separator after code block
   1219 ---@return table stripped content
   1220 function M.stylize_markdown(bufnr, contents, opts)
   1221  vim.deprecate('vim.lsp.util.stylize_markdown', nil, '0.14')
   1222  validate('contents', contents, 'table')
   1223  validate('opts', opts, 'table', true)
   1224  opts = opts or {}
   1225 
   1226  -- table of fence types to {ft, begin, end}
   1227  -- when ft is nil, we get the ft from the regex match
   1228  local matchers = {
   1229    block = { nil, '```+%s*([a-zA-Z0-9_]*)', '```+' },
   1230    pre = { nil, '<pre>([a-z0-9]*)', '</pre>' },
   1231    code = { '', '<code>', '</code>' },
   1232    text = { 'text', '<text>', '</text>' },
   1233  }
   1234 
   1235  --- @param line string
   1236  --- @return {type:string,ft:string}?
   1237  local function match_begin(line)
   1238    for type, pattern in pairs(matchers) do
   1239      --- @type string?
   1240      local ret = line:match(string.format('^%%s*%s%%s*$', pattern[2]))
   1241      if ret then
   1242        return {
   1243          type = type,
   1244          ft = pattern[1] or ret,
   1245        }
   1246      end
   1247    end
   1248  end
   1249 
   1250  --- @param line string
   1251  --- @param match {type:string,ft:string}
   1252  --- @return string
   1253  local function match_end(line, match)
   1254    local pattern = matchers[match.type]
   1255    return line:match(string.format('^%%s*%s%%s*$', pattern[3]))
   1256  end
   1257 
   1258  -- Clean up
   1259  contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
   1260 
   1261  local stripped = {} --- @type string[]
   1262  local highlights = {} --- @type {ft:string,start:integer,finish:integer}[]
   1263 
   1264  local i = 1
   1265  while i <= #contents do
   1266    local line = contents[i]
   1267    local match = match_begin(line)
   1268    if match then
   1269      local start = #stripped
   1270      i = i + 1
   1271      while i <= #contents do
   1272        line = contents[i]
   1273        if match_end(line, match) then
   1274          i = i + 1
   1275          break
   1276        end
   1277        stripped[#stripped + 1] = line
   1278        i = i + 1
   1279      end
   1280      table.insert(highlights, {
   1281        ft = match.ft,
   1282        start = start + 1,
   1283        finish = #stripped,
   1284      })
   1285      -- add a separator, but not on the last line
   1286      if opts.separator and i < #contents then
   1287        stripped[#stripped + 1] = '---'
   1288      end
   1289    else
   1290      -- strip any empty lines or separators prior to this separator in actual markdown
   1291      if line:match('^---+$') then
   1292        while
   1293          stripped[#stripped]
   1294          and (stripped[#stripped]:match('^%s*$') or stripped[#stripped]:match('^---+$'))
   1295        do
   1296          stripped[#stripped] = nil
   1297        end
   1298      end
   1299      -- add the line if its not an empty line following a separator
   1300      if
   1301        not (line:match('^%s*$') and stripped[#stripped] and stripped[#stripped]:match('^---+$'))
   1302      then
   1303        stripped[#stripped + 1] = line
   1304      end
   1305      i = i + 1
   1306    end
   1307  end
   1308 
   1309  -- Handle some common html escape sequences
   1310  --- @type string[]
   1311  stripped = vim.tbl_map(
   1312    --- @param line string
   1313    function(line)
   1314      local escapes = {
   1315        ['&gt;'] = '>',
   1316        ['&lt;'] = '<',
   1317        ['&quot;'] = '"',
   1318        ['&apos;'] = "'",
   1319        ['&ensp;'] = ' ',
   1320        ['&emsp;'] = ' ',
   1321        ['&amp;'] = '&',
   1322      }
   1323      return (line:gsub('&[^ ;]+;', escapes))
   1324    end,
   1325    stripped
   1326  )
   1327 
   1328  -- Compute size of float needed to show (wrapped) lines
   1329  opts.wrap_at = opts.wrap_at or (vim.wo['wrap'] and api.nvim_win_get_width(0))
   1330  local width = M._make_floating_popup_size(stripped, opts)
   1331 
   1332  local sep_line = string.rep('─', math.min(width, opts.wrap_at or width))
   1333 
   1334  for l in ipairs(stripped) do
   1335    if stripped[l]:match('^---+$') then
   1336      stripped[l] = sep_line
   1337    end
   1338  end
   1339 
   1340  api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
   1341 
   1342  local idx = 1
   1343  -- keep track of syntaxes we already included.
   1344  -- no need to include the same syntax more than once
   1345  local langs = {} --- @type table<string,boolean>
   1346  local fences = get_markdown_fences()
   1347  local function apply_syntax_to_region(ft, start, finish)
   1348    if ft == '' then
   1349      vim.cmd(
   1350        string.format(
   1351          'syntax region markdownCode start=+\\%%%dl+ end=+\\%%%dl+ keepend extend',
   1352          start,
   1353          finish + 1
   1354        )
   1355      )
   1356      return
   1357    end
   1358    ft = fences[ft] or ft
   1359    local name = ft .. idx
   1360    idx = idx + 1
   1361    local lang = '@' .. ft:upper()
   1362    if not langs[lang] then
   1363      -- HACK: reset current_syntax, since some syntax files like markdown won't load if it is already set
   1364      pcall(api.nvim_buf_del_var, bufnr, 'current_syntax')
   1365      if #api.nvim_get_runtime_file(('syntax/%s.vim'):format(ft), true) == 0 then
   1366        return
   1367      end
   1368      --- @diagnostic disable-next-line:param-type-mismatch
   1369      pcall(vim.cmd, string.format('syntax include %s syntax/%s.vim', lang, ft))
   1370      langs[lang] = true
   1371    end
   1372    vim.cmd(
   1373      string.format(
   1374        'syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s keepend',
   1375        name,
   1376        start,
   1377        finish + 1,
   1378        lang
   1379      )
   1380    )
   1381  end
   1382 
   1383  -- needs to run in the buffer for the regions to work
   1384  vim._with({ buf = bufnr }, function()
   1385    -- we need to apply lsp_markdown regions speperately, since otherwise
   1386    -- markdown regions can "bleed" through the other syntax regions
   1387    -- and mess up the formatting
   1388    local last = 1
   1389    for _, h in ipairs(highlights) do
   1390      if last < h.start then
   1391        apply_syntax_to_region('lsp_markdown', last, h.start - 1)
   1392      end
   1393      apply_syntax_to_region(h.ft, h.start, h.finish)
   1394      last = h.finish + 1
   1395    end
   1396    if last <= #stripped then
   1397      apply_syntax_to_region('lsp_markdown', last, #stripped)
   1398    end
   1399  end)
   1400 
   1401  return stripped
   1402 end
   1403 
   1404 --- @class (private) vim.lsp.util._normalize_markdown.Opts
   1405 --- @field width integer Thematic breaks are expanded to this size. Defaults to 80.
   1406 
   1407 --- Normalizes Markdown input to a canonical form.
   1408 ---
   1409 --- The returned Markdown adheres to the GitHub Flavored Markdown (GFM)
   1410 --- specification, as required by the LSP.
   1411 ---
   1412 --- The following transformations are made:
   1413 ---
   1414 ---   1. Carriage returns ('\r') and empty lines at the beginning and end are removed
   1415 ---   2. Successive empty lines are collapsed into a single empty line
   1416 ---   3. Thematic breaks are expanded to the given width
   1417 ---
   1418 ---@param contents string[]
   1419 ---@param opts? vim.lsp.util._normalize_markdown.Opts
   1420 ---@return string[] table of lines containing normalized Markdown
   1421 ---@see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#markupContent
   1422 ---@see https://github.github.com/gfm
   1423 function M._normalize_markdown(contents, opts)
   1424  validate('contents', contents, 'table')
   1425  validate('opts', opts, 'table', true)
   1426  opts = opts or {}
   1427 
   1428  -- 1. Carriage returns are removed
   1429  contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n', { trimempty = true })
   1430 
   1431  -- 2. Successive empty lines are collapsed into a single empty line
   1432  contents = collapse_blank_lines(contents)
   1433 
   1434  -- 3. Thematic breaks are expanded to the given width
   1435  local divider = string.rep('─', opts.width or 80)
   1436  contents = replace_separators(contents, divider)
   1437 
   1438  return contents
   1439 end
   1440 
   1441 --- Closes the preview window
   1442 ---
   1443 ---@param winnr integer window id of preview window
   1444 ---@param bufnrs table? optional list of ignored buffers
   1445 local function close_preview_window(winnr, bufnrs)
   1446  vim.schedule(function()
   1447    -- exit if we are in one of ignored buffers
   1448    if bufnrs and vim.list_contains(bufnrs, api.nvim_get_current_buf()) then
   1449      return
   1450    end
   1451 
   1452    local augroup = 'nvim.preview_window_' .. winnr
   1453    pcall(api.nvim_del_augroup_by_name, augroup)
   1454    pcall(api.nvim_win_close, winnr, true)
   1455  end)
   1456 end
   1457 
   1458 --- Creates autocommands to close a preview window when events happen.
   1459 ---
   1460 ---@param events table list of events
   1461 ---@param winnr integer window id of preview window
   1462 ---@param floating_bufnr integer floating preview buffer
   1463 ---@param bufnr integer buffer that opened the floating preview buffer
   1464 ---@see autocmd-events
   1465 local function close_preview_autocmd(events, winnr, floating_bufnr, bufnr)
   1466  local augroup = api.nvim_create_augroup('nvim.preview_window_' .. winnr, {
   1467    clear = true,
   1468  })
   1469 
   1470  -- close the preview window when entered a buffer that is not
   1471  -- the floating window buffer or the buffer that spawned it
   1472  api.nvim_create_autocmd('BufLeave', {
   1473    group = augroup,
   1474    buffer = bufnr,
   1475    callback = function()
   1476      vim.schedule(function()
   1477        -- When jumping to the quickfix window from the preview window,
   1478        -- do not close the preview window.
   1479        if api.nvim_get_option_value('filetype', { buf = 0 }) ~= 'qf' then
   1480          close_preview_window(winnr, { floating_bufnr, bufnr })
   1481        end
   1482      end)
   1483    end,
   1484  })
   1485 
   1486  if #events > 0 then
   1487    api.nvim_create_autocmd(events, {
   1488      group = augroup,
   1489      buffer = bufnr,
   1490      callback = function()
   1491        close_preview_window(winnr)
   1492      end,
   1493    })
   1494  end
   1495 end
   1496 
   1497 --- Computes size of float needed to show contents (with optional wrapping)
   1498 ---
   1499 ---@param contents string[] of lines to show in window
   1500 ---@param opts? vim.lsp.util.open_floating_preview.Opts
   1501 ---@return integer width size of float
   1502 ---@return integer height size of float
   1503 function M._make_floating_popup_size(contents, opts)
   1504  validate('contents', contents, 'table')
   1505  validate('opts', opts, 'table', true)
   1506  opts = opts or {}
   1507 
   1508  local width = opts.width
   1509  local height = opts.height
   1510  local wrap_at = opts.wrap_at
   1511  local max_width = opts.max_width
   1512  local max_height = opts.max_height
   1513  local line_widths = {} --- @type table<integer,integer>
   1514 
   1515  if not width then
   1516    width = 0
   1517    for i, line in ipairs(contents) do
   1518      -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
   1519      line_widths[i] = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
   1520      width = math.max(line_widths[i], width)
   1521    end
   1522  end
   1523 
   1524  local _, border_width = get_border_size(opts)
   1525  local screen_width = api.nvim_win_get_width(0)
   1526  width = math.min(width, screen_width)
   1527 
   1528  -- make sure borders are always inside the screen
   1529  width = math.min(width, screen_width - border_width)
   1530 
   1531  -- Make sure that the width is large enough to fit the title.
   1532  local title_length = 0
   1533  local chunks = type(opts.title) == 'string' and { { opts.title } } or opts.title or {}
   1534  for _, chunk in
   1535    ipairs(chunks --[=[@as [string, string][]]=])
   1536  do
   1537    title_length = title_length + vim.fn.strdisplaywidth(chunk[1])
   1538  end
   1539 
   1540  width = math.max(width, title_length)
   1541 
   1542  if wrap_at then
   1543    wrap_at = math.min(wrap_at, width)
   1544  end
   1545 
   1546  if max_width then
   1547    width = math.min(width, max_width)
   1548    wrap_at = math.min(wrap_at or max_width, max_width)
   1549  end
   1550 
   1551  if not height then
   1552    height = #contents
   1553    if wrap_at and width >= wrap_at then
   1554      height = 0
   1555      if vim.tbl_isempty(line_widths) then
   1556        for _, line in ipairs(contents) do
   1557          local line_width = vim.fn.strdisplaywidth(line:gsub('%z', '\n'))
   1558          height = height + math.max(1, math.ceil(line_width / wrap_at))
   1559        end
   1560      else
   1561        for i = 1, #contents do
   1562          height = height + math.max(1, math.ceil(line_widths[i] / wrap_at))
   1563        end
   1564      end
   1565    end
   1566  end
   1567  if max_height then
   1568    height = math.min(height, max_height)
   1569  end
   1570 
   1571  return width, height
   1572 end
   1573 
   1574 --- @class vim.lsp.util.open_floating_preview.Opts
   1575 ---
   1576 --- Height of floating window
   1577 --- @field height? integer
   1578 ---
   1579 --- Width of floating window
   1580 --- @field width? integer
   1581 ---
   1582 --- Wrap long lines
   1583 --- (default: `true`)
   1584 --- @field wrap? boolean
   1585 ---
   1586 --- Character to wrap at for computing height when wrap is enabled
   1587 --- @field wrap_at? integer
   1588 ---
   1589 --- Maximal width of floating window
   1590 --- @field max_width? integer
   1591 ---
   1592 --- Maximal height of floating window
   1593 --- @field max_height? integer
   1594 ---
   1595 --- If a popup with this id is opened, then focus it
   1596 --- @field focus_id? string
   1597 ---
   1598 --- List of events that closes the floating window
   1599 --- @field close_events? table
   1600 ---
   1601 --- Make float focusable.
   1602 --- (default: `true`)
   1603 --- @field focusable? boolean
   1604 ---
   1605 --- If `true`, and if {focusable} is also `true`, focus an existing floating
   1606 --- window with the same {focus_id}
   1607 --- (default: `true`)
   1608 --- @field focus? boolean
   1609 ---
   1610 --- offset to add to `col`
   1611 --- @field offset_x? integer
   1612 ---
   1613 --- offset to add to `row`
   1614 --- @field offset_y? integer
   1615 --- @field border? string|(string|[string,string])[] override `border`
   1616 --- @field zindex? integer override `zindex`, defaults to 50
   1617 --- @field title? string|[string,string][]
   1618 --- @field title_pos? 'left'|'center'|'right'
   1619 ---
   1620 --- (default: `'cursor'`)
   1621 --- @field relative? 'mouse'|'cursor'|'editor'
   1622 ---
   1623 --- Adjusts placement relative to cursor.
   1624 --- - "auto": place window based on which side of the cursor has more lines
   1625 --- - "above": place the window above the cursor unless there are not enough lines
   1626 ---   to display the full window height.
   1627 --- - "below": place the window below the cursor unless there are not enough lines
   1628 ---   to display the full window height.
   1629 --- (default: `'auto'`)
   1630 --- @field anchor_bias? 'auto'|'above'|'below'
   1631 ---
   1632 --- @field _update_win? integer
   1633 
   1634 --- Shows contents in a floating window.
   1635 ---
   1636 ---@param contents table of lines to show in window
   1637 ---@param syntax string of syntax to set for opened buffer
   1638 ---@param opts? vim.lsp.util.open_floating_preview.Opts with optional fields
   1639 --- (additional keys are filtered with |vim.lsp.util.make_floating_popup_options()|
   1640 --- before they are passed on to |nvim_open_win()|)
   1641 ---@return integer bufnr of newly created float window
   1642 ---@return integer winid of newly created float window preview window
   1643 function M.open_floating_preview(contents, syntax, opts)
   1644  validate('contents', contents, 'table')
   1645  validate('syntax', syntax, 'string', true)
   1646  validate('opts', opts, 'table', true)
   1647  opts = opts or {}
   1648  opts.wrap = opts.wrap ~= false -- wrapping by default
   1649  opts.focus = opts.focus ~= false
   1650  opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' }
   1651 
   1652  local bufnr = api.nvim_get_current_buf()
   1653 
   1654  local floating_winnr = opts._update_win
   1655 
   1656  -- Create/get the buffer
   1657  local floating_bufnr --- @type integer
   1658  if floating_winnr then
   1659    floating_bufnr = api.nvim_win_get_buf(floating_winnr)
   1660  else
   1661    -- check if this popup is focusable and we need to focus
   1662    if opts.focus_id and opts.focusable ~= false and opts.focus then
   1663      -- Go back to previous window if we are in a focusable one
   1664      local current_winnr = api.nvim_get_current_win()
   1665      if vim.w[current_winnr][opts.focus_id] then
   1666        api.nvim_command('wincmd p')
   1667        return bufnr, current_winnr
   1668      end
   1669      do
   1670        local win = find_window_by_var(opts.focus_id, bufnr)
   1671        if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
   1672          -- focus and return the existing buf, win
   1673          api.nvim_set_current_win(win)
   1674          api.nvim_command('stopinsert')
   1675          return api.nvim_win_get_buf(win), win
   1676        end
   1677      end
   1678    end
   1679 
   1680    -- check if another floating preview already exists for this buffer
   1681    -- and close it if needed
   1682    local existing_float = vim.b[bufnr].lsp_floating_preview
   1683    if existing_float and api.nvim_win_is_valid(existing_float) then
   1684      api.nvim_win_close(existing_float, true)
   1685    end
   1686    floating_bufnr = api.nvim_create_buf(false, true)
   1687  end
   1688 
   1689  -- Set up the contents, using treesitter for markdown
   1690  local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil
   1691 
   1692  if do_stylize then
   1693    local width = M._make_floating_popup_size(contents, opts)
   1694    contents = M._normalize_markdown(contents, { width = width })
   1695  else
   1696    -- Clean up input: trim empty lines
   1697    contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true })
   1698 
   1699    if syntax then
   1700      vim.bo[floating_bufnr].syntax = syntax
   1701    end
   1702  end
   1703 
   1704  vim.bo[floating_bufnr].modifiable = true
   1705  api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents)
   1706 
   1707  if floating_winnr then
   1708    api.nvim_win_set_config(floating_winnr, {
   1709      border = opts.border,
   1710      title = opts.title,
   1711    })
   1712  else
   1713    -- Compute size of float needed to show (wrapped) lines
   1714    if opts.wrap then
   1715      opts.wrap_at = opts.wrap_at or api.nvim_win_get_width(0)
   1716    else
   1717      opts.wrap_at = nil
   1718    end
   1719 
   1720    -- TODO(lewis6991): These function assume the current window to determine options,
   1721    -- therefore it won't work for opts._update_win and the current window if the floating
   1722    -- window
   1723    local width, height = M._make_floating_popup_size(contents, opts)
   1724    local float_option = M.make_floating_popup_options(width, height, opts)
   1725 
   1726    floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
   1727 
   1728    api.nvim_buf_set_keymap(
   1729      floating_bufnr,
   1730      'n',
   1731      'q',
   1732      '<cmd>bdelete<cr>',
   1733      { silent = true, noremap = true, nowait = true }
   1734    )
   1735    close_preview_autocmd(opts.close_events, floating_winnr, floating_bufnr, bufnr)
   1736 
   1737    -- save focus_id
   1738    if opts.focus_id then
   1739      api.nvim_win_set_var(floating_winnr, opts.focus_id, bufnr)
   1740    end
   1741    api.nvim_buf_set_var(bufnr, 'lsp_floating_preview', floating_winnr)
   1742    api.nvim_win_set_var(floating_winnr, 'lsp_floating_bufnr', bufnr)
   1743  end
   1744 
   1745  api.nvim_create_autocmd('WinClosed', {
   1746    group = api.nvim_create_augroup('nvim.closing_floating_preview', { clear = true }),
   1747    callback = function(args)
   1748      local winid = tonumber(args.match)
   1749      local ok, preview_bufnr = pcall(api.nvim_win_get_var, winid, 'lsp_floating_bufnr')
   1750      if
   1751        ok
   1752        and api.nvim_buf_is_valid(preview_bufnr)
   1753        and winid == vim.b[preview_bufnr].lsp_floating_preview
   1754      then
   1755        vim.b[bufnr].lsp_floating_preview = nil
   1756        return true
   1757      end
   1758    end,
   1759  })
   1760 
   1761  vim.wo[floating_winnr].foldenable = false -- Disable folding.
   1762  vim.wo[floating_winnr].wrap = opts.wrap -- Soft wrapping.
   1763  vim.wo[floating_winnr].linebreak = true -- Break lines a bit nicer
   1764  vim.wo[floating_winnr].breakindent = true -- Slightly better list presentation.
   1765  vim.wo[floating_winnr].smoothscroll = true -- Scroll by screen-line instead of buffer-line.
   1766 
   1767  vim.bo[floating_bufnr].modifiable = false
   1768  vim.bo[floating_bufnr].bufhidden = 'wipe'
   1769 
   1770  if do_stylize then
   1771    vim.wo[floating_winnr].conceallevel = 2
   1772    vim.wo[floating_winnr].concealcursor = ''
   1773    vim.bo[floating_bufnr].filetype = 'markdown'
   1774    vim.treesitter.start(floating_bufnr)
   1775    if not opts.height then
   1776      -- Reduce window height if TS highlighter conceals code block backticks.
   1777      local win_height = api.nvim_win_get_height(floating_winnr)
   1778      local text_height = api.nvim_win_text_height(floating_winnr, { max_height = win_height }).all
   1779      if text_height < win_height then
   1780        api.nvim_win_set_height(floating_winnr, text_height)
   1781      end
   1782    end
   1783  end
   1784 
   1785  return floating_bufnr, floating_winnr
   1786 end
   1787 
   1788 do --[[ References ]]
   1789  local reference_ns = api.nvim_create_namespace('nvim.lsp.references')
   1790 
   1791  --- Removes document highlights from a buffer.
   1792  ---
   1793  ---@param bufnr integer? Buffer id
   1794  function M.buf_clear_references(bufnr)
   1795    api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1)
   1796  end
   1797 
   1798  --- Shows a list of document highlights for a certain buffer.
   1799  ---
   1800  ---@param bufnr integer Buffer id
   1801  ---@param references lsp.DocumentHighlight[] objects to highlight
   1802  ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
   1803  ---@see https://microsoft.github.io/language-server-protocol/specification/#textDocumentContentChangeEvent
   1804  function M.buf_highlight_references(bufnr, references, position_encoding)
   1805    validate('bufnr', bufnr, 'number', true)
   1806    validate('position_encoding', position_encoding, 'string', false)
   1807    for _, reference in ipairs(references) do
   1808      local range = reference.range
   1809      local start_line = range.start.line
   1810      local end_line = range['end'].line
   1811 
   1812      local start_idx = get_line_byte_from_position(bufnr, range.start, position_encoding)
   1813      local end_idx = get_line_byte_from_position(bufnr, range['end'], position_encoding)
   1814 
   1815      local document_highlight_kind = {
   1816        [protocol.DocumentHighlightKind.Text] = 'LspReferenceText',
   1817        [protocol.DocumentHighlightKind.Read] = 'LspReferenceRead',
   1818        [protocol.DocumentHighlightKind.Write] = 'LspReferenceWrite',
   1819      }
   1820      local kind = reference['kind'] or protocol.DocumentHighlightKind.Text
   1821      vim.hl.range(
   1822        bufnr,
   1823        reference_ns,
   1824        document_highlight_kind[kind],
   1825        { start_line, start_idx },
   1826        { end_line, end_idx },
   1827        { priority = vim.hl.priorities.user }
   1828      )
   1829    end
   1830  end
   1831 end
   1832 
   1833 local position_sort = sort_by_key(function(v)
   1834  return { v.start.line, v.start.character }
   1835 end)
   1836 
   1837 --- Returns the items with the byte position calculated correctly and in sorted
   1838 --- order, for display in quickfix and location lists.
   1839 ---
   1840 --- The `user_data` field of each resulting item will contain the original
   1841 --- `Location` or `LocationLink` it was computed from.
   1842 ---
   1843 --- The result can be passed to the {list} argument of |setqflist()| or
   1844 --- |setloclist()|.
   1845 ---
   1846 ---@param locations lsp.Location[]|lsp.LocationLink[]
   1847 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
   1848 ---                         default to first client of buffer
   1849 ---@return vim.quickfix.entry[] # See |setqflist()| for the format
   1850 function M.locations_to_items(locations, position_encoding)
   1851  if position_encoding == nil then
   1852    vim.notify_once(
   1853      'locations_to_items must be called with valid position encoding',
   1854      vim.log.levels.WARN
   1855    )
   1856    position_encoding = vim.lsp.get_clients({ bufnr = 0 })[1].offset_encoding
   1857  end
   1858 
   1859  local items = {} --- @type vim.quickfix.entry[]
   1860 
   1861  ---@type table<string, {start: lsp.Position, end: lsp.Position, location: lsp.Location|lsp.LocationLink}[]>
   1862  local grouped = {}
   1863  for _, d in ipairs(locations) do
   1864    -- locations may be Location or LocationLink
   1865    local uri = d.uri or d.targetUri
   1866    local range = d.range or d.targetSelectionRange
   1867    grouped[uri] = grouped[uri] or {}
   1868    table.insert(grouped[uri], { start = range.start, ['end'] = range['end'], location = d })
   1869  end
   1870 
   1871  for uri, rows in vim.spairs(grouped) do
   1872    table.sort(rows, position_sort)
   1873    local filename = vim.uri_to_fname(uri)
   1874 
   1875    local line_numbers = {}
   1876    for _, temp in ipairs(rows) do
   1877      table.insert(line_numbers, temp.start.line)
   1878      if temp.start.line ~= temp['end'].line then
   1879        table.insert(line_numbers, temp['end'].line)
   1880      end
   1881    end
   1882 
   1883    -- get all the lines for this uri
   1884    local lines = get_lines(vim.uri_to_bufnr(uri), line_numbers)
   1885 
   1886    for _, temp in ipairs(rows) do
   1887      local pos = temp.start
   1888      local end_pos = temp['end']
   1889      local row = pos.line
   1890      local end_row = end_pos.line
   1891      local line = lines[row] or ''
   1892      local end_line = lines[end_row] or ''
   1893      local col = vim.str_byteindex(line, position_encoding, pos.character, false)
   1894      local end_col = vim.str_byteindex(end_line, position_encoding, end_pos.character, false)
   1895 
   1896      items[#items + 1] = {
   1897        filename = filename,
   1898        lnum = row + 1,
   1899        end_lnum = end_row + 1,
   1900        col = col + 1,
   1901        end_col = end_col + 1,
   1902        text = line,
   1903        user_data = temp.location,
   1904      }
   1905    end
   1906  end
   1907  return items
   1908 end
   1909 
   1910 --- Converts symbols to quickfix list items.
   1911 ---
   1912 ---@param symbols lsp.DocumentSymbol[]|lsp.SymbolInformation[]|lsp.WorkspaceSymbol[] list of symbols
   1913 ---@param bufnr? integer buffer handle or 0 for current, defaults to current
   1914 ---@param position_encoding? 'utf-8'|'utf-16'|'utf-32'
   1915 ---                         default to first client of buffer
   1916 ---@return vim.quickfix.entry[] # See |setqflist()| for the format
   1917 function M.symbols_to_items(symbols, bufnr, position_encoding)
   1918  bufnr = vim._resolve_bufnr(bufnr)
   1919  if position_encoding == nil then
   1920    vim.notify_once(
   1921      'symbols_to_items must be called with valid position encoding',
   1922      vim.log.levels.WARN
   1923    )
   1924    position_encoding = assert(vim.lsp.get_clients({ bufnr = bufnr })[1]).offset_encoding
   1925  end
   1926 
   1927  local items = {} --- @type vim.quickfix.entry[]
   1928  for _, symbol in ipairs(symbols) do
   1929    --- @type string?, lsp.Range?
   1930    local filename, range
   1931 
   1932    if symbol.location then
   1933      --- @cast symbol lsp.SymbolInformation
   1934      filename = vim.uri_to_fname(symbol.location.uri)
   1935      range = symbol.location.range
   1936    elseif symbol.selectionRange then
   1937      --- @cast symbol lsp.DocumentSymbol
   1938      filename = api.nvim_buf_get_name(bufnr)
   1939      range = symbol.selectionRange
   1940    end
   1941 
   1942    if filename and range then
   1943      local kind = protocol.SymbolKind[symbol.kind] or 'Unknown'
   1944 
   1945      local lnum = range['start'].line + 1
   1946      local col = get_line_byte_from_position(bufnr, range['start'], position_encoding) + 1
   1947      local end_lnum = range['end'].line + 1
   1948      local end_col = get_line_byte_from_position(bufnr, range['end'], position_encoding) + 1
   1949 
   1950      local is_deprecated = symbol.deprecated
   1951        or (symbol.tags and vim.tbl_contains(symbol.tags, protocol.SymbolTag.Deprecated))
   1952      local text = string.format(
   1953        '[%s] %s%s%s',
   1954        kind,
   1955        symbol.name,
   1956        symbol.containerName and ' in ' .. symbol.containerName or '',
   1957        is_deprecated and ' (deprecated)' or ''
   1958      )
   1959 
   1960      items[#items + 1] = {
   1961        filename = filename,
   1962        lnum = lnum,
   1963        col = col,
   1964        end_lnum = end_lnum,
   1965        end_col = end_col,
   1966        kind = kind,
   1967        text = text,
   1968      }
   1969    end
   1970 
   1971    if symbol.children then
   1972      list_extend(items, M.symbols_to_items(symbol.children, bufnr, position_encoding))
   1973    end
   1974  end
   1975 
   1976  return items
   1977 end
   1978 
   1979 --- Removes empty lines from the beginning and end.
   1980 ---@deprecated use `vim.split()` with `trimempty` instead
   1981 ---@param lines table list of lines to trim
   1982 ---@return table trimmed list of lines
   1983 function M.trim_empty_lines(lines)
   1984  vim.deprecate('vim.lsp.util.trim_empty_lines()', 'vim.split() with `trimempty`', '0.12')
   1985  local start = 1
   1986  for i = 1, #lines do
   1987    if lines[i] ~= nil and #lines[i] > 0 then
   1988      start = i
   1989      break
   1990    end
   1991  end
   1992  local finish = 1
   1993  for i = #lines, 1, -1 do
   1994    if lines[i] ~= nil and #lines[i] > 0 then
   1995      finish = i
   1996      break
   1997    end
   1998  end
   1999  return vim.list_slice(lines, start, finish)
   2000 end
   2001 
   2002 --- Accepts markdown lines and tries to reduce them to a filetype if they
   2003 --- comprise just a single code block.
   2004 ---
   2005 --- CAUTION: Modifies the input in-place!
   2006 ---
   2007 ---@deprecated
   2008 ---@param lines string[] list of lines
   2009 ---@return string filetype or "markdown" if it was unchanged.
   2010 function M.try_trim_markdown_code_blocks(lines)
   2011  vim.deprecate('vim.lsp.util.try_trim_markdown_code_blocks()', nil, '0.12')
   2012  local language_id = assert(lines[1]):match('^```(.*)')
   2013  if language_id then
   2014    local has_inner_code_fence = false
   2015    for i = 2, (#lines - 1) do
   2016      local line = lines[i] --[[@as string]]
   2017      if line:sub(1, 3) == '```' then
   2018        has_inner_code_fence = true
   2019        break
   2020      end
   2021    end
   2022    -- No inner code fences + starting with code fence = hooray.
   2023    if not has_inner_code_fence then
   2024      table.remove(lines, 1)
   2025      table.remove(lines)
   2026      return language_id
   2027    end
   2028  end
   2029  return 'markdown'
   2030 end
   2031 
   2032 ---@param window integer?: |window-ID| or 0 for current, defaults to current
   2033 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
   2034 local function make_position_param(window, position_encoding)
   2035  window = window or 0
   2036  local buf = api.nvim_win_get_buf(window)
   2037  local row, col = unpack(api.nvim_win_get_cursor(window))
   2038  row = row - 1
   2039  local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
   2040  if not line then
   2041    return { line = 0, character = 0 }
   2042  end
   2043 
   2044  col = vim.str_utfindex(line, position_encoding, col, false)
   2045 
   2046  return { line = row, character = col }
   2047 end
   2048 
   2049 --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position.
   2050 ---
   2051 ---@param window integer?: |window-ID| or 0 for current, defaults to current
   2052 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
   2053 ---@return lsp.TextDocumentPositionParams
   2054 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams
   2055 function M.make_position_params(window, position_encoding)
   2056  window = window or 0
   2057  local buf = api.nvim_win_get_buf(window)
   2058  if position_encoding == nil then
   2059    vim.notify_once(
   2060      'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.',
   2061      vim.log.levels.WARN
   2062    )
   2063    --- @diagnostic disable-next-line: deprecated
   2064    position_encoding = M._get_offset_encoding(buf)
   2065  end
   2066  return {
   2067    textDocument = M.make_text_document_params(buf),
   2068    position = make_position_param(window, position_encoding),
   2069  }
   2070 end
   2071 
   2072 --- Utility function for getting the encoding of the first LSP client on the given buffer.
   2073 ---@deprecated
   2074 ---@param bufnr integer buffer handle or 0 for current, defaults to current
   2075 ---@return 'utf-8'|'utf-16'|'utf-32' encoding first client if there is one, nil otherwise
   2076 function M._get_offset_encoding(bufnr)
   2077  validate('bufnr', bufnr, 'number', true)
   2078 
   2079  local offset_encoding --- @type 'utf-8'|'utf-16'|'utf-32'?
   2080 
   2081  for _, client in pairs(vim.lsp.get_clients({ bufnr = bufnr })) do
   2082    if client.offset_encoding == nil then
   2083      vim.notify_once(
   2084        string.format(
   2085          'Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.',
   2086          client.id
   2087        ),
   2088        vim.log.levels.ERROR
   2089      )
   2090    end
   2091    local this_offset_encoding = client.offset_encoding
   2092    if not offset_encoding then
   2093      offset_encoding = this_offset_encoding
   2094    elseif offset_encoding ~= this_offset_encoding then
   2095      vim.notify_once(
   2096        'warning: multiple different client offset_encodings detected for buffer, vim.lsp.util._get_offset_encoding() uses the offset_encoding from the first client',
   2097        vim.log.levels.WARN
   2098      )
   2099    end
   2100  end
   2101  --- @cast offset_encoding -? hack - not safe
   2102 
   2103  return offset_encoding
   2104 end
   2105 
   2106 --- Using the current position in the current buffer, creates an object that
   2107 --- can be used as a building block for several LSP requests, such as
   2108 --- `textDocument/codeAction`, `textDocument/colorPresentation`,
   2109 --- `textDocument/rangeFormatting`.
   2110 ---
   2111 ---@param window integer?: |window-ID| or 0 for current, defaults to current
   2112 ---@param position_encoding "utf-8"|"utf-16"|"utf-32"
   2113 ---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
   2114 function M.make_range_params(window, position_encoding)
   2115  local buf = api.nvim_win_get_buf(window or 0)
   2116  if position_encoding == nil then
   2117    vim.notify_once(
   2118      'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.',
   2119      vim.log.levels.WARN
   2120    )
   2121    --- @diagnostic disable-next-line: deprecated
   2122    position_encoding = M._get_offset_encoding(buf)
   2123  end
   2124  local position = make_position_param(window, position_encoding)
   2125  return {
   2126    textDocument = M.make_text_document_params(buf),
   2127    range = { start = position, ['end'] = position },
   2128  }
   2129 end
   2130 
   2131 --- Using the given range in the current buffer, creates an object that
   2132 --- is similar to |vim.lsp.util.make_range_params()|.
   2133 ---
   2134 ---@param start_pos [integer,integer]? {row,col} mark-indexed position.
   2135 --- Defaults to the start of the last visual selection.
   2136 ---@param end_pos [integer,integer]? {row,col} mark-indexed position.
   2137 --- Defaults to the end of the last visual selection.
   2138 ---@param bufnr integer? buffer handle or 0 for current, defaults to current
   2139 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
   2140 ---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range }
   2141 function M.make_given_range_params(start_pos, end_pos, bufnr, position_encoding)
   2142  validate('start_pos', start_pos, 'table', true)
   2143  validate('end_pos', end_pos, 'table', true)
   2144  validate('position_encoding', position_encoding, 'string', true)
   2145  bufnr = vim._resolve_bufnr(bufnr)
   2146  if position_encoding == nil then
   2147    vim.notify_once(
   2148      'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.',
   2149      vim.log.levels.WARN
   2150    )
   2151    --- @diagnostic disable-next-line: deprecated
   2152    position_encoding = M._get_offset_encoding(bufnr)
   2153  end
   2154  --- @type [integer, integer]
   2155  local A = { unpack(start_pos or api.nvim_buf_get_mark(bufnr, '<')) }
   2156  --- @type [integer, integer]
   2157  local B = { unpack(end_pos or api.nvim_buf_get_mark(bufnr, '>')) }
   2158  -- convert to 0-index
   2159  A[1] = A[1] - 1
   2160  B[1] = B[1] - 1
   2161  -- account for position_encoding.
   2162  if A[2] > 0 then
   2163    A[2] = M.character_offset(bufnr, A[1], A[2], position_encoding)
   2164  end
   2165  if B[2] > 0 then
   2166    B[2] = M.character_offset(bufnr, B[1], B[2], position_encoding)
   2167  end
   2168  -- we need to offset the end character position otherwise we loose the last
   2169  -- character of the selection, as LSP end position is exclusive
   2170  -- see https://microsoft.github.io/language-server-protocol/specification#range
   2171  if vim.o.selection ~= 'exclusive' then
   2172    B[2] = B[2] + 1
   2173  end
   2174  return {
   2175    textDocument = M.make_text_document_params(bufnr),
   2176    range = {
   2177      start = { line = A[1], character = A[2] },
   2178      ['end'] = { line = B[1], character = B[2] },
   2179    },
   2180  }
   2181 end
   2182 
   2183 --- Creates a `TextDocumentIdentifier` object for the current buffer.
   2184 ---
   2185 ---@param bufnr integer?: Buffer handle, defaults to current
   2186 ---@return lsp.TextDocumentIdentifier
   2187 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier
   2188 function M.make_text_document_params(bufnr)
   2189  return { uri = vim.uri_from_bufnr(bufnr or 0) }
   2190 end
   2191 
   2192 --- Create the workspace params
   2193 ---@param added lsp.WorkspaceFolder[]
   2194 ---@param removed lsp.WorkspaceFolder[]
   2195 ---@return lsp.DidChangeWorkspaceFoldersParams
   2196 function M.make_workspace_params(added, removed)
   2197  return { event = { added = added, removed = removed } }
   2198 end
   2199 
   2200 --- Returns indentation size.
   2201 ---
   2202 ---@see 'shiftwidth'
   2203 ---@param bufnr integer?: Buffer handle, defaults to current
   2204 ---@return integer indentation size
   2205 function M.get_effective_tabstop(bufnr)
   2206  validate('bufnr', bufnr, 'number', true)
   2207  local bo = bufnr and vim.bo[bufnr] or vim.bo
   2208  local sw = bo.shiftwidth
   2209  return (sw == 0 and bo.tabstop) or sw
   2210 end
   2211 
   2212 --- Creates a `DocumentFormattingParams` object for the current buffer and cursor position.
   2213 ---
   2214 ---@param options lsp.FormattingOptions? with valid `FormattingOptions` entries
   2215 ---@return lsp.DocumentFormattingParams object
   2216 ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting
   2217 function M.make_formatting_params(options)
   2218  validate('options', options, 'table', true)
   2219  options = vim.tbl_extend('keep', options or {}, {
   2220    tabSize = M.get_effective_tabstop(),
   2221    insertSpaces = vim.bo.expandtab,
   2222  })
   2223  return {
   2224    textDocument = { uri = vim.uri_from_bufnr(0) },
   2225    options = options,
   2226  }
   2227 end
   2228 
   2229 --- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer.
   2230 ---
   2231 ---@param buf integer buffer number (0 for current)
   2232 ---@param row integer 0-indexed line
   2233 ---@param col integer 0-indexed byte offset in line
   2234 ---@param offset_encoding? 'utf-8'|'utf-16'|'utf-32'
   2235 ---                        defaults to `offset_encoding` of first client of `buf`
   2236 ---@return integer `offset_encoding` index of the character in line {row} column {col} in buffer {buf}
   2237 function M.character_offset(buf, row, col, offset_encoding)
   2238  local line = get_line(buf, row)
   2239  if offset_encoding == nil then
   2240    vim.notify_once(
   2241      'character_offset must be called with valid offset encoding',
   2242      vim.log.levels.WARN
   2243    )
   2244    offset_encoding = assert(vim.lsp.get_clients({ bufnr = buf })[1]).offset_encoding
   2245  end
   2246  return vim.str_utfindex(line, offset_encoding, col, false)
   2247 end
   2248 
   2249 --- Helper function to return nested values in language server settings
   2250 ---
   2251 ---@param settings table language server settings
   2252 ---@param section  string indicating the field of the settings table
   2253 ---@return table|string|vim.NIL The value of settings accessed via section. `vim.NIL` if not found.
   2254 ---@deprecated
   2255 function M.lookup_section(settings, section)
   2256  vim.deprecate('vim.lsp.util.lookup_section()', 'vim.tbl_get() with `vim.split`', '0.12')
   2257  for part in vim.gsplit(section, '.', { plain = true }) do
   2258    --- @diagnostic disable-next-line:no-unknown
   2259    settings = settings[part]
   2260    if settings == nil then
   2261      return vim.NIL
   2262    end
   2263  end
   2264  return settings
   2265 end
   2266 
   2267 --- Converts line range (0-based, end-inclusive) to lsp range,
   2268 --- handles absence of a trailing newline
   2269 ---
   2270 ---@param bufnr integer
   2271 ---@param start_line integer
   2272 ---@param end_line integer
   2273 ---@param position_encoding 'utf-8'|'utf-16'|'utf-32'
   2274 ---@return lsp.Range
   2275 function M._make_line_range_params(bufnr, start_line, end_line, position_encoding)
   2276  local last_line = api.nvim_buf_line_count(bufnr) - 1
   2277 
   2278  ---@type lsp.Position
   2279  local end_pos
   2280 
   2281  if end_line == last_line and not vim.bo[bufnr].endofline then
   2282    end_pos = {
   2283      line = end_line,
   2284      character = M.character_offset(
   2285        bufnr,
   2286        end_line,
   2287        #get_line(bufnr, end_line),
   2288        position_encoding
   2289      ),
   2290    }
   2291  else
   2292    end_pos = { line = end_line + 1, character = 0 }
   2293  end
   2294 
   2295  return {
   2296    start = { line = start_line, character = 0 },
   2297    ['end'] = end_pos,
   2298  }
   2299 end
   2300 
   2301 ---@class (private) vim.lsp.util._cancel_requests.Filter
   2302 ---@field bufnr? integer
   2303 ---@field clients? vim.lsp.Client[]
   2304 ---@field method? vim.lsp.protocol.Method.ClientToServer.Request
   2305 ---@field type? string
   2306 
   2307 --- Cancel all {filter}ed requests.
   2308 ---
   2309 ---@param filter? vim.lsp.util._cancel_requests.Filter
   2310 function M._cancel_requests(filter)
   2311  filter = filter or {}
   2312  local bufnr = filter.bufnr and vim._resolve_bufnr(filter.bufnr) or nil
   2313  local clients = filter.clients
   2314  local method = filter.method
   2315  local type = filter.type
   2316 
   2317  for _, client in
   2318    ipairs(clients or vim.lsp.get_clients({
   2319      bufnr = bufnr,
   2320      method = method,
   2321    }))
   2322  do
   2323    for id, request in pairs(client.requests) do
   2324      if
   2325        (bufnr == nil or bufnr == request.bufnr)
   2326        and (method == nil or method == request.method)
   2327        and (type == nil or type == request.type)
   2328      then
   2329        client:cancel_request(id)
   2330      end
   2331    end
   2332  end
   2333 end
   2334 
   2335 M._get_line_byte_from_position = get_line_byte_from_position
   2336 
   2337 ---@nodoc
   2338 ---@type table<integer,integer>
   2339 M.buf_versions = setmetatable({}, {
   2340  __index = function(t, bufnr)
   2341    return rawget(t, bufnr) or 0
   2342  end,
   2343 })
   2344 
   2345 return M