From 58e447aad60eb85f0ee3f83c4d7537529e658b17 Mon Sep 17 00:00:00 2001 From: Jonathan Apodaca Date: Sun, 13 Apr 2025 17:06:17 -0600 Subject: [PATCH] Add Range:length and Range:sub --- lua/u/range.lua | 63 +++++++++++++++++++++++++++++++++++++++++---- spec/range_spec.lua | 47 +++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/lua/u/range.lua b/lua/u/range.lua index 6235a67..60997f3 100644 --- a/lua/u/range.lua +++ b/lua/u/range.lua @@ -308,7 +308,6 @@ function Range:contains(x) return false end ---- TODO: test --- @param other u.Range --- @return u.Range|nil, u.Range|nil function Range:difference(other) @@ -337,6 +336,64 @@ function Range:difference(other) return left, right end +function Range:length() + if self:is_empty() then return 0 end + + local line_positions = + vim.fn.getregionpos(self.start:as_real():as_vim(), self.stop:as_real():as_vim(), { type = 'v' }) + + local len = 0 + for linenr, line in ipairs(line_positions) do + if linenr > 1 then len = len + 1 end -- each newline is counted as a char + local line_start_col = line[1][3] + local line_stop_col = line[2][3] + local line_len = line_stop_col - line_start_col + 1 + len = len + line_len + end + return len +end + +--- @param i number 1-based +--- @param j? number 1-based +function Range:sub(i, j) + local line_positions = + vim.fn.getregionpos(self.start:as_real():as_vim(), self.stop:as_real():as_vim(), { type = 'v' }) + + --- @param idx number 1-based + --- @return u.Pos|nil + local function get_pos(idx) + if idx < 0 then return get_pos(self:length() + idx + 1) end + + -- find the position of the first line that contains the i-th character: + local curr_len = 0 + for linenr, line in ipairs(line_positions) do + if linenr > 1 then curr_len = curr_len + 1 end -- each newline is counted as a char + local line_start_col = line[1][3] + local line_stop_col = line[2][3] + local line_len = line_stop_col - line_start_col + 1 + + if curr_len + line_len >= idx then + return Pos.new(self.start.bufnr, line[1][2], line_start_col + (idx - curr_len) - 1) + end + curr_len = curr_len + line_len + end + end + + local start = get_pos(i) + if not start then + -- start is inalid, so return an empty range: + return Range.new(self.start, nil, self.mode) + end + + local stop + if j then stop = get_pos(j) end + if not stop then + -- stop is inalid, so return an empty range: + return Range.new(start, nil, self.mode) + end + return Range.new(start, stop, 'v') +end + --- @return string[] function Range:lines() if self:is_empty() then return {} end @@ -346,10 +403,6 @@ end --- @return string function Range:text() return vim.fn.join(self:lines(), '\n') end ---- @param i number 1-based ---- @param j? number 1-based -function Range:sub(i, j) return self:text():sub(i, j) end - --- @param l number --- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil function Range:line(l) diff --git a/spec/range_spec.lua b/spec/range_spec.lua index ae44f35..f30dd40 100644 --- a/spec/range_spec.lua +++ b/spec/range_spec.lua @@ -446,6 +446,28 @@ describe('Range', function() end) end) + it('length', function() + withbuf({ 'line one', 'and line two' }, function() + local range = Range.new(Pos.new(nil, 2, 4), Pos.new(nil, 2, 9), 'v') + assert.are.same(range:length(), #range:text()) + + range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 9), 'v') + assert.are.same(range:length(), #range:text()) + end) + end) + + it('sub', function() + withbuf({ 'line one', 'and line two' }, function() + local range = Range.new(Pos.new(nil, 2, 4), Pos.new(nil, 2, 9), 'v') + assert.are.same(range:text(), ' line ') + assert.are.same(range:sub(1, -1):text(), ' line ') + assert.are.same(range:sub(2, -2):text(), 'line') + assert.are.same(range:sub(1, 5):text(), ' line') + assert.are.same(range:sub(2, 5):text(), 'line') + assert.are.same(range:sub(20, 25):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') @@ -462,7 +484,10 @@ describe('Range', function() assert.are.same(shrunk.start, Pos.new(nil, 2, 4)) assert.are.same(shrunk.stop, Pos.new(nil, 3, 4)) - assert.has.error(function() range:must_shrink(100) end, 'error in Range:must_shrink: Range:shrink() returned nil') + assert.has.error( + function() range:must_shrink(100) end, + 'error in Range:must_shrink: Range:shrink() returned nil' + ) end) end) @@ -500,10 +525,16 @@ describe('Range', function() local r = Range.new(Pos.new(b, 1, 5), Pos.new(b, 1, 9), 'v') r:replace 'bleh1' - assert.are.same({ 'The bleh1 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) + assert.are.same( + { 'The bleh1 brown fox jumps over the lazy dog' }, + vim.api.nvim_buf_get_lines(b, 0, -1, false) + ) r:replace 'bleh2' - assert.are.same({ 'The bleh2 brown fox jumps over the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) + assert.are.same( + { 'The bleh2 brown fox jumps over the lazy dog' }, + vim.api.nvim_buf_get_lines(b, 0, -1, false) + ) end) end) @@ -517,11 +548,17 @@ describe('Range', function() assert.are.same({ 'jumps', 'over' }, r:lines()) r:replace 'bleh1' - assert.are.same({ 'The quick brown fox bleh1 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) + assert.are.same( + { 'The quick brown fox bleh1 the lazy dog' }, + vim.api.nvim_buf_get_lines(b, 0, -1, false) + ) assert.are.same({ 'bleh1' }, r:lines()) r:replace 'blehGoo2' - assert.are.same({ 'The quick brown fox blehGoo2 the lazy dog' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) + assert.are.same( + { 'The quick brown fox blehGoo2 the lazy dog' }, + vim.api.nvim_buf_get_lines(b, 0, -1, false) + ) end) end)