aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/pos.lua
blob: cb793053a277f2acde4a1db172b4c05716cc1a07 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
---@brief
---
--- EXPERIMENTAL: This API may change in the future. Its semantics are not yet finalized.
--- Subscribe to https://github.com/neovim/neovim/issues/25509
--- to stay updated or contribute to its development.
---
--- Provides operations to compare, calculate, and convert positions represented by |vim.Pos|
--- objects.

local api = vim.api
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(3, 5)
--- local pos2 = vim.pos(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.
---
--- Optional buffer handle.
---
--- When specified, it indicates that this position belongs to a specific buffer.
--- This field is required when performing position conversions.
---@field buf? integer
local Pos = {}
Pos.__index = Pos

---@class vim.Pos.Optional
---@inlinedoc
---@field buf? integer

---@package
---@param row integer
---@param col integer
---@param opts? vim.Pos.Optional
function Pos.new(row, col, opts)
  validate('row', row, 'number')
  validate('col', col, 'number')
  validate('opts', opts, 'table', true)

  opts = opts or {}

  ---@type vim.Pos
  local self = setmetatable({
    row = row,
    col = col,
    buf = opts.buf,
  }, Pos)

  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.row == p2.row then
    if p1.col > p2.col then
      return 1
    elseif p1.col < p2.col then
      return -1
    else
      return 0
    end
  elseif p1.row > p2.row then
    return 1
  end

  return -1
end

---@private
function Pos.__lt(...)
  return cmp_pos(...) == -1
end

---@private
function Pos.__le(...)
  return cmp_pos(...) ~= 1
end

---@private
function Pos.__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]
end

--- Converts |vim.Pos| to `lsp.Position`.
---
--- Example:
--- ```lua
--- -- `buf` is required for conversion to LSP position.
--- local buf = vim.api.nvim_get_current_buf()
--- local pos = vim.pos(3, 5, { buf = buf })
---
--- -- Convert to LSP position, you can call it in a method style.
--- local lsp_pos = pos:lsp('utf-16')
--- ```
---@param pos vim.Pos
---@param position_encoding lsp.PositionEncodingKind
function Pos.to_lsp(pos, position_encoding)
  validate('pos', pos, 'table')
  validate('position_encoding', position_encoding, 'string')

  local buf = assert(pos.buf, 'position is not a buffer position')
  local row, col = pos.row, pos.col
  -- 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)
  end

  ---@type lsp.Position
  return { line = row, character = col }
end

--- Creates a new |vim.Pos| from `lsp.Position`.
---
--- Example:
--- ```lua
--- local buf = vim.api.nvim_get_current_buf()
--- local lsp_pos = {
---   line = 3,
---   character = 5
--- }
---
--- -- `buf` is mandatory, as LSP positions are always associated with a buffer.
--- local pos = vim.pos.lsp(buf, lsp_pos, 'utf-16')
--- ```
---@param buf integer
---@param pos lsp.Position
---@param position_encoding lsp.PositionEncodingKind
function Pos.lsp(buf, pos, position_encoding)
  validate('buf', buf, 'number')
  validate('pos', pos, 'table')
  validate('position_encoding', position_encoding, 'string')

  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), position_encoding, col, false)
  end

  return Pos.new(row, col, { buf = buf })
end

--- Converts |vim.Pos| to cursor position.
---@param pos vim.Pos
---@return [integer, integer]
function Pos.to_cursor(pos)
  return { pos.row + 1, pos.col }
end

--- Creates a new |vim.Pos| from cursor position.
---@param pos [integer, integer]
function Pos.cursor(pos)
  return Pos.new(pos[1] - 1, pos[2])
end

--- Converts |vim.Pos| to extmark position.
---@param pos vim.Pos
---@return [integer, integer]
function Pos.to_extmark(pos)
  return { pos.row, pos.col }
end

--- Creates a new |vim.Pos| from extmark position.
---@param pos [integer, integer]
function Pos.extmark(pos)
  local row, col = unpack(pos)
  return Pos.new(row, col)
end

-- Overload `Range.new` to allow calling this module as a function.
setmetatable(Pos, {
  __call = function(_, ...)
    return Pos.new(...)
  end,
})
---@cast Pos +fun(row: integer, col: integer, opts: vim.Pos.Optional?): vim.Pos

return Pos