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,