---@brief --- --- EXPERIMENTAL: This API is unstable, do not use it. Its semantics are not yet finalized. --- Subscribe to this issue to stay updated: https://github.com/neovim/neovim/issues/25509 --- --- Provides operations to compare, calculate, and convert positions represented by |vim.Pos| --- objects. local api = vim.api local uv = vim.uv local validate = vim.validate --- Represents a well-defined position. --- --- A |vim.Pos| object contains the {row} and {col} coordinates of a position. --- To create a new |vim.Pos| object, call `vim.pos()`. --- --- Example: --- ```lua --- local pos1 = vim.pos(0, 3, 5) --- local pos2 = vim.pos(0, 4, 0) --- --- -- Operators are overloaded for comparing two `vim.Pos` objects. --- if pos1 < pos2 then --- print("pos1 comes before pos2") --- end --- --- if pos1 ~= pos2 then --- print("pos1 and pos2 are different positions") --- end --- ``` --- --- It may include optional fields that enable additional capabilities, --- such as format conversions. --- ---@class vim.Pos ---@field row integer 0-based byte index. ---@field col integer 0-based byte index. ---@field buf integer buffer handle. ---@field private [1] integer underlying representation of row ---@field private [2] integer underlying representation of col ---@field private [3] integer underlying representation of buf local M = {} ---@private ---@param pos vim.Pos ---@param key any function M.__index(pos, key) if key == 'row' then return pos[1] elseif key == 'col' then return pos[2] elseif key == 'buf' then return pos[3] end return M[key] end ---@package ---@param buf integer ---@param row integer ---@param col integer function M.new(buf, row, col) validate('buf', buf, 'number') validate('row', row, 'number') validate('col', col, 'number') if buf == 0 then buf = api.nvim_get_current_buf() end ---@type vim.Pos local self = setmetatable({ row, col, buf, }, M) return self end ---@param p1 vim.Pos First position to compare. ---@param p2 vim.Pos Second position to compare. ---@return integer --- 1: a > b --- 0: a == b --- -1: a < b local function cmp_pos(p1, p2) if p1[1] == p2[1] then if p1[2] > p2[2] then return 1 elseif p1[2] < p2[2] then return -1 else return 0 end elseif p1[1] > p2[1] then return 1 end return -1 end ---@private function M.__lt(...) return cmp_pos(...) == -1 end ---@private function M.__le(...) return cmp_pos(...) ~= 1 end ---@private function M.__eq(...) return cmp_pos(...) == 0 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 } -- 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`. --- --- Example: --- ```lua --- local pos = vim.pos(0, 3, 5) --- --- -- Convert to LSP position, you can call it in a method style. --- local lsp_pos = pos:to_lsp('utf-16') --- ``` ---@param pos vim.Pos ---@param position_encoding lsp.PositionEncodingKind function M.to_lsp(pos, position_encoding) validate('pos', pos, 'table') validate('position_encoding', position_encoding, 'string') local buf, row, col = pos.buf, pos[1], pos[2] -- When on the first character, -- 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 return { line = row, character = col } end --- Creates a new |vim.Pos| from `lsp.Position`. --- --- Example: --- ```lua --- local lsp_pos = { --- line = 3, --- character = 5 --- } --- --- local pos = vim.pos.lsp(0, lsp_pos, 'utf-16') --- ``` ---@param buf integer ---@param pos lsp.Position ---@param position_encoding lsp.PositionEncodingKind function M.lsp(buf, pos, position_encoding) validate('buf', buf, 'number') validate('pos', pos, 'table') validate('position_encoding', position_encoding, 'string') if buf == 0 then buf = api.nvim_get_current_buf() end local row, col = pos.line, pos.character -- When on the first character, -- we can ignore the difference between byte and character. 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) or '', position_encoding, col, false) end return M.new(buf, row, col) end --- Converts |vim.Pos| to cursor position (see |api-indexing|). --- --- Example: --- ```lua --- local pos = vim.pos(0, 3, 5) --- --- -- Convert to cursor position, you can call it in a method style. --- local cursor_pos = { pos:to_cursor() } --- vim.api.nvim_win_set_cursor(0, cursor_pos) --- ``` ---@param pos vim.Pos ---@return integer, integer function M.to_cursor(pos) return pos[1] + 1, pos[2] end --- Creates a new |vim.Pos| from cursor position (see |api-indexing|). ---@param buf integer ---@param pos [integer, integer] function M.cursor(buf, pos) return M.new(buf, pos[1] - 1, pos[2]) end --- Converts |vim.Pos| to mark position (see |api-indexing|). ---@param pos vim.Pos ---@return integer, integer function M.to_mark(pos) return pos[1] + 1, pos[2] end --- Creates a new |vim.Pos| from mark position (see |api-indexing|). ---@param buf integer ---@param row integer ---@param col integer function M.mark(buf, row, col) if buf == 0 then buf = api.nvim_get_current_buf() end return M.new(buf, row - 1, col) end --- Converts |vim.Pos| to extmark position (see |api-indexing|). ---@param pos vim.Pos ---@return integer, integer function M.to_extmark(pos) local row, col = pos[1], pos[2] -- 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 return row, col end --- Creates a new |vim.Pos| from extmark position (see |api-indexing|). ---@param buf integer ---@param row integer ---@param col integer function M.extmark(buf, row, col) if buf == 0 then buf = api.nvim_get_current_buf() end return M.new(buf, row, col) end --- Converts |vim.Pos| to buffer offset. ---@param pos vim.Pos ---@return integer function M.to_offset(pos) return api.nvim_buf_get_offset(pos.buf, pos[1]) + pos[2] end --- Creates a new |vim.Pos| from buffer offset. ---@param buf integer ---@param offset integer ---@return vim.Pos function M.offset(buf, offset) local lnum = vim.list.bisect( setmetatable({}, { __index = function(_, lnum) return api.nvim_buf_get_offset(buf, lnum - 1) end, }), offset, { lo = 1, hi = api.nvim_buf_line_count(buf) + 2, bound = 'upper' } ) - 1 local row = lnum - 1 local col = offset - api.nvim_buf_get_offset(buf, row) 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(_, ...) return M.new(...) end, }) ---@cast M +fun(buf: integer, row: integer, col: integer): vim.Pos return M