neovim

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

pos.lua (5491B)


      1 ---@brief
      2 ---
      3 --- EXPERIMENTAL: This API may change in the future. Its semantics are not yet finalized.
      4 --- Subscribe to https://github.com/neovim/neovim/issues/25509
      5 --- to stay updated or contribute to its development.
      6 ---
      7 --- Provides operations to compare, calculate, and convert positions represented by |vim.Pos|
      8 --- objects.
      9 
     10 local api = vim.api
     11 local validate = vim.validate
     12 
     13 --- Represents a well-defined position.
     14 ---
     15 --- A |vim.Pos| object contains the {row} and {col} coordinates of a position.
     16 --- To create a new |vim.Pos| object, call `vim.pos()`.
     17 ---
     18 --- Example:
     19 --- ```lua
     20 --- local pos1 = vim.pos(3, 5)
     21 --- local pos2 = vim.pos(4, 0)
     22 ---
     23 --- -- Operators are overloaded for comparing two `vim.Pos` objects.
     24 --- if pos1 < pos2 then
     25 ---   print("pos1 comes before pos2")
     26 --- end
     27 ---
     28 --- if pos1 ~= pos2 then
     29 ---   print("pos1 and pos2 are different positions")
     30 --- end
     31 --- ```
     32 ---
     33 --- It may include optional fields that enable additional capabilities,
     34 --- such as format conversions.
     35 ---
     36 ---@class vim.Pos
     37 ---@field row integer 0-based byte index.
     38 ---@field col integer 0-based byte index.
     39 ---
     40 --- Optional buffer handle.
     41 ---
     42 --- When specified, it indicates that this position belongs to a specific buffer.
     43 --- This field is required when performing position conversions.
     44 ---@field buf? integer
     45 local Pos = {}
     46 Pos.__index = Pos
     47 
     48 ---@class vim.Pos.Optional
     49 ---@inlinedoc
     50 ---@field buf? integer
     51 
     52 ---@package
     53 ---@param row integer
     54 ---@param col integer
     55 ---@param opts? vim.Pos.Optional
     56 function Pos.new(row, col, opts)
     57  validate('row', row, 'number')
     58  validate('col', col, 'number')
     59  validate('opts', opts, 'table', true)
     60 
     61  opts = opts or {}
     62 
     63  ---@type vim.Pos
     64  local self = setmetatable({
     65    row = row,
     66    col = col,
     67    buf = opts.buf,
     68  }, Pos)
     69 
     70  return self
     71 end
     72 
     73 ---@param p1 vim.Pos First position to compare.
     74 ---@param p2 vim.Pos Second position to compare.
     75 ---@return integer
     76 --- 1: a > b
     77 --- 0: a == b
     78 --- -1: a < b
     79 local function cmp_pos(p1, p2)
     80  if p1.row == p2.row then
     81    if p1.col > p2.col then
     82      return 1
     83    elseif p1.col < p2.col then
     84      return -1
     85    else
     86      return 0
     87    end
     88  elseif p1.row > p2.row then
     89    return 1
     90  end
     91 
     92  return -1
     93 end
     94 
     95 ---@private
     96 function Pos.__lt(...)
     97  return cmp_pos(...) == -1
     98 end
     99 
    100 ---@private
    101 function Pos.__le(...)
    102  return cmp_pos(...) ~= 1
    103 end
    104 
    105 ---@private
    106 function Pos.__eq(...)
    107  return cmp_pos(...) == 0
    108 end
    109 
    110 --- TODO(ofseed): Make it work for unloaded buffers. Check get_line() in vim.lsp.util.
    111 ---@param buf integer
    112 ---@param row integer
    113 local function get_line(buf, row)
    114  return api.nvim_buf_get_lines(buf, row, row + 1, true)[1]
    115 end
    116 
    117 --- Converts |vim.Pos| to `lsp.Position`.
    118 ---
    119 --- Example:
    120 --- ```lua
    121 --- -- `buf` is required for conversion to LSP position.
    122 --- local buf = vim.api.nvim_get_current_buf()
    123 --- local pos = vim.pos(3, 5, { buf = buf })
    124 ---
    125 --- -- Convert to LSP position, you can call it in a method style.
    126 --- local lsp_pos = pos:lsp('utf-16')
    127 --- ```
    128 ---@param pos vim.Pos
    129 ---@param position_encoding lsp.PositionEncodingKind
    130 function Pos.to_lsp(pos, position_encoding)
    131  validate('pos', pos, 'table')
    132  validate('position_encoding', position_encoding, 'string')
    133 
    134  local buf = assert(pos.buf, 'position is not a buffer position')
    135  local row, col = pos.row, pos.col
    136  -- When on the first character,
    137  -- we can ignore the difference between byte and character.
    138  if col > 0 then
    139    col = vim.str_utfindex(get_line(buf, row), position_encoding, col, false)
    140  end
    141 
    142  ---@type lsp.Position
    143  return { line = row, character = col }
    144 end
    145 
    146 --- Creates a new |vim.Pos| from `lsp.Position`.
    147 ---
    148 --- Example:
    149 --- ```lua
    150 --- local buf = vim.api.nvim_get_current_buf()
    151 --- local lsp_pos = {
    152 ---   line = 3,
    153 ---   character = 5
    154 --- }
    155 ---
    156 --- -- `buf` is mandatory, as LSP positions are always associated with a buffer.
    157 --- local pos = vim.pos.lsp(buf, lsp_pos, 'utf-16')
    158 --- ```
    159 ---@param buf integer
    160 ---@param pos lsp.Position
    161 ---@param position_encoding lsp.PositionEncodingKind
    162 function Pos.lsp(buf, pos, position_encoding)
    163  validate('buf', buf, 'number')
    164  validate('pos', pos, 'table')
    165  validate('position_encoding', position_encoding, 'string')
    166 
    167  local row, col = pos.line, pos.character
    168  -- When on the first character,
    169  -- we can ignore the difference between byte and character.
    170  if col > 0 then
    171    -- `strict_indexing` is disabled, because LSP responses are asynchronous,
    172    -- and the buffer content may have changed, causing out-of-bounds errors.
    173    col = vim.str_byteindex(get_line(buf, row), position_encoding, col, false)
    174  end
    175 
    176  return Pos.new(row, col, { buf = buf })
    177 end
    178 
    179 --- Converts |vim.Pos| to cursor position.
    180 ---@param pos vim.Pos
    181 ---@return [integer, integer]
    182 function Pos.to_cursor(pos)
    183  return { pos.row + 1, pos.col }
    184 end
    185 
    186 --- Creates a new |vim.Pos| from cursor position.
    187 ---@param pos [integer, integer]
    188 function Pos.cursor(pos)
    189  return Pos.new(pos[1] - 1, pos[2])
    190 end
    191 
    192 --- Converts |vim.Pos| to extmark position.
    193 ---@param pos vim.Pos
    194 ---@return [integer, integer]
    195 function Pos.to_extmark(pos)
    196  return { pos.row, pos.col }
    197 end
    198 
    199 --- Creates a new |vim.Pos| from extmark position.
    200 ---@param pos [integer, integer]
    201 function Pos.extmark(pos)
    202  local row, col = unpack(pos)
    203  return Pos.new(row, col)
    204 end
    205 
    206 -- Overload `Range.new` to allow calling this module as a function.
    207 setmetatable(Pos, {
    208  __call = function(_, ...)
    209    return Pos.new(...)
    210  end,
    211 })
    212 ---@cast Pos +fun(row: integer, col: integer, opts: vim.Pos.Optional?): vim.Pos
    213 
    214 return Pos