diff --git a/examples/surround.lua b/examples/surround.lua index a9e9905..533289f 100644 --- a/examples/surround.lua +++ b/examples/surround.lua @@ -1,11 +1,12 @@ local vim_repeat = require 'u.repeat' -local Pos = require 'u.pos' local Range = require 'u.range' local Buffer = require 'u.buffer' local CodeWriter = require 'u.codewriter' local M = {} +local ESC = vim.api.nvim_replace_termcodes('', true, false, true) + local surrounds = { [')'] = { left = '(', right = ')' }, ['('] = { left = '( ', right = ' )' }, @@ -126,7 +127,7 @@ function M.setup() do_surround(range, bounds) -- 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 }) -- Change diff --git a/lua/u/pos.lua b/lua/u/pos.lua index 0331308..cee1de2 100644 --- a/lua/u/pos.lua +++ b/lua/u/pos.lua @@ -12,8 +12,17 @@ end --- @field col number 1-based column index --- @field off number local Pos = {} +Pos.__index = Pos 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 lnum number 1-based --- @param col number 1-based @@ -28,31 +37,13 @@ function Pos.new(bufnr, lnum, col, off) col = col, off = off, } - - 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, - }) + setmetatable(pos, Pos) return pos end function Pos.invalid() return Pos.new(0, 0, 0, 0) end -function Pos.is(x) - if not type(x) == 'table' then return false end - local mt = getmetatable(x) - return mt and mt.__index == Pos -end +function Pos.is(x) return getmetatable(x) == 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.__le(a, b) return a < b or a == b end diff --git a/lua/u/range.lua b/lua/u/range.lua index 337f613..6235a67 100644 --- a/lua/u/range.lua +++ b/lua/u/range.lua @@ -12,6 +12,29 @@ end --- @field stop u.Pos|nil --- @field mode 'v'|'V' local Range = {} +Range.__index = Range +function Range.__tostring(self) + --- @param p u.Pos + local function posstr(p) + if p == nil then + return 'nil' + elseif p.off ~= 0 then + return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off) + else + return string.format('Pos(%d:%d)', p.lnum, p.col) + end + end + + local _1 = posstr(self.start) + local _2 = posstr(self.stop) + return string.format( + 'Range{bufnr=%d, mode=%s, start=%s, stop=%s}', + self.start.bufnr, + self.mode, + _1, + _2 + ) +end --- @param start u.Pos --- @param stop u.Pos|nil @@ -23,36 +46,12 @@ function Range.new(start, stop, mode) end local r = { start = start, stop = stop, mode = mode or 'v' } - local function str() - --- @param p u.Pos - local function posstr(p) - if p == nil then - return 'nil' - elseif p.off ~= 0 then - return string.format('Pos(%d:%d){off=%d}', p.lnum, p.col, p.off) - else - return string.format('Pos(%d:%d)', p.lnum, p.col) - end - end - local _1 = posstr(r.start) - local _2 = posstr(r.stop) - return string.format( - 'Range{bufnr=%d, mode=%s, start=%s, stop=%s}', - r.start.bufnr, - r.mode, - _1, - _2 - ) - end - setmetatable(r, { __index = Range, __tostring = str }) + setmetatable(r, Range) return r end -function Range.is(x) - local mt = getmetatable(x) - return mt and mt.__index == Range -end +function Range.is(x) return getmetatable(x) == Range end --- @param lpos string --- @param rpos string @@ -299,8 +298,44 @@ function Range:trim_stop() return r end ---- @param p u.Pos -function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end +--- @param x u.Pos | u.Range +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[] function Range:lines() diff --git a/spec/range_spec.lua b/spec/range_spec.lua index ec3d3d8..ae44f35 100644 --- a/spec/range_spec.lua +++ b/spec/range_spec.lua @@ -422,6 +422,30 @@ describe('Range', function() 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() withbuf({ 'line one', 'and line two' }, function() local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v')