feat(pos): pos:to_offset(), pos.offset() (#39639)

Problem:
For a given position, it is not easy to compare which of several other positions is closest to it.

Solution:
Add support for converting `vim.Pos` to a buffer byte offset.

This allows for sorting, e.g:
```lua
table.sort(positions, function(pos1, pos2)
  return pos1:to_offset() < pos2:to_offset()
end
```

Or a binary search, e.g:
```lua
vim.list.bisect(positions, pos, { key = function(pos) return pos:to_offset() end })
```

Co-authored-by: Yi Ming <ofseed@foxmail.com>
This commit is contained in:
Justin M. Keyes
2026-05-07 05:16:53 -04:00
committed by GitHub
parent 0197461265
commit ad27075c8d
4 changed files with 85 additions and 2 deletions

View File

@@ -4324,6 +4324,16 @@ lsp({buf}, {pos}, {position_encoding}) *vim.pos.lsp()*
• {pos} (`lsp.Position`)
• {position_encoding} (`lsp.PositionEncodingKind`)
offset({buf}, {offset}) *vim.pos.offset()*
Creates a new |vim.Pos| from buffer offset.
Parameters: ~
• {buf} (`integer`)
• {offset} (`integer`)
Return: ~
(`vim.Pos`) See |vim.Pos|.
to_cursor({pos}) *vim.pos.to_cursor()*
Converts |vim.Pos| to cursor position (see |api-indexing|).
@@ -4358,6 +4368,15 @@ to_lsp({pos}, {position_encoding}) *vim.pos.to_lsp()*
• {pos} (`vim.Pos`) See |vim.Pos|.
• {position_encoding} (`lsp.PositionEncodingKind`)
to_offset({pos}) *vim.pos.to_offset()*
Converts |vim.Pos| to buffer offset.
Parameters: ~
• {pos} (`vim.Pos`) See |vim.Pos|.
Return: ~
(`integer`)
==============================================================================
Lua module: vim.range *vim.range*

View File

@@ -309,8 +309,8 @@ LUA
• |vim.filetype.inspect()| returns a copy of the internal tables used for
filetype detection.
• Added `__eq` metamethod to |vim.VersionRange|. 2 distinct but representing
the same range instances now compare equal.
the same range instances now compare equal.
• |vim.pos| can now convert between positions and buffer offsets.
OPTIONS

View File

@@ -225,6 +225,33 @@ function M.extmark(buf, row, col)
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
-- Overload `Range.new` to allow calling this module as a function.
setmetatable(M, {
__call = function(_, ...)

View File

@@ -2,6 +2,7 @@
local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local eq = t.eq
local dedent = t.dedent
local clear = n.clear
local exec_lua = n.exec_lua
@@ -120,4 +121,40 @@ describe('vim.pos', function()
end)
eq({ 0, 9, buf }, pos2)
end)
it('converts between vim.Pos and buffer offset', function()
local buf = exec_lua(function()
return vim.api.nvim_get_current_buf()
end)
insert(dedent [[
first
second
third
]])
local offsets = exec_lua(function()
return {
vim.pos(buf, 0, 0):to_offset(),
vim.pos(buf, 0, 3):to_offset(),
vim.pos(buf, 1, 0):to_offset(),
vim.pos(buf, 3, 0):to_offset(),
}
end)
eq({ 0, 3, 6, 19 }, offsets)
local positions = exec_lua(function()
return {
vim.pos.offset(buf, 0),
vim.pos.offset(buf, 3),
vim.pos.offset(buf, 6),
vim.pos.offset(buf, 19),
}
end)
eq({
{ 0, 0, buf },
{ 0, 3, buf },
{ 1, 0, buf },
{ 3, 0, buf },
}, positions)
end)
end)