neovim

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

range.lua (6249B)


      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 ranges represented by |vim.Range|
      8 --- objects.
      9 
     10 local validate = vim.validate
     11 
     12 --- Represents a well-defined range.
     13 ---
     14 --- A |vim.Range| object contains a {start} and a {end_} position(see |vim.Pos|).
     15 --- Note that the {end_} position is exclusive.
     16 --- To create a new |vim.Range| object, call `vim.range()`.
     17 ---
     18 --- Example:
     19 --- ```lua
     20 --- local pos1 = vim.pos(3, 5)
     21 --- local pos2 = vim.pos(4, 0)
     22 ---
     23 --- -- Create a range from two positions.
     24 --- local range1 = vim.range(pos1, pos2)
     25 --- -- Or create a range from four integers representing start and end positions.
     26 --- local range2 = vim.range(3, 5, 4, 0)
     27 ---
     28 --- -- Because `vim.Range` is end exclusive, `range1` and `range2` both represent
     29 --- -- a range starting at the row 3, column 5 and ending at where the row 3 ends.
     30 ---
     31 --- -- Operators are overloaded for comparing two `vim.Pos` objects.
     32 --- if range1 == range2 then
     33 ---   print("range1 and range2 are the same range")
     34 --- end
     35 --- ```
     36 ---
     37 --- It may include optional fields that enable additional capabilities,
     38 --- such as format conversions. Note that the {start} and {end_} positions
     39 --- need to have the same optional fields.
     40 ---
     41 ---@class vim.Range
     42 ---@field start vim.Pos Start position.
     43 ---@field end_ vim.Pos End position, exclusive.
     44 local Range = {}
     45 Range.__index = Range
     46 
     47 ---@package
     48 ---@overload fun(self: vim.Range, start: vim.Pos, end_: vim.Pos): vim.Range
     49 ---@overload fun(self: vim.Range, start_row: integer, start_col: integer, end_row: integer, end_col: integer, opts?: vim.Pos.Optional): vim.Range
     50 function Range.new(...)
     51  ---@type vim.Pos, vim.Pos, vim.Pos.Optional
     52  local start, end_
     53 
     54  local nargs = select('#', ...)
     55  if nargs == 2 then
     56    ---@type vim.Pos, vim.Pos
     57    start, end_ = ...
     58    validate('start', start, 'table')
     59    validate('end_', end_, 'table')
     60 
     61    if start.buf ~= end_.buf then
     62      error('start and end positions must belong to the same buffer')
     63    end
     64  elseif nargs == 4 or nargs == 5 then
     65    ---@type integer, integer, integer, integer, vim.Pos.Optional
     66    local start_row, start_col, end_row, end_col, opts = ...
     67    start, end_ = vim.pos(start_row, start_col, opts), vim.pos(end_row, end_col, opts)
     68  else
     69    error('invalid parameters')
     70  end
     71 
     72  ---@type vim.Range
     73  local self = setmetatable({
     74    start = start,
     75    end_ = end_,
     76  }, Range)
     77 
     78  return self
     79 end
     80 
     81 ---@private
     82 ---@param r1 vim.Range
     83 ---@param r2 vim.Range
     84 function Range.__lt(r1, r2)
     85  return r1.end_ < r2.start
     86 end
     87 
     88 ---@private
     89 ---@param r1 vim.Range
     90 ---@param r2 vim.Range
     91 function Range.__le(r1, r2)
     92  return r1.end_ <= r2.start
     93 end
     94 
     95 ---@private
     96 ---@param r1 vim.Range
     97 ---@param r2 vim.Range
     98 function Range.__eq(r1, r2)
     99  return r1.start == r2.start and r1.end_ == r2.end_
    100 end
    101 
    102 --- Checks whether the given range is empty; i.e., start >= end.
    103 ---
    104 ---@return boolean `true` if the given range is empty
    105 function Range:is_empty()
    106  return self.start >= self.end_
    107 end
    108 
    109 --- Checks whether {outer} range contains {inner} range or position.
    110 ---
    111 ---@param outer vim.Range
    112 ---@param inner vim.Range|vim.Pos
    113 ---@return boolean `true` if {outer} range fully contains {inner} range or position.
    114 function Range.has(outer, inner)
    115  if inner.start then
    116    -- inner is a range
    117    return outer.start <= inner.start and outer.end_ >= inner.end_
    118  else
    119    -- inner is a position
    120    return outer.start <= inner and outer.end_ >= inner
    121  end
    122 end
    123 
    124 --- Computes the common range shared by the given ranges.
    125 ---
    126 ---@param r1 vim.Range First range to intersect.
    127 ---@param r2 vim.Range Second range to intersect
    128 ---@return vim.Range? range that is present inside both `r1` and `r2`.
    129 ---                   `nil` if such range does not exist.
    130 function Range.intersect(r1, r2)
    131  if r1.end_ <= r2.start or r1.start >= r2.end_ then
    132    return nil
    133  end
    134  local rs = r1.start <= r2.start and r2 or r1
    135  local re = r1.end_ >= r2.end_ and r2 or r1
    136  return Range.new(rs.start, re.end_)
    137 end
    138 
    139 --- Converts |vim.Range| to `lsp.Range`.
    140 ---
    141 --- Example:
    142 --- ```lua
    143 --- -- `buf` is required for conversion to LSP range.
    144 --- local buf = vim.api.nvim_get_current_buf()
    145 --- local range = vim.range(3, 5, 4, 0, { buf = buf })
    146 ---
    147 --- -- Convert to LSP range, you can call it in a method style.
    148 --- local lsp_range = range:to_lsp('utf-16')
    149 --- ```
    150 ---@param range vim.Range
    151 ---@param position_encoding lsp.PositionEncodingKind
    152 ---@return lsp.Range
    153 function Range.to_lsp(range, position_encoding)
    154  validate('range', range, 'table')
    155  validate('position_encoding', position_encoding, 'string', true)
    156 
    157  ---@type lsp.Range
    158  return {
    159    ['start'] = range.start:to_lsp(position_encoding),
    160    ['end'] = range.end_:to_lsp(position_encoding),
    161  }
    162 end
    163 
    164 --- Creates a new |vim.Range| from `lsp.Range`.
    165 ---
    166 --- Example:
    167 --- ```lua
    168 --- local buf = vim.api.nvim_get_current_buf()
    169 --- local lsp_range = {
    170 ---   ['start'] = { line = 3, character = 5 },
    171 ---   ['end'] = { line = 4, character = 0 }
    172 --- }
    173 ---
    174 --- -- `buf` is mandatory, as LSP ranges are always associated with a buffer.
    175 --- local range = vim.range.lsp(buf, lsp_range, 'utf-16')
    176 --- ```
    177 ---@param buf integer
    178 ---@param range lsp.Range
    179 ---@param position_encoding lsp.PositionEncodingKind
    180 function Range.lsp(buf, range, position_encoding)
    181  validate('buf', buf, 'number')
    182  validate('range', range, 'table')
    183  validate('position_encoding', position_encoding, 'string')
    184 
    185  -- TODO(ofseed): avoid using `Pos:lsp()` here,
    186  -- as they need reading files separately if buffer is unloaded.
    187  local start = vim.pos.lsp(buf, range['start'], position_encoding)
    188  local end_ = vim.pos.lsp(buf, range['end'], position_encoding)
    189 
    190  return Range.new(start, end_)
    191 end
    192 
    193 -- Overload `Range.new` to allow calling this module as a function.
    194 setmetatable(Range, {
    195  __call = function(_, ...)
    196    return Range.new(...)
    197  end,
    198 })
    199 ---@cast Range +fun(start: vim.Pos, end_: vim.Pos): vim.Range
    200 ---@cast Range +fun(start_row: integer, start_col: integer, end_row: integer, end_col: integer, opts?: vim.Pos.Optional): vim.Range
    201 
    202 return Range