diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 794df90d97..258cd71996 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -3949,6 +3949,205 @@ vim.net.request({url}, {opts}, {on_response}) *vim.net.request()* success. +============================================================================== +Lua module: vim.pos *vim.pos* + +WARNING: This module is under experimental support. Its semantics are not yet +finalized, and the stability of this API is not guaranteed. Avoid using it +outside of Nvim. You may subscribe to or participate in the tracking issue +https://github.com/neovim/neovim/issues/25509 to stay updated or contribute to +its development. + +Built on |vim.Pos| objects, this module offers operations that support +comparisons and conversions between various types of positions. + + +*vim.Pos* + Represents a well-defined position. + + A |vim.Pos| object contains the {row} and {col} coordinates of a position. + To create a new |vim.Pos| object, call `vim.pos()`. + + Example: >lua + local pos1 = vim.pos(3, 5) + local pos2 = vim.pos(4, 0) + + -- Operators are overloaded for comparing two `vim.Pos` objects. + if pos1 < pos2 then + print("pos1 comes before pos2") + end + + if pos1 ~= pos2 then + print("pos1 and pos2 are different positions") + end +< + + It may include optional fields that enable additional capabilities, such + as format conversions. + + Fields: ~ + • {row} (`integer`) 0-based byte index. + • {col} (`integer`) 0-based byte index. + • {buf}? (`integer`) Optional buffer handle. + + When specified, it indicates that this position belongs to a + specific buffer. This field is required when performing + position conversions. + • {to_lsp} (`fun(pos: vim.Pos, position_encoding: lsp.PositionEncodingKind)`) + See |Pos:to_lsp()|. + • {lsp} (`fun(buf: integer, pos: lsp.Position, position_encoding: lsp.PositionEncodingKind)`) + See |Pos:lsp()|. + + +Pos:lsp({buf}, {pos}, {position_encoding}) *Pos:lsp()* + Creates a new |vim.Pos| from `lsp.Position`. + + Example: >lua + local buf = vim.api.nvim_get_current_buf() + local lsp_pos = { + line = 3, + character = 5 + } + + -- `buf` is mandatory, as LSP positions are always associated with a buffer. + local pos = vim.pos.lsp(buf, lsp_pos, 'utf-16') +< + + Parameters: ~ + • {buf} (`integer`) + • {pos} (`lsp.Position`) + • {position_encoding} (`lsp.PositionEncodingKind`) + +Pos:to_lsp({pos}, {position_encoding}) *Pos:to_lsp()* + Converts |vim.Pos| to `lsp.Position`. + + Example: >lua + -- `buf` is required for conversion to LSP position. + local buf = vim.api.nvim_get_current_buf() + local pos = vim.pos(3, 5, { buf = buf }) + + -- Convert to LSP position, you can call it in a method style. + local lsp_pos = pos:lsp('utf-16') +< + + Parameters: ~ + • {pos} (`vim.Pos`) See |vim.Pos|. + • {position_encoding} (`lsp.PositionEncodingKind`) + + +============================================================================== +Lua module: vim.range *vim.range* + +WARNING: This module is under experimental support. Its semantics are not yet +finalized, and the stability of this API is not guaranteed. Avoid using it +outside of Nvim. You may subscribe to or participate in the tracking issue +https://github.com/neovim/neovim/issues/25509 to stay updated or contribute to +its development. + +Built on |vim.Range| objects, this module offers operations that support +comparisons as well as containment checks (for positions and for other +ranges). conversions between various types of ranges is also provided. + + +*vim.Range* + Represents a well-defined range. + + A |vim.Range| object contains a {start} and a {end_} position(see + |vim.Pos|). Note that the {end_} position is exclusive. To create a new + |vim.Range| object, call `vim.range()`. + + Example: >lua + local pos1 = vim.pos(3, 5) + local pos2 = vim.pos(4, 0) + + -- Create a range from two positions. + local range1 = vim.range(pos1, pos2) + -- Or createa range from four integers representing start and end positions. + local range2 = vim.range(3, 5, 4, 0) + + -- Because `vim.Range` is end exclusive, `range1` and `range2` both represent + -- a range starting at the row 3, column 5 and ending at where the row 3 ends. + + -- Operators are overloaded for comparing two `vim.Pos` objects. + if range1 == range2 then + print("range1 and range2 are the same range") + end +< + + It may include optional fields that enable additional capabilities, such + as format conversions. Note that the {start} and {end_} positions need to + have the same optional fields. + + Fields: ~ + • {start} (`vim.Pos`) Start position. + • {end_} (`vim.Pos`) End position, exclusive. + • {has} (`fun(outer: vim.Range, inner: vim.Range): boolean`) See + |Range:has()|. + • {intersect} (`fun(r1: vim.Range, r2: vim.Range): vim.Range?`) See + |Range:intersect()|. + • {to_lsp} (`fun(range: vim.Range, position_encoding: lsp.PositionEncodingKind)`) + See |Range:to_lsp()|. + • {lsp} (`fun(buf: integer, range: lsp.Range, position_encoding: lsp.PositionEncodingKind)`) + See |Range:lsp()|. + + +Range:has({outer}, {inner}) *Range:has()* + Checks whether {outer} range contains {inner} range. + + Parameters: ~ + • {outer} (`vim.Range`) See |vim.Range|. + • {inner} (`vim.Range`) See |vim.Range|. + + Return: ~ + (`boolean`) `true` if {outer} range fully contains {inner} range. + +Range:intersect({r1}, {r2}) *Range:intersect()* + Computes the common range shared by the given ranges. + + Parameters: ~ + • {r1} (`vim.Range`) First range to intersect. See |vim.Range|. + • {r2} (`vim.Range`) Second range to intersect. See |vim.Range|. + + Return: ~ + (`vim.Range?`) range that is present inside both `r1` and `r2`. `nil` + if such range does not exist. See |vim.Range|. + +Range:lsp({buf}, {range}, {position_encoding}) *Range:lsp()* + Creates a new |vim.Range| from `lsp.Range`. + + Example: >lua + local buf = vim.api.nvim_get_current_buf() + local lsp_range = { + ['start'] = { line = 3, character = 5 }, + ['end'] = { line = 4, character = 0 } + } + + -- `buf` is mandatory, as LSP ranges are always associated with a buffer. + local range = vim.range.lsp(buf, lsp_range, 'utf-16') +< + + Parameters: ~ + • {buf} (`integer`) + • {range} (`lsp.Range`) + • {position_encoding} (`lsp.PositionEncodingKind`) + +Range:to_lsp({range}, {position_encoding}) *Range:to_lsp()* + Converts |vim.Range| to `lsp.Range`. + + Example: >lua + -- `buf` is required for conversion to LSP range. + local buf = vim.api.nvim_get_current_buf() + local range = vim.range(3, 5, 4, 0, { buf = buf }) + + -- Convert to LSP range, you can call it in a method style. + local lsp_range = range:to_lsp('utf-16') +< + + Parameters: ~ + • {range} (`vim.Range`) See |vim.Range|. + • {position_encoding} (`lsp.PositionEncodingKind`) + + ============================================================================== Lua module: vim.re *vim.re* diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index b6eff77c21..cc7cd013ff 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -241,6 +241,7 @@ LUA • Built-in plugin manager |vim.pack| • |vim.list.unique()| to deduplicate lists. • |vim.list.bisect()| for binary search. +• Experimental `vim.pos` and `vim.range` for Position/Range abstraction. OPTIONS diff --git a/runtime/lua/vim/_editor.lua b/runtime/lua/vim/_editor.lua index 50fac8cf9e..78e264a2a5 100644 --- a/runtime/lua/vim/_editor.lua +++ b/runtime/lua/vim/_editor.lua @@ -42,6 +42,8 @@ for k, v in pairs({ pack = true, _watch = true, net = true, + pos = true, + range = true, }) do vim._submodules[k] = v end diff --git a/runtime/lua/vim/_meta.lua b/runtime/lua/vim/_meta.lua index 9d3daa20c0..02f886cf96 100644 --- a/runtime/lua/vim/_meta.lua +++ b/runtime/lua/vim/_meta.lua @@ -21,6 +21,8 @@ vim.keymap = require('vim.keymap') vim.loader = require('vim.loader') vim.lsp = require('vim.lsp') vim.pack = require('vim.pack') +vim.pos = require('vim.pos') +vim.range = require('vim.range') vim.re = require('vim.re') vim.secure = require('vim.secure') vim.snippet = require('vim.snippet') diff --git a/runtime/lua/vim/pos.lua b/runtime/lua/vim/pos.lua new file mode 100644 index 0000000000..648be60e81 --- /dev/null +++ b/runtime/lua/vim/pos.lua @@ -0,0 +1,189 @@ +---@brief +--- +--- WARNING: This module is under experimental support. +--- Its semantics are not yet finalized, +--- and the stability of this API is not guaranteed. +--- Avoid using it outside of Nvim. +--- You may subscribe to or participate in the tracking issue +--- https://github.com/neovim/neovim/issues/25509 +--- to stay updated or contribute to its development. +--- +--- Built on |vim.Pos| objects, this module offers operations +--- that support comparisons and conversions between various types of positions. + +local api = vim.api +local validate = vim.validate + +--- Represents a well-defined position. +--- +--- A |vim.Pos| object contains the {row} and {col} coordinates of a position. +--- To create a new |vim.Pos| object, call `vim.pos()`. +--- +--- Example: +--- ```lua +--- local pos1 = vim.pos(3, 5) +--- local pos2 = vim.pos(4, 0) +--- +--- -- Operators are overloaded for comparing two `vim.Pos` objects. +--- if pos1 < pos2 then +--- print("pos1 comes before pos2") +--- end +--- +--- if pos1 ~= pos2 then +--- print("pos1 and pos2 are different positions") +--- end +--- ``` +--- +--- It may include optional fields that enable additional capabilities, +--- such as format conversions. +--- +---@class vim.Pos +---@field row integer 0-based byte index. +---@field col integer 0-based byte index. +--- +--- Optional buffer handle. +--- +--- When specified, it indicates that this position belongs to a specific buffer. +--- This field is required when performing position conversions. +---@field buf? integer +local Pos = {} +Pos.__index = Pos + +---@class vim.Pos.Optional +---@inlinedoc +---@field buf? integer + +---@package +---@param row integer +---@param col integer +---@param opts vim.Pos.Optional +function Pos.new(row, col, opts) + validate('row', row, 'number') + validate('col', col, 'number') + validate('opts', opts, 'table', true) + + opts = opts or {} + + ---@type vim.Pos + local self = setmetatable({ + row = row, + col = col, + buf = opts.buf, + }, Pos) + + return self +end + +---@param p1 vim.Pos First position to compare. +---@param p2 vim.Pos Second position to compare. +---@return integer +--- 1: a > b +--- 0: a == b +--- -1: a < b +local function cmp_pos(p1, p2) + if p1.row == p2.row then + if p1.col > p2.col then + return 1 + elseif p1.col < p2.col then + return -1 + else + return 0 + end + elseif p1.row > p2.row then + return 1 + end + + return -1 +end + +---@private +function Pos.__lt(...) + return cmp_pos(...) == -1 +end + +---@private +function Pos.__le(...) + return cmp_pos(...) ~= 1 +end + +---@private +function Pos.__eq(...) + return cmp_pos(...) == 0 +end + +--- TODO(ofseed): Make it work for unloaded buffers. Check get_line() in vim.lsp.util. +---@param buf integer +---@param row integer +local function get_line(buf, row) + return api.nvim_buf_get_lines(buf, row, row + 1, true)[1] +end + +--- Converts |vim.Pos| to `lsp.Position`. +--- +--- Example: +--- ```lua +--- -- `buf` is required for conversion to LSP position. +--- local buf = vim.api.nvim_get_current_buf() +--- local pos = vim.pos(3, 5, { buf = buf }) +--- +--- -- Convert to LSP position, you can call it in a method style. +--- local lsp_pos = pos:lsp('utf-16') +--- ``` +---@param pos vim.Pos +---@param position_encoding lsp.PositionEncodingKind +function Pos.to_lsp(pos, position_encoding) + validate('pos', pos, 'table') + validate('position_encoding', position_encoding, 'string') + + local buf = assert(pos.buf, 'position is not a buffer position') + local row, col = pos.row, pos.col + -- When on the first character, + -- we can ignore the difference between byte and character. + if col > 0 then + col = vim.str_utfindex(get_line(buf, row), position_encoding, col, false) + end + + ---@type lsp.Position + return { line = row, character = col } +end + +--- Creates a new |vim.Pos| from `lsp.Position`. +--- +--- Example: +--- ```lua +--- local buf = vim.api.nvim_get_current_buf() +--- local lsp_pos = { +--- line = 3, +--- character = 5 +--- } +--- +--- -- `buf` is mandatory, as LSP positions are always associated with a buffer. +--- local pos = vim.pos.lsp(buf, lsp_pos, 'utf-16') +--- ``` +---@param buf integer +---@param pos lsp.Position +---@param position_encoding lsp.PositionEncodingKind +function Pos.lsp(buf, pos, position_encoding) + validate('buf', buf, 'number') + validate('pos', pos, 'table') + validate('position_encoding', position_encoding, 'string') + + local row, col = pos.line, pos.character + -- When on the first character, + -- we can ignore the difference between byte and character. + if col > 0 then + col = vim.str_byteindex(get_line(buf, row), position_encoding, col) + end + + return Pos.new(row, col, { buf = buf }) +end + +-- Overload `Range.new` to allow calling this module as a function. +setmetatable(Pos, { + __call = function(_, ...) + return Pos.new(...) + end, +}) +---@cast Pos +fun(row: integer, col: integer, opts: vim.Pos.Optional?): vim.Pos + +return Pos diff --git a/runtime/lua/vim/range.lua b/runtime/lua/vim/range.lua new file mode 100644 index 0000000000..7fdb170874 --- /dev/null +++ b/runtime/lua/vim/range.lua @@ -0,0 +1,194 @@ +---@brief +--- +--- WARNING: This module is under experimental support. +--- Its semantics are not yet finalized, +--- and the stability of this API is not guaranteed. +--- Avoid using it outside of Nvim. +--- You may subscribe to or participate in the tracking issue +--- https://github.com/neovim/neovim/issues/25509 +--- to stay updated or contribute to its development. +--- +--- Built on |vim.Range| objects, this module offers operations +--- that support comparisons as well as containment checks +--- (for positions and for other ranges). +--- conversions between various types of ranges is also provided. + +local validate = vim.validate + +--- Represents a well-defined range. +--- +--- A |vim.Range| object contains a {start} and a {end_} position(see |vim.Pos|). +--- Note that the {end_} position is exclusive. +--- To create a new |vim.Range| object, call `vim.range()`. +--- +--- Example: +--- ```lua +--- local pos1 = vim.pos(3, 5) +--- local pos2 = vim.pos(4, 0) +--- +--- -- Create a range from two positions. +--- local range1 = vim.range(pos1, pos2) +--- -- Or createa range from four integers representing start and end positions. +--- local range2 = vim.range(3, 5, 4, 0) +--- +--- -- Because `vim.Range` is end exclusive, `range1` and `range2` both represent +--- -- a range starting at the row 3, column 5 and ending at where the row 3 ends. +--- +--- -- Operators are overloaded for comparing two `vim.Pos` objects. +--- if range1 == range2 then +--- print("range1 and range2 are the same range") +--- end +--- ``` +--- +--- It may include optional fields that enable additional capabilities, +--- such as format conversions. Note that the {start} and {end_} positions +--- need to have the same optional fields. +--- +---@class vim.Range +---@field start vim.Pos Start position. +---@field end_ vim.Pos End position, exclusive. +local Range = {} +Range.__index = Range + +---@package +---@overload fun(self: vim.Range, start: vim.Pos, end_: vim.Pos): vim.Range +---@overload fun(self: vim.Range, start_row: integer, start_col: integer, end_row: integer, end_col: integer, opts?: vim.Pos.Optional): vim.Range +function Range.new(...) + ---@type vim.Pos, vim.Pos, vim.Pos.Optional + local start, end_ + + local nargs = select('#', ...) + if nargs == 2 then + ---@type vim.Pos, vim.Pos + start, end_ = ... + validate('start', start, 'table') + validate('end_', end_, 'table') + + if start.buf ~= end_.buf then + error('start and end positions must belong to the same buffer') + end + elseif nargs == 4 or nargs == 5 then + ---@type integer, integer, integer, integer, vim.Pos.Optional + local start_row, start_col, end_row, end_col, opts = ... + start, end_ = vim.pos(start_row, start_col, opts), vim.pos(end_row, end_col, opts) + else + error('invalid parameters') + end + + ---@type vim.Range + local self = setmetatable({ + start = start, + end_ = end_, + }, Range) + + return self +end + +---@private +---@param r1 vim.Range +---@param r2 vim.Range +function Range.__lt(r1, r2) + return r1.end_ < r2.start +end + +---@private +---@param r1 vim.Range +---@param r2 vim.Range +function Range.__le(r1, r2) + return r1.end_ <= r2.start +end + +---@private +---@param r1 vim.Range +---@param r2 vim.Range +function Range.__eq(r1, r2) + return r1.start == r2.start and r1.end_ == r2.end_ +end + +--- Checks whether {outer} range contains {inner} range. +--- +---@param outer vim.Range +---@param inner vim.Range +---@return boolean `true` if {outer} range fully contains {inner} range. +function Range.has(outer, inner) + return outer.start <= inner.start and outer.end_ >= inner.end_ +end + +--- Computes the common range shared by the given ranges. +--- +---@param r1 vim.Range First range to intersect. +---@param r2 vim.Range Second range to intersect +---@return vim.Range? range that is present inside both `r1` and `r2`. +--- `nil` if such range does not exist. +function Range.intersect(r1, r2) + if r1.end_ <= r2.start or r1.start >= r2.end_ then + return nil + end + local rs = r1.start <= r2.start and r2 or r1 + local re = r1.end_ >= r2.end_ and r2 or r1 + return Range.new(rs.start, re.end_) +end + +--- Converts |vim.Range| to `lsp.Range`. +--- +--- Example: +--- ```lua +--- -- `buf` is required for conversion to LSP range. +--- local buf = vim.api.nvim_get_current_buf() +--- local range = vim.range(3, 5, 4, 0, { buf = buf }) +--- +--- -- Convert to LSP range, you can call it in a method style. +--- local lsp_range = range:to_lsp('utf-16') +--- ``` +---@param range vim.Range +---@param position_encoding lsp.PositionEncodingKind +function Range.to_lsp(range, position_encoding) + validate('range', range, 'table') + validate('position_encoding', position_encoding, 'string', true) + + ---@type lsp.Range + return { + ['start'] = range.start:to_lsp(position_encoding), + ['end'] = range.end_:to_lsp(position_encoding), + } +end + +--- Creates a new |vim.Range| from `lsp.Range`. +--- +--- Example: +--- ```lua +--- local buf = vim.api.nvim_get_current_buf() +--- local lsp_range = { +--- ['start'] = { line = 3, character = 5 }, +--- ['end'] = { line = 4, character = 0 } +--- } +--- +--- -- `buf` is mandatory, as LSP ranges are always associated with a buffer. +--- local range = vim.range.lsp(buf, lsp_range, 'utf-16') +--- ``` +---@param buf integer +---@param range lsp.Range +---@param position_encoding lsp.PositionEncodingKind +function Range.lsp(buf, range, position_encoding) + validate('buf', buf, 'number') + validate('range', range, 'table') + validate('position_encoding', position_encoding, 'string') + + -- 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 Range.new(start, end_) +end + +-- Overload `Range.new` to allow calling this module as a function. +setmetatable(Range, { + __call = function(_, ...) + return Range.new(...) + end, +}) +---@cast Range +fun(start: vim.Pos, end_: vim.Pos): vim.Range +---@cast Range +fun(start_row: integer, start_col: integer, end_row: integer, end_col: integer, opts?: vim.Pos.Optional): vim.Range + +return Range diff --git a/src/gen/gen_vimdoc.lua b/src/gen/gen_vimdoc.lua index bc53d2cf74..c2692793ca 100755 --- a/src/gen/gen_vimdoc.lua +++ b/src/gen/gen_vimdoc.lua @@ -155,6 +155,8 @@ local config = { 'lpeg.lua', 'mpack.lua', 'net.lua', + 'pos.lua', + 'range.lua', 're.lua', 'regex.lua', 'secure.lua', @@ -193,6 +195,8 @@ local config = { 'runtime/lua/vim/keymap.lua', 'runtime/lua/vim/loader.lua', 'runtime/lua/vim/net.lua', + 'runtime/lua/vim/pos.lua', + 'runtime/lua/vim/range.lua', 'runtime/lua/vim/secure.lua', 'runtime/lua/vim/shared.lua', 'runtime/lua/vim/snippet.lua', diff --git a/test/functional/lua/pos_spec.lua b/test/functional/lua/pos_spec.lua new file mode 100644 index 0000000000..176c6489b0 --- /dev/null +++ b/test/functional/lua/pos_spec.lua @@ -0,0 +1,91 @@ +-- Test suite for vim.pos +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local eq = t.eq + +local clear = n.clear +local exec_lua = n.exec_lua +local insert = n.insert + +describe('vim.pos', function() + before_each(clear) + after_each(clear) + + it('creates a position with or without optional fields', function() + local pos = exec_lua(function() + return vim.pos(3, 5) + end) + eq(3, pos.row) + eq(5, pos.col) + eq(nil, pos.buf) + + local buf = exec_lua(function() + return vim.api.nvim_create_buf(false, true) + end) + pos = exec_lua(function() + return vim.pos(3, 5, { buf = buf }) + end) + eq(3, pos.row) + eq(5, pos.col) + eq(buf, pos.buf) + end) + + it('supports comparisons by overloaded mathmatical operators', function() + eq( + true, + exec_lua(function() + return vim.pos(3, 5) < vim.pos(4, 5) + end) + ) + eq( + true, + exec_lua(function() + return vim.pos(3, 5) <= vim.pos(3, 6) + end) + ) + eq( + true, + exec_lua(function() + return vim.pos(3, 5) > vim.pos(2, 5) + end) + ) + eq( + true, + exec_lua(function() + return vim.pos(3, 5) >= vim.pos(3, 5) + end) + ) + eq( + true, + exec_lua(function() + return vim.pos(3, 5) == vim.pos(3, 5) + end) + ) + eq( + true, + exec_lua(function() + return vim.pos(3, 5) ~= vim.pos(3, 6) + end) + ) + end) + + it('supports conversion between vim.Pos and lsp.Position', function() + local buf = exec_lua(function() + return vim.api.nvim_get_current_buf() + end) + insert('Neovim 是 Vim 的分支,专注于扩展性和可用性。') + local lsp_pos = exec_lua(function() + local pos = vim.pos(0, 36, { buf = buf }) + return pos:to_lsp('utf-16') + end) + eq({ line = 0, character = 20 }, lsp_pos) + local pos = exec_lua(function() + return vim.pos.lsp(buf, lsp_pos, 'utf-16') + end) + eq({ + buf = buf, + row = 0, + col = 36, + }, pos) + end) +end) diff --git a/test/functional/lua/range_spec.lua b/test/functional/lua/range_spec.lua new file mode 100644 index 0000000000..662cc3a5e6 --- /dev/null +++ b/test/functional/lua/range_spec.lua @@ -0,0 +1,86 @@ +-- Test suite for vim.range +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local eq = t.eq + +local clear = n.clear +local exec_lua = n.exec_lua +local insert = n.insert + +describe('vim.range', function() + before_each(clear) + after_each(clear) + + it('creates a range with or without optional fields', function() + local range = exec_lua(function() + return vim.range(3, 5, 4, 6) + end) + eq(3, range.start.row) + eq(5, range.start.col) + eq(4, range.end_.row) + eq(6, range.end_.col) + eq(nil, range.start.buf) + eq(nil, range.end_.buf) + local buf = exec_lua(function() + return vim.api.nvim_create_buf(false, true) + end) + range = exec_lua(function() + return vim.range(3, 5, 4, 6, { buf = buf }) + end) + eq(buf, range.start.buf) + eq(buf, range.end_.buf) + end) + + it('create a range from two positions when optional fields are not matched', function() + local range = exec_lua(function() + return vim.range(vim.pos(3, 5), vim.pos(4, 6)) + end) + eq(3, range.start.row) + eq(5, range.start.col) + eq(4, range.end_.row) + eq(6, range.end_.col) + eq(nil, range.start.buf) + eq(nil, range.end_.buf) + + local buf1 = exec_lua(function() + return vim.api.nvim_create_buf(false, true) + end) + range = exec_lua(function() + return vim.range(vim.pos(3, 5, { buf = buf1 }), vim.pos(4, 6, { buf = buf1 })) + end) + eq(buf1, range.start.buf) + eq(buf1, range.end_.buf) + + local buf2 = exec_lua(function() + return vim.api.nvim_create_buf(false, true) + end) + local success = exec_lua(function() + return pcall(function() + return vim.range(vim.pos(3, 5, { buf = buf1 }), vim.pos(4, 6, { buf = buf2 })) + end) + end) + eq(success, false) + end) + + it('supports conversion between vim.Range and lsp.Range', function() + local buf = exec_lua(function() + return vim.api.nvim_get_current_buf() + end) + insert('Neovim 是 Vim 的分支,专注于扩展性和可用性。') + local lsp_range = exec_lua(function() + local range = vim.range(0, 10, 0, 36, { buf = buf }) + return range:to_lsp('utf-16') + end) + eq({ + ['start'] = { line = 0, character = 8 }, + ['end'] = { line = 0, character = 20 }, + }, lsp_range) + local range = exec_lua(function() + return vim.range.lsp(buf, lsp_range, 'utf-16') + end) + eq({ + start = { row = 0, col = 10, buf = buf }, + end_ = { row = 0, col = 36, buf = buf }, + }, range) + end) +end)