From 8d1233a1440cb93f64ff1d6181138437251e3e63 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Tue, 19 May 2026 16:32:12 +0800 Subject: [PATCH 1/3] feat(pos,range)!: remove `range.cursor()` and `range:to_cursor()`, Problem: - A window can only have one cursor, ranges selected by the cursor are typically obtained by marks like ">" and "<", instead of calling get_cursor() twice. - `vim.Range` is described as end-exclusive, but the current `range.cursor()`/`range:to_cursor()` are end-inclusive. - Conversion between `vim.Range` and mark-indexed range can be done by `range.mark()`/`range:to_mark()` Solution: Remove `range.cursor()` and `range:to_cursor()`, --- runtime/doc/lua.txt | 37 ++++-------------- runtime/doc/news.txt | 2 + runtime/lua/vim/lsp/inline_completion.lua | 2 +- runtime/lua/vim/pos.lua | 9 +++++ runtime/lua/vim/range.lua | 46 ----------------------- 5 files changed, 20 insertions(+), 76 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 725d013999..bdbb441dba 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -4560,6 +4560,14 @@ offset({buf}, {offset}) *vim.pos.offset()* to_cursor({pos}) *vim.pos.to_cursor()* 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) +< + Parameters: ~ • {pos} (`vim.Pos`) See |vim.Pos|. @@ -4658,22 +4666,6 @@ Provides operations to compare, calculate, and convert ranges represented by • {start_row} (`integer`) 0-based byte index. -cursor({buf}, {start_pos}, {end_pos}) *vim.range.cursor()* - Creates a new |vim.Range| from mark-like range (see |api-indexing|). - - Example: >lua - local start = vim.api.nvim_win_get_cursor(0) - -- move the cursor - local end_ = vim.api.nvim_win_get_cursor(0) - - local range = vim.range.cursor(0, start, end_) -< - - Parameters: ~ - • {buf} (`integer`) - • {start_pos} (`[integer, integer]`) - • {end_pos} (`[integer, integer]`) - *vim.range.extmark()* extmark({buf}, {start_row}, {start_col}, {end_row}, {end_col}) Creates a new |vim.Range| from extmark range (see |api-indexing|). @@ -4757,19 +4749,6 @@ mark({buf}, {start_row}, {start_col}, {end_row}, {end_col}) • {end_row} (`integer`) • {end_col} (`integer`) -to_cursor({range}) *vim.range.to_cursor()* - Converts |vim.Range| to mark-like range (see |api-indexing|). - - Example: >lua - local range = vim.range(0, 3, 5, 4, 0) - - -- Convert to cursor range, you can call it in a method style. - local cursor_range = range:to_cursor() -< - - Parameters: ~ - • {range} (`vim.Range`) See |vim.Range|. - to_extmark({range}) *vim.range.to_extmark()* Converts |vim.Range| to extmark range (see |api-indexing|). diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index adf2a09583..880f772306 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -33,6 +33,8 @@ LSP LUA • vim.pos, vim.range always require the `buf` parameter. +• range.cursor() and range.to_cursor() are removed. + Use range.mark() and range.to_mark() instead. DIAGNOSTICS 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/pos.lua b/runtime/lua/vim/pos.lua index a37353f736..c1142aa632 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -278,6 +278,15 @@ function M.lsp(buf, pos, position_encoding) 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 function M.to_cursor(pos) diff --git a/runtime/lua/vim/range.lua b/runtime/lua/vim/range.lua index e069df4a3a..faf5f650ad 100644 --- a/runtime/lua/vim/range.lua +++ b/runtime/lua/vim/range.lua @@ -438,52 +438,6 @@ function M.extmark(buf, start_row, start_col, end_row, end_col) return M.new(start, end_) end ---- Converts |vim.Range| to mark-like range (see |api-indexing|). ---- ---- Example: ---- ```lua ---- local range = vim.range(0, 3, 5, 4, 0) ---- ---- -- Convert to cursor range, you can call it in a method style. ---- local cursor_range = range:to_cursor() ---- ``` ----@param range vim.Range -function M.to_cursor(range) - validate('range', range, 'table') - - local srow, scol = vim.pos(range.buf, range[1], range[2]):to_cursor() - local erow, ecol = vim.pos(range.buf, range[3], range[4]):to_cursor() - return srow, scol, erow, ecol -end - ---- Creates a new |vim.Range| from mark-like range (see |api-indexing|). ---- ---- Example: ---- ```lua ---- local start = vim.api.nvim_win_get_cursor(0) ---- -- move the cursor ---- local end_ = vim.api.nvim_win_get_cursor(0) ---- ---- local range = vim.range.cursor(0, start, end_) ---- ``` ----@param buf integer ----@param start_pos [integer, integer] ----@param end_pos [integer, integer] -function M.cursor(buf, start_pos, end_pos) - validate('buf', buf, 'number') - validate('range', start_pos, 'table') - validate('range', end_pos, 'table') - - if buf == 0 then - buf = api.nvim_get_current_buf() - end - - local start = vim.pos.cursor(buf, start_pos) - local end_ = vim.pos.cursor(buf, end_pos) - - return M.new(start, end_) -end - -- Overload `Range.new` to allow calling this module as a function. setmetatable(M, { __call = function(_, ...) From b1c1f320890a4577a5edebfe267d2c5f6f8dd130 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Wed, 20 May 2026 16:02:57 +0800 Subject: [PATCH 2/3] refactor(pos,range): extract `vim.pos._util` Problem: - To share logic, creating a `vim.Range` currently creates two `vim.Pos` values as intermediates, which causes unnecessary table allocations. - `pos.lua` and `range.lua` contain some overlapping logic. Solution: Add `vim.pos._util`, a module for handling positions represented directly by `row` and `col`. --- runtime/lua/vim/lsp/util.lua | 6 +- runtime/lua/vim/pos.lua | 197 +++---------------------- runtime/lua/vim/pos/_util.lua | 205 ++++++++++++++++++++++++++ runtime/lua/vim/range.lua | 122 +++++++-------- runtime/lua/vim/treesitter/_range.lua | 49 +----- test/functional/lua/pos_spec.lua | 30 ---- test/functional/lua/range_spec.lua | 42 ++++++ 7 files changed, 326 insertions(+), 325 deletions(-) create mode 100644 runtime/lua/vim/pos/_util.lua 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 c1142aa632..93865049c3 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 +---@private +---@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.__lt(...) - 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.__le(...) - return cmp_pos(...) ~= 1 -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 # 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 - 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 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]] -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`. @@ -225,21 +116,7 @@ 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`. @@ -265,15 +142,7 @@ 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 @@ -290,21 +159,21 @@ end ---@param pos vim.Pos ---@return integer, integer function M.to_cursor(pos) - return pos[1] + 1, pos[2] + return util.to_mark(pos[1], pos[2]) end --- Creates a new |vim.Pos| from cursor position (see |api-indexing|). ---@param buf integer ---@param pos [integer, integer] function M.cursor(buf, pos) - return M.new(buf, pos[1] - 1, pos[2]) + return M.new(buf, util.from_mark(pos[1], pos[2])) end --- Converts |vim.Pos| to mark position (see |api-indexing|). ---@param pos vim.Pos ---@return integer, integer function M.to_mark(pos) - return pos[1] + 1, pos[2] + return util.to_mark(pos[1], pos[2]) end --- Creates a new |vim.Pos| from mark position (see |api-indexing|). @@ -316,33 +185,14 @@ function M.mark(buf, row, col) buf = api.nvim_get_current_buf() end - return M.new(buf, row - 1, col) + return M.new(buf, util.from_mark(row, col)) end --- Converts |vim.Pos| to extmark position (see |api-indexing|). ---@param pos vim.Pos ---@return integer, integer 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 + return pos[1], pos[2] end --- Creates a new |vim.Pos| from extmark position (see |api-indexing|). @@ -384,11 +234,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 # 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 + 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 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]] +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 faf5f650ad..6ba3281526 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,7 @@ 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 + return util.cmp_pos.ge(range[1], range[2], range[3], range[4]) end --- Checks whether {outer} range contains {inner} range or position. @@ -224,8 +194,8 @@ end function M.has(outer, inner) 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 +207,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. @@ -266,14 +236,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 +263,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 @@ -323,12 +294,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|). @@ -351,8 +319,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 @@ -383,13 +351,13 @@ function M.mark(buf, start_row, start_col, end_row, end_col) 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_row, start_col = util.from_mark(start_row, start_col) + end_row, end_col = util.from_mark(end_row, end_col) if vim.o.selection ~= 'exclusive' then - end_[1], end_[2] = to_exclusive_pos(buf, end_[1], end_[2]) + end_row, end_col = to_exclusive_pos(buf, end_row, end_col) end - return M.new(start, end_) + return M.new(buf, start_row, start_col, end_row, end_col) end --- Converts |vim.Range| to extmark range (see |api-indexing|). @@ -405,9 +373,26 @@ end 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|). @@ -432,10 +417,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_row, end_col) - - return M.new(start, end_) + return M.new(buf, start_row, start_col, end_row, end_col) end -- Overload `Range.new` to allow calling this module as a function. diff --git a/runtime/lua/vim/treesitter/_range.lua b/runtime/lua/vim/treesitter/_range.lua index 8ec938acc5..b37135efc7 100644 --- a/runtime/lua/vim/treesitter/_range.lua +++ b/runtime/lua/vim/treesitter/_range.lua @@ -1,4 +1,5 @@ local api = vim.api +local util = require('vim.pos._util') local M = {} @@ -25,52 +26,8 @@ local M = {} ---@alias Range Range2|Range4|Range6 ----@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 - -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 }) +-- TODO(ofseed): directly use `cmp_pos` from `util` and replace all exported usages. +M.cmp_pos = util.cmp_pos ---Check if a variable is a valid range object ---@param r any diff --git a/test/functional/lua/pos_spec.lua b/test/functional/lua/pos_spec.lua index db5975d973..6805b0f308 100644 --- a/test/functional/lua/pos_spec.lua +++ b/test/functional/lua/pos_spec.lua @@ -92,36 +92,6 @@ describe('vim.pos', function() }, pos) end) - it("converts between vim.Pos and extmark on buffer's last line", function() - local buf = exec_lua(function() - return vim.api.nvim_get_current_buf() - end) - insert('Some text') - local extmark_pos = { - exec_lua(function() - local pos = vim.pos(buf, 1, 0) - return pos:to_extmark() - end), - } - eq({ 0, 9 }, extmark_pos) - local pos = exec_lua(function() - return vim.pos.extmark(buf, extmark_pos[1], extmark_pos[2]) - end) - eq({ 0, 9, buf }, pos) - - local extmark_pos2 = { - exec_lua(function() - local pos2 = vim.pos(buf, 0, 9) - return pos2:to_extmark() - end), - } - eq({ 0, 9 }, extmark_pos2) - local pos2 = exec_lua(function() - return vim.pos.extmark(buf, extmark_pos2[1], extmark_pos2[2]) - 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() diff --git a/test/functional/lua/range_spec.lua b/test/functional/lua/range_spec.lua index d165080d81..fff29acd7b 100644 --- a/test/functional/lua/range_spec.lua +++ b/test/functional/lua/range_spec.lua @@ -92,6 +92,48 @@ describe('vim.range', function() eq({ 1, 0, 1, 0 }, mark_range) end) + it("converts between vim.Range and extmark on buffer's last line", function() + local buf = exec_lua(function() + return vim.api.nvim_get_current_buf() + end) + insert('Some text') + local extmark_range = { + exec_lua(function() + local range = vim.range(buf, 0, 0, 1, 0) + return range:to_extmark() + end), + } + eq({ 0, 0, 0, 9 }, extmark_range) + local range = exec_lua(function() + return vim.range.extmark( + buf, + extmark_range[1], + extmark_range[2], + extmark_range[3], + extmark_range[4] + ) + end) + eq({ 0, 0, 0, 9, buf }, range) + + local extmark_range2 = { + exec_lua(function() + local range2 = vim.range(buf, 0, 0, 0, 9) + return range2:to_extmark() + end), + } + eq({ 0, 0, 0, 9 }, extmark_range2) + local range2 = exec_lua(function() + return vim.range.extmark( + buf, + extmark_range2[1], + extmark_range2[2], + extmark_range2[3], + extmark_range2[4] + ) + end) + eq({ 0, 0, 0, 9, buf }, range2) + end) + it('checks whether a range contains a position', function() eq( true, From cd3b544611aa2181b04c77ffd36f470a46c61879 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Wed, 20 May 2026 16:03:12 +0800 Subject: [PATCH 3/3] refactor(pos,range): add missing validators and improve the docs Problem: Our documentation is incomplete or inconsistent in several ways: - Some public APIs lack corresponding validators. - Some public APIs lack usage examples. - The meaning of some return values or parameters is not clearly explained. Solution: Add the missing validators, examples, and clarifications. --- runtime/doc/lua.txt | 135 ++++++++++++++++++++++++++++++-------- runtime/lua/vim/pos.lua | 125 +++++++++++++++++++++++++++++------ runtime/lua/vim/range.lua | 39 +++++++---- 3 files changed, 238 insertions(+), 61 deletions(-) diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index bdbb441dba..55c120deb2 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -4510,18 +4510,33 @@ by |vim.Pos| objects. cursor({buf}, {pos}) *vim.pos.cursor()* 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) +< + Parameters: ~ • {buf} (`integer`) - • {pos} (`[integer, integer]`) + • {pos} (`[integer, integer]`) (lnum, col) tuple + + Return: ~ + (`vim.Pos`) See |vim.Pos|. extmark({buf}, {row}, {col}) *vim.pos.extmark()* Creates a new |vim.Pos| from extmark position (see |api-indexing|). + Example: >lua + local pos = vim.pos.extmark(0, 3, 5) +< + Parameters: ~ • {buf} (`integer`) • {row} (`integer`) • {col} (`integer`) + Return: ~ + (`vim.Pos`) See |vim.Pos|. + lsp({buf}, {pos}, {position_encoding}) *vim.pos.lsp()* Creates a new |vim.Pos| from `lsp.Position`. @@ -4539,16 +4554,38 @@ lsp({buf}, {pos}, {position_encoding}) *vim.pos.lsp()* • {pos} (`lsp.Position`) • {position_encoding} (`lsp.PositionEncodingKind`) -mark({buf}, {row}, {col}) *vim.pos.mark()* + Return: ~ + (`vim.Pos`) See |vim.Pos|. + +mark({buf}, {lnum}, {col}) *vim.pos.mark()* 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) +< + Parameters: ~ - • {buf} (`integer`) - • {row} (`integer`) - • {col} (`integer`) + • {buf} (`integer`) + • {lnum} (`integer`) + • {col} (`integer`) + + Return: ~ + (`vim.Pos`) See |vim.Pos|. offset({buf}, {offset}) *vim.pos.offset()* - 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) +< Parameters: ~ • {buf} (`integer`) @@ -4572,18 +4609,25 @@ to_cursor({pos}) *vim.pos.to_cursor()* • {pos} (`vim.Pos`) See |vim.Pos|. Return (multiple): ~ - (`integer`) - (`integer`) + (`integer`) lnum + (`integer`) col to_extmark({pos}) *vim.pos.to_extmark()* 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() +< + Parameters: ~ • {pos} (`vim.Pos`) See |vim.Pos|. Return (multiple): ~ - (`integer`) - (`integer`) + (`integer`) row + (`integer`) col to_lsp({pos}, {position_encoding}) *vim.pos.to_lsp()* Converts |vim.Pos| to `lsp.Position`. @@ -4599,18 +4643,40 @@ to_lsp({pos}, {position_encoding}) *vim.pos.to_lsp()* • {pos} (`vim.Pos`) See |vim.Pos|. • {position_encoding} (`lsp.PositionEncodingKind`) + Return: ~ + (`lsp.Position`) + to_mark({pos}) *vim.pos.to_mark()* 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, {}) +< + Parameters: ~ • {pos} (`vim.Pos`) See |vim.Pos|. Return (multiple): ~ - (`integer`) - (`integer`) + (`integer`) lnum + (`integer`) col to_offset({pos}) *vim.pos.to_offset()* - 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 +< Parameters: ~ • {pos} (`vim.Pos`) See |vim.Pos|. @@ -4681,6 +4747,9 @@ extmark({buf}, {start_row}, {start_col}, {end_row}, {end_col}) • {end_row} (`integer`) • {end_col} (`integer`) + Return: ~ + (`vim.Range`) See |vim.Range|. + has({outer}, {inner}) *vim.range.has()* Checks whether {outer} range contains {inner} range or position. @@ -4729,25 +4798,31 @@ lsp({buf}, {range}, {position_encoding}) *vim.range.lsp()* • {range} (`lsp.Range`) • {position_encoding} (`lsp.PositionEncodingKind`) + Return: ~ + (`vim.Range`) See |vim.Range|. + *vim.range.mark()* -mark({buf}, {start_row}, {start_col}, {end_row}, {end_col}) +mark({buf}, {start_lnum}, {start_col}, {end_lnum}, {end_col}) Creates a new |vim.Range| from "mark-indexed" range (see |api-indexing|). 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) < Parameters: ~ - • {buf} (`integer`) - • {start_row} (`integer`) - • {start_col} (`integer`) - • {end_row} (`integer`) - • {end_col} (`integer`) + • {buf} (`integer`) + • {start_lnum} (`integer`) + • {start_col} (`integer`) + • {end_lnum} (`integer`) + • {end_col} (`integer`) + + Return: ~ + (`vim.Range`) See |vim.Range|. to_extmark({range}) *vim.range.to_extmark()* Converts |vim.Range| to extmark range (see |api-indexing|). @@ -4762,6 +4837,12 @@ to_extmark({range}) *vim.range.to_extmark()* Parameters: ~ • {range} (`vim.Range`) See |vim.Range|. + Return (multiple): ~ + (`integer`) start_row + (`integer`) start_col + (`integer`) end_row + (`integer`) end_col + to_lsp({range}, {position_encoding}) *vim.range.to_lsp()* Converts |vim.Range| to `lsp.Range`. @@ -4786,17 +4867,17 @@ to_mark({range}) *vim.range.to_mark()* 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() < Parameters: ~ • {range} (`vim.Range`) See |vim.Range|. Return (multiple): ~ - (`integer`) - (`integer`) - (`integer`) - (`integer`) + (`integer`) start_lnum + (`integer`) start_col + (`integer`) end_lnum + (`integer`) end_col ============================================================================== diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua index 93865049c3..15cc318ddd 100644 --- a/runtime/lua/vim/pos.lua +++ b/runtime/lua/vim/pos.lua @@ -112,6 +112,7 @@ 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') @@ -133,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') @@ -157,49 +159,110 @@ end --- 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) + 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, util.from_mark(pos[1], pos[2])) -end + validate('buf', buf, 'number') + validate('pos', pos, 'table') ---- Converts |vim.Pos| to mark position (see |api-indexing|). ----@param pos vim.Pos ----@return integer, integer -function M.to_mark(pos) - return util.to_mark(pos[1], pos[2]) -end - ---- Creates a new |vim.Pos| from mark position (see |api-indexing|). ----@param buf integer ----@param row integer ----@param col integer -function M.mark(buf, row, col) if buf == 0 then buf = api.nvim_get_current_buf() end - return M.new(buf, util.from_mark(row, col)) + 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 lnum, integer col +function M.to_mark(pos) + 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 lnum integer +---@param col integer +---@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, 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) + 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 @@ -207,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) diff --git a/runtime/lua/vim/range.lua b/runtime/lua/vim/range.lua index 6ba3281526..489b41e2b3 100644 --- a/runtime/lua/vim/range.lua +++ b/runtime/lua/vim/range.lua @@ -183,6 +183,7 @@ end ---@param range vim.Range ---@return boolean `true` if the given range is empty. function M.is_empty(range) + validate('range', range, 'table') return util.cmp_pos.ge(range[1], range[2], range[3], range[4]) end @@ -192,6 +193,9 @@ 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 util.cmp_pos.le(outer[1], outer[2], inner[1], inner[2]) @@ -225,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 @@ -285,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') @@ -306,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') @@ -329,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 - start_row, start_col = util.from_mark(start_row, start_col) - end_row, end_col = util.from_mark(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_row, end_col = to_exclusive_pos(buf, end_row, end_col) + end_lnum, end_col = to_exclusive_pos(buf, end_lnum, end_col) end - return M.new(buf, start_row, start_col, end_row, end_col) + return M.new(buf, start_lnum, start_col, end_lnum, end_col) end --- Converts |vim.Range| to extmark range (see |api-indexing|). @@ -370,6 +379,7 @@ 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') @@ -406,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')