Files
ethersync-p2p/nvim-plugin/lua/ethersync/utils.lua
aileot 201101b11c refactor(nvim): insert "ethersync" to each module
not to pollute lua package.loaded namespaces with general terms.
2025-07-30 17:54:22 +02:00

173 lines
6.8 KiB
Lua

-- SPDX-FileCopyrightText: 2019 Neovim contributors
--
-- SPDX-License-Identifier: Apache-2.0
local M = {}
-- The following functions are taken from the Neovim source code:
-- https://github.com/neovim/neovim/blob/master/runtime/lua/vim/lsp/util.lua
--- Gets the zero-indexed lines from the given buffer.
---
---@param bufnr integer bufnr to get the lines from
---@param rows integer[] zero-indexed line numbers
---@return table<integer, string>|string a table mapping rows to lines
local function get_lines(bufnr, rows)
rows = type(rows) == "table" and rows or { rows }
local lines = {}
for _, row in ipairs(rows) do
lines[row] = (vim.api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { "" })[1]
end
return lines
end
--- Gets the zero-indexed line from the given buffer.
--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events.
--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI.
---
---@param bufnr integer
---@param row integer zero-indexed line number
---@return string the line at row in filename
local function get_line(bufnr, row)
return get_lines(bufnr, { row })[row]
end
--- Applies a list of text edits to a buffer.
---@param text_edits table list of `TextEdit` objects
---@param bufnr integer Buffer id
---@param offset_encoding string utf-8|utf-16|utf-32
---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit
function M.apply_text_edits(text_edits, bufnr, offset_encoding)
vim.validate({
text_edits = { text_edits, "t", false },
bufnr = { bufnr, "number", false },
offset_encoding = { offset_encoding, "string", false },
})
if not next(text_edits) then
return
end
vim.bo[bufnr].buflisted = true
-- Fix reversed range and indexing each text_edits
local index = 0
text_edits = vim.tbl_map(function(text_edit)
index = index + 1
text_edit._index = index
if
text_edit.range.start.line > text_edit.range["end"].line
or text_edit.range.start.line == text_edit.range["end"].line
and text_edit.range.start.character > text_edit.range["end"].character
then
local start = text_edit.range.start
text_edit.range.start = text_edit.range["end"]
text_edit.range["end"] = start
end
return text_edit
end, text_edits)
-- Sort text_edits
table.sort(text_edits, function(a, b)
if a.range.start.line ~= b.range.start.line then
return a.range.start.line > b.range.start.line
end
if a.range.start.character ~= b.range.start.character then
return a.range.start.character > b.range.start.character
end
if a._index ~= b._index then
return a._index > b._index
end
end)
-- save and restore local marks since they get deleted by nvim_buf_set_lines
local marks = {}
for _, m in pairs(vim.fn.getmarklist(bufnr)) do
if m.mark:match("^'[a-z]$") then
marks[m.mark:sub(2, 2)] = { m.pos[2], m.pos[3] - 1 } -- api-indexed
end
end
-- Apply text edits.
local disable_eol = false
for _, text_edit in ipairs(text_edits) do
-- Normalize line ending
text_edit.newText, _ = string.gsub(text_edit.newText, "\r\n?", "\n")
-- Convert from LSP style ranges to Neovim style ranges.
local e = {
start_row = text_edit.range.start.line,
start_col = vim.lsp.util._get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding),
end_row = text_edit.range["end"].line,
end_col = vim.lsp.util._get_line_byte_from_position(bufnr, text_edit.range["end"], offset_encoding),
text = vim.split(text_edit.newText, "\n", { plain = true }),
}
local max = vim.api.nvim_buf_line_count(bufnr)
-- If the whole edit is after the lines in the buffer we can simply add the new text to the end
-- of the buffer.
if max <= e.start_row then
-- If the replacement text ends with a newline, drop it - we're happy with keeping the implicit
-- newline at the end of the buffer (which we're assuming to be there, because we're getting
-- an insert *after* the visible lines.
-- TODO: Is this always correct?
if #e.text > 1 and e.text[#e.text] == "" then
table.remove(e.text)
end
vim.api.nvim_buf_set_lines(bufnr, max, max, false, e.text)
else
local last_line_len = #(get_line(bufnr, math.min(e.end_row, max - 1)) or "")
-- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't
-- accept it so we should fix it here.
if max <= e.end_row then
e.end_row = max - 1
e.end_col = last_line_len
disable_eol = true
-- "a" + 'eol' + replace((0,1), (1,0), "") => "a" + 'noeol'
-- "a" + 'eol' + replace((0,1), (1,0), "\n\n") => "a\n\n" + 'noeol' (I guess?)
else
-- If the replacement is over the end of a line (i.e. e.end_col is out of bounds and the
-- replacement text ends with a newline We can likely assume that the replacement is assumed
-- to be meant to replace the newline with another newline and we need to make sure this
-- doesn't add an extra empty line. E.g. when the last line to be replaced contains a '\r'
-- in the file some servers (clangd on windows) will include that character in the line
-- while nvim_buf_set_text doesn't count it as part of the line.
if
e.end_col > last_line_len
and #text_edit.newText > 0
and string.sub(text_edit.newText, -1) == "\n"
then
table.remove(e.text, #e.text)
end
end
-- Make sure we don't go out of bounds for e.end_col
e.end_col = math.min(last_line_len, e.end_col)
vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text)
end
end
local max = vim.api.nvim_buf_line_count(bufnr)
-- no need to restore marks that still exist
for _, m in pairs(vim.fn.getmarklist(bufnr)) do
marks[m.mark:sub(2, 2)] = nil
end
-- restore marks
for mark, pos in pairs(marks) do
if pos then
-- make sure we don't go out of bounds
pos[1] = math.min(pos[1], max)
pos[2] = math.min(pos[2], #(get_line(bufnr, pos[1] - 1) or ""))
vim.api.nvim_buf_set_mark(bufnr or 0, mark, pos[1], pos[2], {})
end
end
if disable_eol then
vim.bo.eol = false
end
end
return M