diff --git a/lua/u/pos.lua b/lua/u/pos.lua index cee1de2..2a3d251 100644 --- a/lua/u/pos.lua +++ b/lua/u/pos.lua @@ -43,25 +43,23 @@ end function Pos.invalid() return Pos.new(0, 0, 0, 0) 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 function Pos.__eq(a, b) - return Pos.is(a) and Pos.is(b) and a.bufnr == b.bufnr and a.lnum == b.lnum and a.col == b.col + return getmetatable(a) == Pos and getmetatable(b) == Pos and a.bufnr == b.bufnr and a.lnum == b.lnum and a.col == b.col end function Pos.__add(x, y) if type(x) == 'number' then x, y = y, x end - if not Pos.is(x) or type(y) ~= 'number' then return nil end + if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end return x:next(y) end function Pos.__sub(x, y) if type(x) == 'number' then x, y = y, x end - if not Pos.is(x) or type(y) ~= 'number' then return nil end + if getmetatable(x) ~= Pos or type(y) ~= 'number' then return nil end return x:next(-y) end diff --git a/lua/u/range.lua b/lua/u/range.lua index 60997f3..9b28cb7 100644 --- a/lua/u/range.lua +++ b/lua/u/range.lua @@ -1,5 +1,10 @@ +--- @module +--- @brief Utilities for manipulating ranges of text. + local Pos = require 'u.pos' +-- Certain functions in the Range class yank text. In order to prevent unwanted +-- highlighting, we intercept and discard some calls to the `on_yank` callback. local orig_on_yank = (vim.hl or vim.highlight).on_yank local on_yank_enabled = true; ((vim.hl or vim.highlight) --[[@as any]]).on_yank = function(opts) @@ -36,6 +41,10 @@ function Range.__tostring(self) ) end +-------------------------------------------------------------------------------- +-- Range constructors: +-------------------------------------------------------------------------------- + --- @param start u.Pos --- @param stop u.Pos|nil --- @param mode? 'v'|'V' @@ -51,7 +60,20 @@ function Range.new(start, stop, mode) return r end -function Range.is(x) return getmetatable(x) == Range end +--- @param ranges (u.Range|nil)[] +function Range.smallest(ranges) + --- @type u.Range[] + ranges = vim.iter(ranges):filter(function(r) return r ~= nil and not r:is_empty() end):totable() + if #ranges == 0 then return nil end + + -- find smallest match + local smallest = ranges[1] + for _, r in ipairs(ranges) do + local start, stop = r.start, r.stop + if start > smallest.start and stop < smallest.stop then smallest = r end + end + return smallest +end --- @param lpos string --- @param rpos string @@ -221,7 +243,6 @@ function Range.from_cmd_args(args) return Range.new(start, stop, mode) end ---- function Range.find_nearest_brackets() return Range.smallest { Range.from_motion('a<', { contains_cursor = true }), @@ -239,28 +260,15 @@ function Range.find_nearest_quotes() } end ---- @param ranges (u.Range|nil)[] -function Range.smallest(ranges) - --- @type u.Range[] - ranges = vim.iter(ranges):filter(function(r) return r ~= nil and not r:is_empty() end):totable() - if #ranges == 0 then return nil end - - -- find smallest match - local smallest = ranges[1] - for _, r in ipairs(ranges) do - local start, stop = r.start, r.stop - if start > smallest.start and stop < smallest.stop then smallest = r end - end - return smallest -end +-------------------------------------------------------------------------------- +-- Structural utilities: +-------------------------------------------------------------------------------- function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) end -function Range:line_count() - if self:is_empty() then return 0 end - return self.stop.lnum - self.start.lnum + 1 -end + +function Range:is_empty() return self.stop == nil end function Range:to_linewise() local r = self:clone() @@ -272,32 +280,6 @@ function Range:to_linewise() return r end -function Range:is_empty() return self.stop == nil end - -function Range:trim_start() - if self:is_empty() then return end - - local r = self:clone() - while r.start:char():match '%s' do - local next = r.start:next(1) - if next == nil then break end - r.start = next - end - return r -end - -function Range:trim_stop() - if self:is_empty() then return end - - local r = self:clone() - while r.stop:char():match '%s' do - local next = r.stop:next(-1) - if next == nil then break end - r.stop = next - end - return r -end - --- @param x u.Pos | u.Range function Range:contains(x) if getmetatable(x) == Pos then @@ -336,6 +318,52 @@ function Range:difference(other) return left, right end +--- @param left string +--- @param right string +function Range:save_to_pos(left, right) + if self:is_empty() then + self.start:save_to_pos(left) + self.start:save_to_pos(right) + else + self.start:save_to_pos(left) + self.stop:save_to_pos(right) + end +end + +--- @param left string +--- @param right string +function Range:save_to_marks(left, right) + if self:is_empty() then + self.start:save_to_mark(left) + self.start:save_to_mark(right) + else + self.start:save_to_mark(left) + self.stop:save_to_mark(right) + end +end + +function Range:set_visual_selection() + if self:is_empty() then return end + if vim.api.nvim_get_current_buf() ~= self.start.bufnr then + error 'Range:set_visual_selection() called on a buffer other than the current buffer' + end + + local curr_mode = vim.fn.mode() + if curr_mode ~= self.mode then vim.cmd.normal { args = { self.mode }, bang = true } end + + self.start:save_to_pos '.' + vim.cmd.normal { args = { 'o' }, bang = true } + self.stop:save_to_pos '.' +end + +-------------------------------------------------------------------------------- +-- Range.from_* functions: +-------------------------------------------------------------------------------- + +-------------------------------------------------------------------------------- +-- Text access/manipulation utilities: +-------------------------------------------------------------------------------- + function Range:length() if self:is_empty() then return 0 end @@ -353,6 +381,35 @@ function Range:length() return len end +function Range:line_count() + if self:is_empty() then return 0 end + return self.stop.lnum - self.start.lnum + 1 +end + +function Range:trim_start() + if self:is_empty() then return end + + local r = self:clone() + while r.start:char():match '%s' do + local next = r.start:next(1) + if next == nil then break end + r.start = next + end + return r +end + +function Range:trim_stop() + if self:is_empty() then return end + + local r = self:clone() + while r.stop:char():match '%s' do + local next = r.stop:next(-1) + if next == nil then break end + r.stop = next + end + return r +end + --- @param i number 1-based --- @param j? number 1-based function Range:sub(i, j) @@ -507,44 +564,6 @@ function Range:must_shrink(amount) return shrunk end ---- @param left string ---- @param right string -function Range:save_to_pos(left, right) - if self:is_empty() then - self.start:save_to_pos(left) - self.start:save_to_pos(right) - else - self.start:save_to_pos(left) - self.stop:save_to_pos(right) - end -end - ---- @param left string ---- @param right string -function Range:save_to_marks(left, right) - if self:is_empty() then - self.start:save_to_mark(left) - self.start:save_to_mark(right) - else - self.start:save_to_mark(left) - self.stop:save_to_mark(right) - end -end - -function Range:set_visual_selection() - if self:is_empty() then return end - if vim.api.nvim_get_current_buf() ~= self.start.bufnr then - error 'Range:set_visual_selection() called on a buffer other than the current buffer' - end - - local curr_mode = vim.fn.mode() - if curr_mode ~= self.mode then vim.cmd.normal { args = { self.mode }, bang = true } end - - self.start:save_to_pos '.' - vim.cmd.normal { args = { 'o' }, bang = true } - self.stop:save_to_pos '.' -end - --- @param group string --- @param opts? { timeout?: number, priority?: number, on_macro?: boolean } function Range:highlight(group, opts)