add Range:difference
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 9s

This commit is contained in:
Jonathan Apodaca 2025-04-13 14:44:28 -06:00
parent dacb186324
commit 9f5fdb4f2b
4 changed files with 101 additions and 50 deletions

View File

@ -1,11 +1,12 @@
local vim_repeat = require 'u.repeat' local vim_repeat = require 'u.repeat'
local Pos = require 'u.pos'
local Range = require 'u.range' local Range = require 'u.range'
local Buffer = require 'u.buffer' local Buffer = require 'u.buffer'
local CodeWriter = require 'u.codewriter' local CodeWriter = require 'u.codewriter'
local M = {} local M = {}
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
local surrounds = { local surrounds = {
[')'] = { left = '(', right = ')' }, [')'] = { left = '(', right = ')' },
['('] = { left = '( ', right = ' )' }, ['('] = { left = '( ', right = ' )' },
@ -126,7 +127,7 @@ function M.setup()
do_surround(range, bounds) do_surround(range, bounds)
-- this is a visual mapping: end in normal mode: -- this is a visual mapping: end in normal mode:
vim.cmd { cmd = 'normal', args = { '' }, bang = true } vim.cmd.normal(ESC)
end, { noremap = true, silent = true }) end, { noremap = true, silent = true })
-- Change -- Change

View File

@ -12,8 +12,17 @@ end
--- @field col number 1-based column index --- @field col number 1-based column index
--- @field off number --- @field off number
local Pos = {} local Pos = {}
Pos.__index = Pos
Pos.MAX_COL = MAX_COL Pos.MAX_COL = MAX_COL
function Pos.__tostring(self)
if self.off ~= 0 then
return string.format('Pos(%d:%d){bufnr=%d, off=%d}', self.lnum, self.col, self.bufnr, self.off)
else
return string.format('Pos(%d:%d){bufnr=%d}', self.lnum, self.col, self.bufnr)
end
end
--- @param bufnr? number --- @param bufnr? number
--- @param lnum number 1-based --- @param lnum number 1-based
--- @param col number 1-based --- @param col number 1-based
@ -28,31 +37,13 @@ function Pos.new(bufnr, lnum, col, off)
col = col, col = col,
off = off, off = off,
} }
setmetatable(pos, Pos)
local function str()
if pos.off ~= 0 then
return string.format('Pos(%d:%d){bufnr=%d, off=%d}', pos.lnum, pos.col, pos.bufnr, pos.off)
else
return string.format('Pos(%d:%d){bufnr=%d}', pos.lnum, pos.col, pos.bufnr)
end
end
setmetatable(pos, {
__index = Pos,
__tostring = str,
__lt = Pos.__lt,
__le = Pos.__le,
__eq = Pos.__eq,
})
return pos return pos
end end
function Pos.invalid() return Pos.new(0, 0, 0, 0) end function Pos.invalid() return Pos.new(0, 0, 0, 0) end
function Pos.is(x) function Pos.is(x) return getmetatable(x) == Pos end
if not type(x) == 'table' then return false end
local mt = getmetatable(x)
return mt and mt.__index == Pos
end
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
function Pos.__le(a, b) return a < b or a == b end function Pos.__le(a, b) return a < b or a == b end

View File

@ -12,18 +12,8 @@ end
--- @field stop u.Pos|nil --- @field stop u.Pos|nil
--- @field mode 'v'|'V' --- @field mode 'v'|'V'
local Range = {} local Range = {}
Range.__index = Range
--- @param start u.Pos function Range.__tostring(self)
--- @param stop u.Pos|nil
--- @param mode? 'v'|'V'
--- @return u.Range
function Range.new(start, stop, mode)
if stop ~= nil and stop < start then
start, stop = stop, start
end
local r = { start = start, stop = stop, mode = mode or 'v' }
local function str()
--- @param p u.Pos --- @param p u.Pos
local function posstr(p) local function posstr(p)
if p == nil then if p == nil then
@ -35,24 +25,33 @@ function Range.new(start, stop, mode)
end end
end end
local _1 = posstr(r.start) local _1 = posstr(self.start)
local _2 = posstr(r.stop) local _2 = posstr(self.stop)
return string.format( return string.format(
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}', 'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
r.start.bufnr, self.start.bufnr,
r.mode, self.mode,
_1, _1,
_2 _2
) )
end
--- @param start u.Pos
--- @param stop u.Pos|nil
--- @param mode? 'v'|'V'
--- @return u.Range
function Range.new(start, stop, mode)
if stop ~= nil and stop < start then
start, stop = stop, start
end end
setmetatable(r, { __index = Range, __tostring = str })
local r = { start = start, stop = stop, mode = mode or 'v' }
setmetatable(r, Range)
return r return r
end end
function Range.is(x) function Range.is(x) return getmetatable(x) == Range end
local mt = getmetatable(x)
return mt and mt.__index == Range
end
--- @param lpos string --- @param lpos string
--- @param rpos string --- @param rpos string
@ -299,8 +298,44 @@ function Range:trim_stop()
return r return r
end end
--- @param p u.Pos --- @param x u.Pos | u.Range
function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end function Range:contains(x)
if getmetatable(x) == Pos then
return not self:is_empty() and x >= self.start and x <= self.stop
elseif getmetatable(x) == Range then
return self:contains(x.start) and self:contains(x.stop)
end
return false
end
--- TODO: test
--- @param other u.Range
--- @return u.Range|nil, u.Range|nil
function Range:difference(other)
local outer, inner = self, other
if not outer:contains(inner) then
outer, inner = inner, outer
end
if not outer:contains(inner) then return nil, nil end
local left
if outer.start ~= inner.start then
local stop = inner.start:clone() - 1
left = Range.new(outer.start, stop)
else
left = Range.new(outer.start) -- empty range
end
local right
if inner.stop ~= outer.stop then
local start = inner.stop:clone() + 1
right = Range.new(start, outer.stop)
else
right = Range.new(inner.stop) -- empty range
end
return left, right
end
--- @return string[] --- @return string[]
function Range:lines() function Range:lines()

View File

@ -422,6 +422,30 @@ describe('Range', function()
end) end)
end) end)
it('difference', function()
withbuf({ 'line one', 'and line two' }, function()
local range_outer = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 2, 12), 'v')
local range_inner = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 8), 'v')
assert.are.same(range_outer:text(), 'and line two')
assert.are.same(range_inner:text(), 'line')
local left, right = range_outer:difference(range_inner)
assert.are.same(left:text(), 'and ')
assert.are.same(right:text(), ' two')
left, right = range_inner:difference(range_outer)
assert.are.same(left:text(), 'and ')
assert.are.same(right:text(), ' two')
left, right = range_outer:difference(range_outer)
assert.are.same(left:is_empty(), true)
assert.are.same(left:text(), '')
assert.are.same(right:is_empty(), true)
assert.are.same(right:text(), '')
end)
end)
it('shrink', function() it('shrink', function()
withbuf({ 'line one', 'and line two' }, function() withbuf({ 'line one', 'and line two' }, function()
local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v') local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v')