From 6f5ba9ebf688c8d03bcce85f8b58a24719c3d2d4 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 8 May 2026 18:39:26 +0800 Subject: [PATCH 1/3] fix(pos): precisely handle positions at the end of the file --- runtime/lua/vim/pos.lua | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua index 1b6e8e6e7f..14e3f5138c 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -143,6 +143,12 @@ function M.to_lsp(pos, position_encoding) -- we can ignore the difference between byte and character. if col > 0 then col = vim.str_utfindex(get_line(buf, row), position_encoding, col, false) + elseif col == 0 and row == api.nvim_buf_line_count(buf) and not vim.bo[buf].endofline then + -- Some LSP servers reject ranges that end at the virtual EOF position + -- (i.e., `[line_count, 0]`) when the buffer has no trailing newline. + -- Normalize such positions to the end of the last real line instead. + row = row - 1 + col = vim.str_utfindex(get_line(buf, row), position_encoding) end ---@type lsp.Position @@ -178,7 +184,7 @@ function M.lsp(buf, pos, position_encoding) if col > 0 then -- `strict_indexing` is disabled, because LSP responses are asynchronous, -- and the buffer content may have changed, causing out-of-bounds errors. - col = vim.str_byteindex(get_line(buf, row), position_encoding, col, false) + col = vim.str_byteindex(get_line(buf, row) or '', position_encoding, col, false) end return M.new(buf, row, col) @@ -202,10 +208,21 @@ end ---@param pos vim.Pos ---@return integer, integer function M.to_extmark(pos) - local line_count = api.nvim_buf_line_count(pos.buf) - local row, col = pos[1], pos[2] - if col == 0 and row == line_count then + -- Consider a buffer like this: + -- ``` + -- 0123456 + -- abcdefg + -- ``` + -- + -- Two ways to describe the range of the first line, i.e. '0123456': + -- 1. `{ start_row = 0, start_col = 0, end_row = 0, end_col = 7 }` + -- 2. `{ start_row = 0, start_col = 0, end_row = 1, end_col = 0 }` + -- + -- Both of the above methods satisfy the "end-exclusive" definition, + -- but `nvim_buf_set_extmark()` throws an out-of-bounds error for the second method, + -- so we need to convert it to the first method. + if col == 0 and row == api.nvim_buf_line_count(pos.buf) then row = row - 1 col = #get_line(pos.buf, row) end From 1c687c76b0abadcbed1dbf3669cba7aad8f23b7e Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Fri, 15 May 2026 19:16:10 +0800 Subject: [PATCH 2/3] refactor(pos): move `get_lines` from `lsp.util` to `pos` --- runtime/lua/vim/lsp/util.lua | 93 +------------------------------- runtime/lua/vim/pos.lua | 102 +++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 96 deletions(-) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 077e1831b8..5d3538db6b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -133,98 +133,9 @@ local function sort_by_key(fn) end end ---- Gets the zero-indexed lines from the given buffer. ---- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. ---- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. ---- ----@param bufnr integer bufnr to get the lines from ----@param rows integer[] zero-indexed line numbers ----@return table # a table mapping rows to lines -local function get_lines(bufnr, rows) - --- @type integer[] - rows = type(rows) == 'table' and rows or { rows } +local get_lines = vim.pos._get_lines - -- This is needed for bufload and bufloaded - bufnr = vim._resolve_bufnr(bufnr) - - local function buf_lines() - local lines = {} --- @type table - for _, row in ipairs(rows) do - lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] - end - return lines - end - - -- use loaded buffers if available - if vim.fn.bufloaded(bufnr) == 1 then - return buf_lines() - end - - local uri = vim.uri_from_bufnr(bufnr) - - -- load the buffer if this is not a file uri - -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. - if uri:sub(1, 4) ~= 'file' then - vim.fn.bufload(bufnr) - return buf_lines() - end - - local filename = api.nvim_buf_get_name(bufnr) - if vim.fn.isdirectory(filename) ~= 0 then - return {} - end - - -- get the data from the file - local fd = uv.fs_open(filename, 'r', 438) - if not fd then - return {} - end - local stat = assert(uv.fs_fstat(fd)) - local data = assert(uv.fs_read(fd, stat.size, 0)) - uv.fs_close(fd) - - local lines = {} --- @type table rows we need to retrieve - local need = 0 -- keep track of how many unique rows we need - for _, row in pairs(rows) do - if not lines[row] then - need = need + 1 - end - lines[row] = true - end - - local found = 0 - local lnum = 0 - - for line in string.gmatch(data, '([^\n]*)\n?') do - if lines[lnum] == true then - lines[lnum] = line - found = found + 1 - if found == need then - break - end - end - lnum = lnum + 1 - end - - -- change any lines we didn't find to the empty string - for i, line in pairs(lines) do - if line == true then - lines[i] = '' - end - end - return lines --[[@as table]] -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 +local get_line = vim.pos._get_line --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ---@param position lsp.Position diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua index 14e3f5138c..e5d0d4c84e 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -7,6 +7,7 @@ --- objects. local api = vim.api +local uv = vim.uv local validate = vim.validate --- Represents a well-defined position. @@ -116,11 +117,97 @@ function M.__eq(...) return cmp_pos(...) == 0 end ---- TODO(ofseed): Make it work for unloaded buffers. Check get_line() in vim.lsp.util. ----@param buf integer ----@param row integer -local function get_line(buf, row) - return api.nvim_buf_get_lines(buf, row, row + 1, true)[1] +--- Gets the zero-indexed lines from the given buffer. +--- Works on unloaded buffers by reading the file using libuv to bypass buf reading events. +--- Falls back to loading the buffer and nvim_buf_get_lines for buffers with non-file URI. +--- +---@param bufnr integer bufnr to get the lines from +---@param rows integer[] zero-indexed line numbers +---@return table # a table mapping rows to lines +local function get_lines(bufnr, rows) + --- @type integer[] + rows = type(rows) == 'table' and rows or { rows } + + -- This is needed for bufload and bufloaded + bufnr = vim._resolve_bufnr(bufnr) + + local function buf_lines() + local lines = {} --- @type table + for _, row in ipairs(rows) do + lines[row] = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or { '' })[1] + end + return lines + end + + -- use loaded buffers if available + if vim.fn.bufloaded(bufnr) == 1 then + return buf_lines() + end + + local uri = vim.uri_from_bufnr(bufnr) + + -- load the buffer if this is not a file uri + -- Custom language server protocol extensions can result in servers sending URIs with custom schemes. Plugins are able to load these via `BufReadCmd` autocmds. + if uri:sub(1, 4) ~= 'file' then + vim.fn.bufload(bufnr) + return buf_lines() + end + + local filename = api.nvim_buf_get_name(bufnr) + if vim.fn.isdirectory(filename) ~= 0 then + return {} + end + + -- get the data from the file + local fd = uv.fs_open(filename, 'r', 438) + if not fd then + return {} + end + local stat = assert(uv.fs_fstat(fd)) + local data = assert(uv.fs_read(fd, stat.size, 0)) + uv.fs_close(fd) + + local lines = {} --- @type table rows we need to retrieve + local need = 0 -- keep track of how many unique rows we need + for _, row in pairs(rows) do + if not lines[row] then + need = need + 1 + end + lines[row] = true + end + + local found = 0 + local lnum = 0 + + for line in string.gmatch(data, '([^\n]*)\n?') do + if lines[lnum] == true then + lines[lnum] = line + found = found + 1 + if found == need then + break + end + end + lnum = lnum + 1 + end + + -- change any lines we didn't find to the empty string + for i, line in pairs(lines) do + if line == true then + lines[i] = '' + end + end + return lines --[[@as table]] +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 --- Converts |vim.Pos| to `lsp.Position`. @@ -269,6 +356,11 @@ function M.offset(buf, offset) return M.new(buf, row, col) end +-- TODO(ofseed): remove these exported functions by replacing their usages with `vim.pos`. +M._get_lines = get_lines + +M._get_line = get_line + -- Overload `Range.new` to allow calling this module as a function. setmetatable(M, { __call = function(_, ...) From a7f09db9de683f0bb1f4bc5725e39be63666b89c Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Sat, 16 May 2026 23:44:32 +0800 Subject: [PATCH 3/3] refactor(lsp): remove some private utility functions Problem: `make_position_params`/`get_line_byte_from_position`/`make_line_range_params` are private functions and their functionality can be replaced by `vim.pos` now. Solution: Remove them, use `vim.pos` instead. --- runtime/lua/vim/lsp/_tagfunc.lua | 3 +- runtime/lua/vim/lsp/buf.lua | 46 +++++------ runtime/lua/vim/lsp/handlers.lua | 10 +-- runtime/lua/vim/lsp/inlay_hint.lua | 9 +-- runtime/lua/vim/lsp/util.lua | 119 +++++------------------------ 5 files changed, 48 insertions(+), 139 deletions(-) diff --git a/runtime/lua/vim/lsp/_tagfunc.lua b/runtime/lua/vim/lsp/_tagfunc.lua index a0791c78b1..a71c831199 100644 --- a/runtime/lua/vim/lsp/_tagfunc.lua +++ b/runtime/lua/vim/lsp/_tagfunc.lua @@ -10,7 +10,8 @@ local util = lsp.util local function mk_tag_item(name, range, uri, position_encoding) local bufnr = vim.uri_to_bufnr(uri) -- This is get_line_byte_from_position is 0-indexed, call cursor expects a 1-indexed position - local byte = util._get_line_byte_from_position(bufnr, range.start, position_encoding) + 1 + local pos = vim.pos.lsp(bufnr, range.start, position_encoding) + local byte = pos.col + 1 return { name = name, filename = vim.uri_to_fname(uri), diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 4861a1a517..af339a6244 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -38,17 +38,21 @@ local function ctx_is_valid(ctx) then return false end + ---@type lsp.Position? local p = ctx.params and ctx.params.position if not p then return true end - local cur = api.nvim_win_get_cursor(0) local c = lsp.get_client_by_id(ctx.client_id) local enc = c and c.offset_encoding + if not enc then + return false + end - return cur[1] - 1 == p.line and enc and cur[2] == util._get_line_byte_from_position(bufnr, p, enc) - or false + local cur_pos = vim.pos.cursor(bufnr, api.nvim_win_get_cursor(0)) + local pos = vim.pos.lsp(bufnr, p, enc) + return cur_pos == pos end --- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts @@ -162,19 +166,15 @@ function M.hover(config) else vim.list_extend(contents, util.convert_input_to_markdown_lines(result.contents)) end - local range = result.range - if range then - local start = range.start - local end_ = range['end'] - local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) - local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) + if result.range then + local range = vim.range.lsp(bufnr, result.range, client.offset_encoding) vim.hl.range( bufnr, hover_ns, 'LspReferenceTarget', - { start.line, start_idx }, - { end_.line, end_idx }, + { range.start_row, range.start_col }, + { range.end_row, range.end_col }, { priority = vim.hl.priorities.user } ) end @@ -739,12 +739,13 @@ function M.rename(new_name, opts) --- @param range lsp.Range --- @param position_encoding 'utf-8'|'utf-16'|'utf-32' local function get_text_at_range(range, position_encoding) + local vim_range = vim.range.lsp(bufnr, range, position_encoding) return api.nvim_buf_get_text( bufnr, - range.start.line, - util._get_line_byte_from_position(bufnr, range.start, position_encoding), - range['end'].line, - util._get_line_byte_from_position(bufnr, range['end'], position_encoding), + vim_range.start_row, + vim_range.start_col, + vim_range.end_row, + vim_range.end_col, {} )[1] end @@ -787,26 +788,21 @@ function M.rename(new_name, opts) return end - local range ---@type lsp.Range? + local range ---@type vim.Range? if result.start then ---@cast result lsp.Range - range = result + range = vim.range.lsp(bufnr, result, client.offset_encoding) elseif result.range then ---@cast result { range: lsp.Range, placeholder: string } - range = result.range + range = vim.range.lsp(bufnr, result.range, client.offset_encoding) end if range then - local start = range.start - local end_ = range['end'] - local start_idx = util._get_line_byte_from_position(bufnr, start, client.offset_encoding) - local end_idx = util._get_line_byte_from_position(bufnr, end_, client.offset_encoding) - vim.hl.range( bufnr, rename_ns, 'LspReferenceTarget', - { start.line, start_idx }, - { end_.line, end_idx }, + { range.start_row, range.start_col }, + { range.end_row, range.end_col }, { priority = vim.hl.priorities.user } ) end diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index f4765183af..1d0043dc8b 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -574,16 +574,12 @@ local function make_type_hierarchy_handler() local client = assert(vim.lsp.get_client_by_id(ctx.client_id)) local items = {} for _, type_hierarchy_item in pairs(result) do - local col = util._get_line_byte_from_position( - ctx.bufnr, - type_hierarchy_item.range.start, - client.offset_encoding - ) + local pos = vim.pos.lsp(ctx.bufnr, type_hierarchy_item.range.start, client.offset_encoding) table.insert(items, { filename = assert(vim.uri_to_fname(type_hierarchy_item.uri)), text = format_item(type_hierarchy_item), - lnum = type_hierarchy_item.range.start.line + 1, - col = col + 1, + lnum = pos.row + 1, + col = pos.col + 1, }) end vim.fn.setqflist({}, ' ', { title = 'LSP type hierarchy', items = items }) diff --git a/runtime/lua/vim/lsp/inlay_hint.lua b/runtime/lua/vim/lsp/inlay_hint.lua index e738f0c2bd..49d5f2c4c8 100644 --- a/runtime/lua/vim/lsp/inlay_hint.lua +++ b/runtime/lua/vim/lsp/inlay_hint.lua @@ -100,12 +100,9 @@ local function refresh(bufnr, client_id) do client:request('textDocument/inlayHint', { textDocument = util.make_text_document_params(bufnr), - range = util._make_line_range_params( - bufnr, - 0, - api.nvim_buf_line_count(bufnr) - 1, - client.offset_encoding - ), + range = vim + .range(bufnr, 0, 0, api.nvim_buf_line_count(bufnr), 0) + :to_lsp(client.offset_encoding), }, nil, bufnr) end end diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5d3538db6b..d846ce1d01 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -137,23 +137,6 @@ local get_lines = vim.pos._get_lines local get_line = vim.pos._get_line ---- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position ----@param position lsp.Position ----@param position_encoding 'utf-8'|'utf-16'|'utf-32' ----@return integer -local function get_line_byte_from_position(bufnr, position, position_encoding) - -- LSP's line and characters are 0-indexed - -- Vim's line and columns are 1-indexed - local col = position.character - -- When on the first character, we can ignore the difference between byte and - -- character - if col > 0 then - local line = get_line(bufnr, position.line) or '' - return vim.str_byteindex(line, position_encoding, col, false) - end - return col -end - --- Applies a list of text edits to a buffer. Note: this mutates `text_edits` (sorts in-place and --- adds `_index` fields). --- @@ -229,10 +212,9 @@ function M.apply_text_edits(text_edits, bufnr, position_encoding, change_annotat text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') -- Convert from LSP style ranges to Neovim style ranges. - local start_row = text_edit.range.start.line - local start_col = get_line_byte_from_position(bufnr, text_edit.range.start, position_encoding) - local end_row = text_edit.range['end'].line - local end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], position_encoding) + local range = vim.range.lsp(bufnr, text_edit.range, position_encoding) + local start_row, start_col, end_row, end_col = + range.start_row, range.start_col, range.end_row, range.end_col local text = vim.split(text_edit.newText, '\n', { plain = true }) local max = api.nvim_buf_line_count(bufnr) @@ -902,8 +884,8 @@ function M.show_document(location, position_encoding, opts) local range = location.range or location.targetSelectionRange if range then -- Jump to new location (adjusting for encoding of characters) - local row = range.start.line - local col = get_line_byte_from_position(bufnr, range.start, position_encoding) + local pos = vim.pos.lsp(bufnr, range.start, position_encoding) + local row, col = pos.row, pos.col api.nvim_win_set_cursor(win, { row + 1, col }) vim._with({ win = win }, function() -- Open folds under the cursor @@ -1675,12 +1657,7 @@ do --[[ References ]] validate('bufnr', bufnr, 'number', true) validate('position_encoding', position_encoding, 'string', false) for _, reference in ipairs(references) do - local range = reference.range - local start_line = range.start.line - local end_line = range['end'].line - - local start_idx = get_line_byte_from_position(bufnr, range.start, position_encoding) - local end_idx = get_line_byte_from_position(bufnr, range['end'], position_encoding) + local range = vim.range.lsp(bufnr, reference.range, position_encoding) local document_highlight_kind = { [protocol.DocumentHighlightKind.Text] = 'LspReferenceText', @@ -1692,8 +1669,8 @@ do --[[ References ]] bufnr, reference_ns, document_highlight_kind[kind], - { start_line, start_idx }, - { end_line, end_idx }, + { range.start_row, range.start_col }, + { range.end_row, range.end_col }, { priority = vim.hl.priorities.user } ) end @@ -1783,27 +1760,21 @@ function M.symbols_to_items(symbols, bufnr, position_encoding) local items = {} --- @type vim.quickfix.entry[] for _, symbol in ipairs(symbols) do - --- @type string?, lsp.Range? + --- @type string?, vim.Range? local filename, range if symbol.location then --- @cast symbol lsp.SymbolInformation filename = vim.uri_to_fname(symbol.location.uri) - range = symbol.location.range + range = vim.range.lsp(bufnr, symbol.location.range, position_encoding) elseif symbol.selectionRange then --- @cast symbol lsp.DocumentSymbol filename = api.nvim_buf_get_name(bufnr) - range = symbol.selectionRange + range = vim.range.lsp(bufnr, symbol.selectionRange, position_encoding) end if filename and range then local kind = protocol.SymbolKind[symbol.kind] or 'Unknown' - - local lnum = range['start'].line + 1 - local col = get_line_byte_from_position(bufnr, range['start'], position_encoding) + 1 - local end_lnum = range['end'].line + 1 - local end_col = get_line_byte_from_position(bufnr, range['end'], position_encoding) + 1 - local is_deprecated = not vim.isnil(symbol.deprecated or nil) or ( not vim.isnil(symbol.tags) @@ -1819,10 +1790,10 @@ function M.symbols_to_items(symbols, bufnr, position_encoding) items[#items + 1] = { filename = filename, - lnum = lnum, - col = col, - end_lnum = end_lnum, - end_col = end_col, + lnum = range.start_row + 1, + col = range.start_col + 1, + end_lnum = range.end_row + 1, + end_col = range.end_col + 1, kind = kind, text = text, } @@ -1836,23 +1807,6 @@ function M.symbols_to_items(symbols, bufnr, position_encoding) return items end ----@param win integer?: |window-ID| or 0 for current, defaults to current ----@param position_encoding 'utf-8'|'utf-16'|'utf-32' -local function make_position_param(win, position_encoding) - win = win or 0 - local buf = api.nvim_win_get_buf(win) - local row, col = unpack(api.nvim_win_get_cursor(win)) - row = row - 1 - local line = api.nvim_buf_get_lines(buf, row, row + 1, true)[1] - if not line then - return { line = 0, character = 0 } - end - - col = vim.str_utfindex(line, position_encoding, col, false) - - return { line = row, character = col } -end - --- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. --- ---@param win integer?: |window-ID| or 0 for current, defaults to current @@ -1864,7 +1818,7 @@ function M.make_position_params(win, position_encoding) local buf = api.nvim_win_get_buf(win) return { textDocument = M.make_text_document_params(buf), - position = make_position_param(win, position_encoding), + position = vim.pos.cursor(buf, api.nvim_win_get_cursor(win)):to_lsp(position_encoding), } end @@ -1877,8 +1831,9 @@ end ---@param position_encoding "utf-8"|"utf-16"|"utf-32" ---@return { textDocument: { uri: lsp.DocumentUri }, range: lsp.Range } function M.make_range_params(win, position_encoding) - local buf = api.nvim_win_get_buf(win or 0) - local position = make_position_param(win, position_encoding) + win = win or 0 + local buf = api.nvim_win_get_buf(win) + local position = vim.pos.cursor(buf, api.nvim_win_get_cursor(win)):to_lsp(position_encoding) return { textDocument = M.make_text_document_params(buf), range = { start = position, ['end'] = position }, @@ -1990,40 +1945,6 @@ function M.character_offset(buf, row, col, position_encoding) return vim.str_utfindex(line, position_encoding, col, false) end ---- Converts line range (0-based, end-inclusive) to lsp range, ---- handles absence of a trailing newline ---- ----@param bufnr integer ----@param start_line integer ----@param end_line integer ----@param position_encoding 'utf-8'|'utf-16'|'utf-32' ----@return lsp.Range -function M._make_line_range_params(bufnr, start_line, end_line, position_encoding) - local last_line = api.nvim_buf_line_count(bufnr) - 1 - - ---@type lsp.Position - local end_pos - - if end_line == last_line and not vim.bo[bufnr].endofline then - end_pos = { - line = end_line, - character = M.character_offset( - bufnr, - end_line, - #get_line(bufnr, end_line), - position_encoding - ), - } - else - end_pos = { line = end_line + 1, character = 0 } - end - - return { - start = { line = start_line, character = 0 }, - ['end'] = end_pos, - } -end - ---@class (private) vim.lsp.util._cancel_requests.Filter ---@field bufnr? integer ---@field clients? vim.lsp.Client[] @@ -2058,8 +1979,6 @@ function M._cancel_requests(filter) end end -M._get_line_byte_from_position = get_line_byte_from_position - ---@nodoc ---@type table M.buf_versions = setmetatable({}, {