diff options
| author | Justin M. Keyes <justinkz@gmail.com> | 2026-05-20 05:35:09 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-20 05:35:09 -0400 |
| commit | 1f75fef951562562985e40ff8fa9ccfc022214fd (patch) | |
| tree | 057b5c3203e02eb9ccd57e720df32273c5848352 /runtime/lua/vim | |
| parent | f1fad32e2ea4e5d06994d123603b565aa9c03a32 (diff) | |
| parent | cd3b544611aa2181b04c77ffd36f470a46c61879 (diff) | |
| download | rneovim-1f75fef951562562985e40ff8fa9ccfc022214fd.tar.gz rneovim-1f75fef951562562985e40ff8fa9ccfc022214fd.tar.bz2 rneovim-1f75fef951562562985e40ff8fa9ccfc022214fd.zip | |
Merge #39889 from ofseed/pos-util
refactor(pos,range): drop `to_cursor`, extract `vim.pos._util`
Diffstat (limited to 'runtime/lua/vim')
| -rw-r--r-- | runtime/lua/vim/lsp/inline_completion.lua | 2 | ||||
| -rw-r--r-- | runtime/lua/vim/lsp/util.lua | 6 | ||||
| -rw-r--r-- | runtime/lua/vim/pos.lua | 305 | ||||
| -rw-r--r-- | runtime/lua/vim/pos/_util.lua | 205 | ||||
| -rw-r--r-- | runtime/lua/vim/range.lua | 199 | ||||
| -rw-r--r-- | runtime/lua/vim/treesitter/_range.lua | 49 |
6 files changed, 407 insertions, 359 deletions
diff --git a/runtime/lua/vim/lsp/inline_completion.lua b/runtime/lua/vim/lsp/inline_completion.lua index e759562747..7dae0c717b 100644 --- a/runtime/lua/vim/lsp/inline_completion.lua +++ b/runtime/lua/vim/lsp/inline_completion.lua @@ -348,7 +348,7 @@ function Completor:accept(item) api.nvim_buf_set_text(self.bufnr, start_row, start_col, end_row, end_col, lines) local win = api.nvim_get_current_win() win = api.nvim_win_get_buf(win) == self.bufnr and win or vim.fn.bufwinid(self.bufnr) - local row, col = item.range:to_cursor() + local row, col = item.range:to_mark() api.nvim_win_set_cursor(win, { row + #lines - 1, (#lines == 1 and col or 0) + #lines[#lines], diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 7a37d5ebfa..1687037e6b 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -133,9 +133,9 @@ local function sort_by_key(fn) end end -local get_lines = vim.pos._get_lines - -local get_line = vim.pos._get_line +-- TODO(ofseed): remove these exported functions by replacing their usages with `vim.pos`. +local get_lines = require('vim.pos._util').get_lines +local get_line = require('vim.pos._util').get_line --- Applies a list of text edits to a buffer. Note: this mutates `text_edits` (sorts in-place and --- adds `_index` fields). diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua index a37353f736..15cc318ddd 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -7,8 +7,8 @@ --- objects. local api = vim.api -local uv = vim.uv local validate = vim.validate +local util = require('vim.pos._util') --- Represents a well-defined position. --- @@ -80,134 +80,25 @@ function M.new(buf, row, col) 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 +---@param p1 vim.Pos +---@param p2 vim.Pos +function M.__lt(p1, p2) + return util.cmp_pos.lt(p1[1], p1[2], p2[1], p2[2]) end ---@private -function M.__le(...) - return cmp_pos(...) ~= 1 +---@param p1 vim.Pos +---@param p2 vim.Pos +function M.__le(p1, p2) + return util.cmp_pos.le(p1[1], p1[2], p2[1], p2[2]) 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<integer, string> # 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<integer,string> - 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<integer,true|string> 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<integer,string>]] -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] +---@param p1 vim.Pos +---@param p2 vim.Pos +function M.__eq(p1, p2) + return util.cmp_pos.eq(p1[1], p1[2], p2[1], p2[2]) end --- Converts |vim.Pos| to `lsp.Position`. @@ -221,25 +112,12 @@ end --- ``` ---@param pos vim.Pos ---@param position_encoding lsp.PositionEncodingKind +---@return lsp.Position 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 } + return util.to_lsp(pos.buf, pos[1], pos[2], position_encoding) end --- Creates a new |vim.Pos| from `lsp.Position`. @@ -256,6 +134,7 @@ end ---@param buf integer ---@param pos lsp.Position ---@param position_encoding lsp.PositionEncodingKind +---@return vim.Pos function M.lsp(buf, pos, position_encoding) validate('buf', buf, 'number') validate('pos', pos, 'table') @@ -265,82 +144,125 @@ function M.lsp(buf, pos, position_encoding) 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 - + local row, col = util.from_lsp(buf, pos, position_encoding) 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 +---@return integer lnum, integer col function M.to_cursor(pos) - return pos[1] + 1, pos[2] + validate('pos', pos, 'table') + return util.to_mark(pos[1], pos[2]) end --- Creates a new |vim.Pos| from cursor position (see |api-indexing|). +--- +--- Example: +--- ```lua +--- local cursor_pos = vim.api.nvim_win_get_cursor(0) +--- local pos = vim.pos.lsp(0, cursor_pos) +--- ``` ---@param buf integer ----@param pos [integer, integer] +---@param pos [integer, integer] (lnum, col) tuple +---@return vim.Pos function M.cursor(buf, pos) - return M.new(buf, pos[1] - 1, pos[2]) + validate('buf', buf, 'number') + validate('pos', pos, 'table') + + if buf == 0 then + buf = api.nvim_get_current_buf() + end + + return M.new(buf, util.from_mark(pos[1], pos[2])) end --- Converts |vim.Pos| to mark position (see |api-indexing|). +--- +--- Example: +--- ```lua +--- local pos = vim.pos(0, 3, 5) +--- +--- -- Convert to mark position, you can call it in a method style. +--- local lnum, col = pos:to_mark() +--- vim.api.nvim_buf_set_mark(0, 'M', lnum, col, {}) +--- ``` ---@param pos vim.Pos ----@return integer, integer +---@return integer lnum, integer col function M.to_mark(pos) - return pos[1] + 1, pos[2] + validate('pos', pos, 'table') + return util.to_mark(pos[1], pos[2]) end --- Creates a new |vim.Pos| from mark position (see |api-indexing|). +--- +--- Example: +--- ```lua +--- local mark_info = vim.api.nvim_get_mark('M', {}) +--- local lnum, col, buf, name = unpack(mark_info) +--- +--- if lnum == 0 and col == 0 and buf == 0 then +--- return -- mark 'M' is not set. +--- end +--- +--- local pos = vim.pos.mark(buf, lnum, col) +--- ``` ---@param buf integer ----@param row integer +---@param lnum integer ---@param col integer -function M.mark(buf, row, col) +---@return vim.Pos +function M.mark(buf, lnum, col) + validate('buf', buf, 'number') + validate('lnum', lnum, 'number') + validate('col', col, 'number') + if buf == 0 then buf = api.nvim_get_current_buf() end - return M.new(buf, row - 1, col) + return M.new(buf, util.from_mark(lnum, col)) end --- Converts |vim.Pos| to extmark position (see |api-indexing|). +--- +--- Example: +--- ```lua +--- local pos = vim.pos(0, 3, 5) +--- +--- -- Convert to extmark position, you can call it in a method style. +--- local extmark_pos = pos:to_extmark() +--- ``` ---@param pos vim.Pos ----@return integer, integer +---@return integer row, integer col 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 + validate('pos', pos, 'table') + return pos[1], pos[2] end --- Creates a new |vim.Pos| from extmark position (see |api-indexing|). +--- +--- Example: +--- ```lua +--- local pos = vim.pos.extmark(0, 3, 5) +--- ``` ---@param buf integer ---@param row integer ---@param col integer +---@return vim.Pos function M.extmark(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 @@ -348,18 +270,40 @@ function M.extmark(buf, row, col) return M.new(buf, row, col) end ---- Converts |vim.Pos| to buffer offset. +--- Converts |vim.Pos| to buffer (bytes) offset. +--- +--- Example: +--- ```lua +--- local p1 = vim.pos(0, 3, 5) +--- local p2 = vim.pos(0, 4, 0) +--- +--- -- Convert to buffer offset, you can call it in a method style. +--- local offset1 = p1:to_offset() +--- local offset2 = p2:to_offset() +--- -- Can be used to calculate the distance between two locations. +--- local distance = offset2 - offset1 +--- ``` ---@param pos vim.Pos ---@return integer function M.to_offset(pos) + validate('pos', pos, 'table') return api.nvim_buf_get_offset(pos.buf, pos[1]) + pos[2] end ---- Creates a new |vim.Pos| from buffer offset. +--- Creates a new |vim.Pos| from buffer (bytes) offset. +--- +--- Example: +--- ```lua +--- local offset = vim.api.nvim_buf_get_offset(0, vim.api.nvim_buf_line_count(0)) +--- local pos = vim.pos.offset(0, offset) +--- ``` ---@param buf integer ---@param offset integer ---@return vim.Pos function M.offset(buf, offset) + validate('buf', buf, 'number') + validate('offset', offset, 'number') + local lnum = vim.list.bisect( setmetatable({}, { __index = function(_, lnum) @@ -375,11 +319,6 @@ function M.offset(buf, offset) 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(_, ...) diff --git a/runtime/lua/vim/pos/_util.lua b/runtime/lua/vim/pos/_util.lua new file mode 100644 index 0000000000..cd82a649fd --- /dev/null +++ b/runtime/lua/vim/pos/_util.lua @@ -0,0 +1,205 @@ +---@brief +--- Unlike `vim.pos`, this module is used to provide utility functions +--- for unpacked `row`, `col`. +--- +--- The variable names have some implications: +--- +--- - `row` is used to represent a 0-based index of a line. +--- - `lnum` is used to represent a 1-based index of a line, short for "line number". + +local api = vim.api +local uv = vim.uv + +local M = {} + +---@param a_row integer +---@param a_col integer +---@param b_row integer +---@param b_col integer +---@return integer +--- 1: a > b +--- 0: a == b +--- -1: a < b +local function cmp_pos(a_row, a_col, b_row, b_col) + if a_row == b_row then + if a_col > b_col then + return 1 + elseif a_col < b_col then + return -1 + else + return 0 + end + elseif a_row > b_row then + return 1 + end + + return -1 +end + +---@type table<'lt'|'le'|'gt'|'ge'|'eq'|'ne', fun(a_row: integer, a_col: integer, b_row: integer, b_col: integer): boolean> +M.cmp_pos = { + lt = function(...) + return cmp_pos(...) == -1 + end, + le = function(...) + return cmp_pos(...) ~= 1 + end, + gt = function(...) + return cmp_pos(...) == 1 + end, + ge = function(...) + return cmp_pos(...) ~= -1 + end, + eq = function(...) + return cmp_pos(...) == 0 + end, + ne = function(...) + return cmp_pos(...) ~= 0 + end, +} + +setmetatable(M.cmp_pos, { __call = cmp_pos }) + +--- 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<integer, string> # a table mapping rows to lines +function M.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<integer,string> + 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<integer,true|string> 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<integer,string>]] +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 +function M.get_line(bufnr, row) + return M.get_lines(bufnr, { row })[row] +end + +---@param buf integer +---@param row integer +---@param col integer +---@param position_encoding lsp.PositionEncodingKind +function M.to_lsp(buf, row, col, position_encoding) + -- When on the first character, + -- we can ignore the difference between byte and character. + if col > 0 then + col = vim.str_utfindex(M.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(M.get_line(buf, row), position_encoding) + end + ---@type lsp.Position + return { line = row, character = col } +end + +---@param buf integer +---@param position lsp.Position +---@param position_encoding lsp.PositionEncodingKind +function M.from_lsp(buf, position, position_encoding) + local row, col = position.line, position.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(M.get_line(buf, row) or '', position_encoding, col, false) + end + return row, col +end + +---@param row integer +---@param col integer +---@return integer lnum, integer col +function M.to_mark(row, col) + return row + 1, col +end + +---@param lnum integer +---@param col integer +---@return integer row, integer col +function M.from_mark(lnum, col) + return lnum - 1, col +end + +return M diff --git a/runtime/lua/vim/range.lua b/runtime/lua/vim/range.lua index e069df4a3a..489b41e2b3 100644 --- a/runtime/lua/vim/range.lua +++ b/runtime/lua/vim/range.lua @@ -8,6 +8,7 @@ local validate = vim.validate local api = vim.api +local util = require('vim.pos._util') --- Represents a range. Call `vim.range()` to create a new range. --- @@ -114,43 +115,12 @@ function M.new(...) return self 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 - ----@param p1_row integer Row of first position to compare. ----@param p1_col integer Col of first position to compare. ----@param p2_row integer Row of second position to compare. ----@param p2_col integer Col of second position to compare. ----@return integer ---- 1: a > b ---- 0: a == b ---- -1: a < b -local function cmp_pos(p1_row, p1_col, p2_row, p2_col) - 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 - ---@param row integer ---@param col integer ---@param buf integer ---@return integer, integer local function to_inclusive_pos(buf, row, col) - local line = get_line(buf, row) + local line = util.get_line(buf, row) if col > 0 then col = col + vim.str_utf_start(line, col) - 1 elseif col == 0 and row > 0 then @@ -166,7 +136,7 @@ end ---@param buf integer ---@return integer, integer local function to_exclusive_pos(buf, row, col) - local line = get_line(buf, row) + local line = util.get_line(buf, row) if col >= #line then row = row + 1 col = 0 @@ -182,11 +152,11 @@ end ---@param r2 vim.Range function M.__lt(r1, r2) if r1:is_empty() or r2:is_empty() then - return cmp_pos(r1[3], r1[4], r2[1], r2[2]) ~= 1 + return util.cmp_pos.le(r1[3], r1[4], r2[1], r2[2]) end local r1_inclusive_end_row, r1_inclusive_end_col = to_inclusive_pos(r1.buf, r1[3], r1[4]) - return cmp_pos(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) == -1 + return util.cmp_pos.lt(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) end ---@private @@ -194,18 +164,18 @@ end ---@param r2 vim.Range function M.__le(r1, r2) if r1:is_empty() or r2:is_empty() then - return cmp_pos(r1[3], r1[4], r2[1], r2[2]) ~= 1 + return util.cmp_pos.le(r1[3], r1[4], r2[1], r2[2]) end local r1_inclusive_end_row, r1_inclusive_end_col = to_inclusive_pos(r1.buf, r1[3], r1[4]) - return cmp_pos(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) ~= 1 + return util.cmp_pos.le(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) end ---@private ---@param r1 vim.Range ---@param r2 vim.Range function M.__eq(r1, r2) - return cmp_pos(r1[1], r1[2], r2[1], r2[2]) == 0 and cmp_pos(r1[3], r1[4], r2[3], r2[4]) == 0 + return util.cmp_pos.eq(r1[1], r1[2], r2[1], r2[2]) and util.cmp_pos.eq(r1[3], r1[4], r2[3], r2[4]) end --- Checks whether the given range is empty; i.e., start >= end. @@ -213,7 +183,8 @@ end ---@param range vim.Range ---@return boolean `true` if the given range is empty. function M.is_empty(range) - return cmp_pos(range[1], range[2], range[3], range[4]) ~= -1 + validate('range', range, 'table') + return util.cmp_pos.ge(range[1], range[2], range[3], range[4]) end --- Checks whether {outer} range contains {inner} range or position. @@ -222,10 +193,13 @@ end ---@param inner vim.Range|vim.Pos ---@return boolean `true` if {outer} range fully contains {inner} range or position. function M.has(outer, inner) + validate('outer', outer, 'table') + validate('inner', inner, 'table') + if getmetatable(inner) == vim.pos then ---@cast inner -vim.Range - return cmp_pos(outer[1], outer[2], inner[1], inner[2]) ~= 1 - and cmp_pos(outer[3], outer[4], inner[1], inner[2]) ~= -1 + return util.cmp_pos.le(outer[1], outer[2], inner[1], inner[2]) + and util.cmp_pos.ge(outer[3], outer[4], inner[1], inner[2]) end ---@cast inner -vim.Pos @@ -237,15 +211,15 @@ function M.has(outer, inner) -- the text outside `outer` if ( - cmp_pos(outer[1], outer[2], inner[3], inner[4]) ~= -1 - or cmp_pos(outer[3], outer[4], inner[1], inner[2]) ~= 1 + util.cmp_pos.ge(outer[1], outer[2], inner[3], inner[4]) + or util.cmp_pos.le(outer[3], outer[4], inner[1], inner[2]) ) and inner:is_empty() then return false end - return cmp_pos(outer[1], outer[2], inner[1], inner[2]) ~= 1 - and cmp_pos(outer[3], outer[4], inner[3], inner[4]) ~= -1 + return util.cmp_pos.le(outer[1], outer[2], inner[1], inner[2]) + and util.cmp_pos.ge(outer[3], outer[4], inner[3], inner[4]) end --- Computes the common range shared by the given ranges. @@ -255,6 +229,9 @@ end ---@return vim.Range? range that is present inside both `r1` and `r2`. --- `nil` if such range does not exist. function M.intersect(r1, r2) + validate('r1', r1, 'table') + validate('r2', r2, 'table') + if r1.buf ~= r2.buf then return nil end @@ -266,14 +243,14 @@ function M.intersect(r1, r2) local r2_inclusive_end_row, r2_inclusive_end_col = to_inclusive_pos(r2.buf, r2[3], r2[4]) if - cmp_pos(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) ~= 1 - or cmp_pos(r1[1], r1[2], r2_inclusive_end_row, r2_inclusive_end_col) ~= -1 + util.cmp_pos.le(r1_inclusive_end_row, r1_inclusive_end_col, r2[1], r2[2]) + or util.cmp_pos.ge(r1[1], r1[2], r2_inclusive_end_row, r2_inclusive_end_col) then return nil end - local rs = cmp_pos(r1[1], r1[2], r2[1], r2[2]) ~= 1 and r2 or r1 - local re = cmp_pos(r1[3], r1[4], r2[3], r2[4]) ~= -1 and r2 or r1 + local rs = util.cmp_pos.le(r1[1], r1[2], r2[1], r2[2]) and r2 or r1 + local re = util.cmp_pos.ge(r1[3], r1[4], r2[3], r2[4]) and r2 or r1 return M.new(r1.buf, rs[1], rs[2], re[3], re[4]) end @@ -293,10 +270,11 @@ function M.to_lsp(range, position_encoding) validate('range', range, 'table') validate('position_encoding', position_encoding, 'string', true) + local buf = range.buf ---@type lsp.Range return { - ['start'] = vim.pos(range.buf, range[1], range[2]):to_lsp(position_encoding), - ['end'] = vim.pos(range.buf, range[3], range[4]):to_lsp(position_encoding), + ['start'] = util.to_lsp(buf, range[1], range[2], position_encoding), + ['end'] = util.to_lsp(buf, range[3], range[4], position_encoding), } end @@ -314,6 +292,7 @@ end ---@param buf integer ---@param range lsp.Range ---@param position_encoding lsp.PositionEncodingKind +---@return vim.Range function M.lsp(buf, range, position_encoding) validate('buf', buf, 'number') validate('range', range, 'table') @@ -323,12 +302,9 @@ function M.lsp(buf, range, position_encoding) buf = api.nvim_get_current_buf() end - -- TODO(ofseed): avoid using `Pos:lsp()` here, - -- as they need reading files separately if buffer is unloaded. - local start = vim.pos.lsp(buf, range['start'], position_encoding) - local end_ = vim.pos.lsp(buf, range['end'], position_encoding) - - return M.new(start, end_) + local start_row, start_col = util.from_lsp(buf, range['start'], position_encoding) + local end_row, end_col = util.from_lsp(buf, range['end'], position_encoding) + return M.new(buf, start_row, start_col, end_row, end_col) end --- Converts |vim.Range| to extmark range (see |api-indexing|). @@ -338,10 +314,10 @@ end --- local range = vim.range(0, 3, 5, 4, 0) --- --- -- Convert to mark range, you can call it in a method style. ---- local start_row, start_col, end_row, end_col = range:to_mark() +--- local start_lnum, start_col, end_lnum, end_col = range:to_mark() --- ``` ---@param range vim.Range ----@return integer, integer, integer, integer +---@return integer start_lnum, integer start_col, integer end_lnum, integer end_col function M.to_mark(range) validate('range', range, 'table') @@ -351,8 +327,8 @@ function M.to_mark(range) end_row, end_col = to_inclusive_pos(buf, end_row, end_col) end - start_row, start_col = vim.pos(buf, start_row, start_col):to_mark() - end_row, end_col = vim.pos(buf, end_row, end_col):to_mark() + start_row, start_col = util.to_mark(start_row, start_col) + end_row, end_col = util.to_mark(end_row, end_col) return start_row, start_col, end_row, end_col end @@ -361,35 +337,36 @@ end --- Example: --- ```lua --- -- A range represented by marks may be end-inclusive (decided by 'selection' option). ---- local start_row, start_col = unpack(api.nvim_buf_get_mark(bufnr, '<')) ---- local end_row, end_col = unpack(api.nvim_buf_get_mark(bufnr, '>')) +--- local start_lnum, start_col = unpack(api.nvim_buf_get_mark(bufnr, '<')) +--- local end_lnum, end_col = unpack(api.nvim_buf_get_mark(bufnr, '>')) --- --- -- Create an end-exclusive range. ---- local range = vim.range.mark(0, start_row, start_col, end_row, end_col) +--- local range = vim.range.mark(0, start_lnum, start_col, end_lnum, end_col) --- ``` ---@param buf integer ----@param start_row integer +---@param start_lnum integer ---@param start_col integer ----@param end_row integer +---@param end_lnum integer ---@param end_col integer -function M.mark(buf, start_row, start_col, end_row, end_col) +---@return vim.Range +function M.mark(buf, start_lnum, start_col, end_lnum, end_col) validate('buf', buf, 'number') - validate('start_row', start_row, 'number') + validate('start_lnum', start_lnum, 'number') validate('start_col', start_col, 'number') - validate('end_row', end_row, 'number') + validate('end_lnum', end_lnum, 'number') validate('end_col', end_col, 'number') if buf == 0 then buf = api.nvim_get_current_buf() end - local start = vim.pos.mark(buf, start_row, start_col) - local end_ = vim.pos.mark(buf, end_row, end_col) + start_lnum, start_col = util.from_mark(start_lnum, start_col) + end_lnum, end_col = util.from_mark(end_lnum, end_col) if vim.o.selection ~= 'exclusive' then - end_[1], end_[2] = to_exclusive_pos(buf, end_[1], end_[2]) + end_lnum, end_col = to_exclusive_pos(buf, end_lnum, end_col) end - return M.new(start, end_) + return M.new(buf, start_lnum, start_col, end_lnum, end_col) end --- Converts |vim.Range| to extmark range (see |api-indexing|). @@ -402,12 +379,30 @@ end --- local extmark_range = range:to_extmark() --- ``` ---@param range vim.Range +---@return integer start_row, integer start_col, integer end_row, integer end_col function M.to_extmark(range) validate('range', range, 'table') - local srow, scol = vim.pos(range.buf, range[1], range[2]):to_extmark() - local erow, ecol = vim.pos(range.buf, range[3], range[4]):to_extmark() - return srow, scol, erow, ecol + local buf = range.buf + local start_row, start_col, end_row, end_col = range[1], range[2], range[3], range[4] + -- 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 end_col == 0 and end_row == api.nvim_buf_line_count(buf) then + end_row = end_row - 1 + end_col = #util.get_line(buf, end_row) + end + return start_row, start_col, end_row, end_col end --- Creates a new |vim.Range| from extmark range (see |api-indexing|). @@ -421,6 +416,7 @@ end ---@param start_col integer ---@param end_row integer ---@param end_col integer +---@return vim.Range function M.extmark(buf, start_row, start_col, end_row, end_col) validate('buf', buf, 'number') validate('start_row', start_row, 'number') @@ -432,56 +428,7 @@ function M.extmark(buf, start_row, start_col, end_row, end_col) buf = api.nvim_get_current_buf() end - local start = vim.pos.extmark(buf, start_row, start_col) - local end_ = vim.pos.extmark(buf, end_ro |