diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 759ac9b..8cd2fc5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,5 +11,5 @@ jobs: - uses: rhysd/action-setup-vim@v1 with: neovim: true - version: v0.10.1 + version: v0.11 - run: make test diff --git a/.gitignore b/.gitignore index c3a523b..8d65251 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +/.lux/ *.src.rock diff --git a/.luacheckrc b/.luacheckrc new file mode 100644 index 0000000..8317fde --- /dev/null +++ b/.luacheckrc @@ -0,0 +1,2 @@ +-- :vim set ft=lua +globals = { "vim" } diff --git a/Makefile b/Makefile index 386fc81..a05e233 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,8 @@ PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim all: lint test lint: - selene . + lua-language-server --check=lua/u/ --checklevel=Hint + lux check fmt: stylua . diff --git a/README.md b/README.md index 1e86a22..02f4167 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,20 @@ buf:render { ### A note on indices +
+ I love NeoVim. I am coming to love Lua. I don't like 1-based indices; perhaps I am too old. Perhaps I am too steeped in the history of loving the elegance of simple pointer arithmetic. Regardless, the way positions are addressed in NeoVim/Vim is (terrifyingly) mixed. Some methods return 1-based, others accept only 0-based. In order to stay sane, I had to make a choice to store everything in one, uniform representation in this library. I chose (what I humbly think is the only sane way) to stick with the tried-and-true 0-based index scheme. That abstraction leaks into the public API of this library. + +
+ +
+This has changed in v2. After much thought, I realized that: + +1. The 0-based indexing in NeoVim is prevelant in the `:api`, which is designed to be exposed to many languages. As such, it makes sense for this interface to use 0-based indexing. However, many internal Vim functions use 1-based indexing. +2. This is a Lua library (surprise, surprise, duh) - the idioms of the language should take precedence over my preference +3. There were subtle bugs in the code where indices weren't being normalized to 0-based, anyways. Somehow it worked most of the time. + +As such, this library now uses 1-based indexing everywhere, doing the necessary interop conversions when calling `:api` functions. ### 1. Creating a Range @@ -225,8 +238,8 @@ The `Range` utility is the main feature upon which most other things in this lib ```lua local Range = require 'u.range' -local start = Pos.new(0, 0, 0) -- Line 1, first column -local stop = Pos.new(0, 2, 0) -- Line 3, first column +local start = Pos.new(0, 1, 1) -- Line 1, first column +local stop = Pos.new(0, 3, 1) -- Line 3, first column Range.new(start, stop, 'v') -- charwise selection Range.new(start, stop, 'V') -- linewise selection @@ -236,7 +249,7 @@ This is usually not how you want to obtain a `Range`, however. Usually you want ```lua -- get the first line in a buffer: -Range.from_line(0, 0) +Range.from_line(1, 1) -- Text Objects (any text object valid in your configuration is supported): -- get the word the cursor is on: @@ -285,8 +298,8 @@ So far, that's a lot of ways to _get_ a `Range`. But what can you do with a rang local range = ... range:lines() -- get the lines in the range's region range:text() -- get the text (i.e., string) in the range's region -range:line0(0) -- get the first line within this range -range:line0(-1) -- get the last line within this range +range:line(1) -- get the first line within this range +range:line(-1) -- get the last line within this range -- replace with new contents: range:replace { 'replacement line 1', @@ -357,10 +370,10 @@ buf:set_var('...', ...) buf:all() -- returns a Range representing the entire buffer buf:is_empty() -- returns true if the buffer has no text buf:append_line '...' -buf:line0(0) -- returns a Range representing the first line in the buffer -buf:line0(-1) -- returns a Range representing the last line in the buffer -buf:lines(0, 1) -- returns a Range representing the first two lines in the buffer -buf:lines(1, -2) -- returns a Range representing all but the first and last lines of a buffer +buf:line(1) -- returns a Range representing the first line in the buffer +buf:line(-1) -- returns a Range representing the last line in the buffer +buf:lines(1, 2) -- returns a Range representing the first two lines in the buffer +buf:lines(2, -2) -- returns a Range representing all but the first and last lines of a buffer buf:text_object('iw') -- returns a Range representing the text object 'iw' in the give buffer ``` diff --git a/examples/splitjoin.lua b/examples/splitjoin.lua index 6db2db0..772f7a7 100644 --- a/examples/splitjoin.lua +++ b/examples/splitjoin.lua @@ -4,7 +4,7 @@ local Range = require 'u.range' local M = {} ---- @param bracket_range Range +--- @param bracket_range u.Range --- @param left string --- @param right string local function split(bracket_range, left, right) @@ -52,7 +52,7 @@ local function split(bracket_range, left, right) bracket_range:replace(code.lines) end ---- @param bracket_range Range +--- @param bracket_range u.Range --- @param left string --- @param right string local function join(bracket_range, left, right) diff --git a/examples/surround.lua b/examples/surround.lua index e1d4e7b..8ab950b 100644 --- a/examples/surround.lua +++ b/examples/surround.lua @@ -53,7 +53,7 @@ local function prompt_for_bounds() end end ---- @param range Range +--- @param range u.Range --- @param bounds { left: string; right: string } local function do_surround(range, bounds) local left = bounds.left @@ -69,7 +69,7 @@ local function do_surround(range, bounds) range:replace(left .. range:text() .. right) elseif range.mode == 'V' then local buf = Buffer.current() - local cw = CodeWriter.from_line(buf:line0(range.start.lnum):text(), buf.buf) + local cw = CodeWriter.from_line(range.start:line(), buf.buf) -- write the left bound at the current indent level: cw:write(left) @@ -109,10 +109,8 @@ function _G.MySurroundOpFunc(ty) if not vim_repeat.is_repeating() then hl = range:highlight('IncSearch', { priority = 999 }) end local bounds = prompt_for_bounds() - if bounds == nil then - if hl then hl.clear() end - return - end + if hl then hl.clear() end + if bounds == nil then return end do_surround(range, bounds) end @@ -166,10 +164,6 @@ function M.setup() hl_clear() if to == nil then return end - -- Re-fetch the arange, just in case this action is being repeated: - arange = get_fresh_arange() - if arange == nil then return end - if from_c == 't' then -- For tags, we want to replace the inner text, not the tag: local irange = Range.from_text_object('i' .. from_c, { user_defined = true }) @@ -182,7 +176,7 @@ function M.setup() lrange:replace(to.left) else -- replace `from.right` with `to.right`: - local last_line = arange:line0(-1).text() --[[@as string]] + local last_line = arange:line(-1):text() local from_right_match = last_line:match(vim.pesc(from.right) .. '$') if from_right_match then local match_start = arange.stop:clone() @@ -191,7 +185,7 @@ function M.setup() end -- replace `from.left` with `to.left`: - local first_line = arange:line0(0).text() --[[@as string]] + local first_line = arange:line(1):text() local from_left_match = first_line:match('^' .. vim.pesc(from.left)) if from_left_match then local match_end = arange.start:clone() @@ -240,11 +234,11 @@ function M.setup() ) -- delete last line, if it is empty: - local last = buf:line0(final_range.stop.lnum) + local last = buf:line(final_range.stop.lnum) if last:text():match '^%s*$' then last:replace(nil) end -- delete first line, if it is empty: - local first = buf:line0(final_range.start.lnum) + local first = buf:line(final_range.start.lnum) if first:text():match '^%s*$' then first:replace(nil) end else -- trim start: diff --git a/examples/text-objects.lua b/examples/text-objects.lua index 7755b89..4eee399 100644 --- a/examples/text-objects.lua +++ b/examples/text-objects.lua @@ -11,8 +11,7 @@ function M.setup() -- Select current line: utils.define_text_object('a.', function() - local lnum = Pos.from_pos('.').lnum - return Buffer.current():line0(lnum) + return Buffer.current():line(Pos.from_pos('.').lnum) end) -- Select the nearest quote: diff --git a/lua/u/.luarc.json b/lua/u/.luarc.json new file mode 100644 index 0000000..43c1071 --- /dev/null +++ b/lua/u/.luarc.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "diagnostics.globals": ["assert", "vim"], + "runtime.version": "LuaJIT" +} diff --git a/lua/u/buffer.lua b/lua/u/buffer.lua index 41cb645..b25994f 100644 --- a/lua/u/buffer.lua +++ b/lua/u/buffer.lua @@ -1,13 +1,13 @@ local Range = require 'u.range' local Renderer = require 'u.renderer'.Renderer ----@class Buffer +---@class u.Buffer ---@field buf number ----@field private renderer Renderer +---@field private renderer u.Renderer local Buffer = {} ---@param buf? number ----@return Buffer +---@return u.Buffer function Buffer.from_nr(buf) if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end @@ -18,12 +18,12 @@ function Buffer.from_nr(buf) }, { __index = Buffer }) end ----@return Buffer +---@return u.Buffer function Buffer.current() return Buffer.from_nr(0) end ---@param listed boolean ---@param scratch boolean ----@return Buffer +---@return u.Buffer function Buffer.create(listed, scratch) return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch)) end function Buffer:set_tmp_options() @@ -48,7 +48,7 @@ function Buffer:line_count() return vim.api.nvim_buf_line_count(self.buf) end function Buffer:all() return Range.from_buf_text(self.buf) end -function Buffer:is_empty() return self:line_count() == 1 and self:line0(0):text() == '' end +function Buffer:is_empty() return self:line_count() == 1 and self:line(1):text() == '' end ---@param line string function Buffer:append_line(line) @@ -58,8 +58,8 @@ function Buffer:append_line(line) end ---@param num number 0-based line index -function Buffer:line0(num) - if num < 0 then return self:line0(self:line_count() + num) end +function Buffer:line(num) + if num < 0 then num = self:line_count() + num + 1 end return Range.from_line(self.buf, num) end @@ -68,13 +68,14 @@ end function Buffer:lines(start, stop) return Range.from_lines(self.buf, start, stop) end ---@param txt_obj string ----@param opts? { contains_cursor?: boolean; pos?: Pos } +---@param opts? { contains_cursor?: boolean; pos?: u.Pos } function Buffer:text_object(txt_obj, opts) opts = vim.tbl_extend('force', opts or {}, { buf = self.buf }) return Range.from_text_object(txt_obj, opts) end --- @param event string|string[] +--- @diagnostic disable-next-line: undefined-doc-name --- @param opts vim.api.keyset.create_autocmd function Buffer:autocmd(event, opts) vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.buf })) diff --git a/lua/u/codewriter.lua b/lua/u/codewriter.lua index f2489d9..2de0c72 100644 --- a/lua/u/codewriter.lua +++ b/lua/u/codewriter.lua @@ -1,6 +1,6 @@ local Buffer = require 'u.buffer' ----@class CodeWriter +---@class u.CodeWriter ---@field lines string[] ---@field indent_level number ---@field indent_str string @@ -8,7 +8,7 @@ local CodeWriter = {} ---@param indent_level? number ---@param indent_str? string ----@return CodeWriter +---@return u.CodeWriter function CodeWriter.new(indent_level, indent_str) if indent_level == nil then indent_level = 0 end if indent_str == nil then indent_str = ' ' end @@ -22,9 +22,9 @@ function CodeWriter.new(indent_level, indent_str) return cw end ----@param p Pos +---@param p u.Pos function CodeWriter.from_pos(p) - local line = Buffer.from_nr(p.buf):line0(p.lnum):text() + local line = Buffer.from_nr(p.buf):line(p.lnum):text() return CodeWriter.from_line(line, p.buf) end @@ -61,7 +61,7 @@ end ---@param line string function CodeWriter:write(line) self:write_raw(self.indent_str:rep(self.indent_level) .. line) end ----@param f? fun(cw: CodeWriter):any +---@param f? fun(cw: u.CodeWriter):any function CodeWriter:indent(f) local cw = { lines = self.lines, diff --git a/lua/u/logger.lua b/lua/u/logger.lua index c76bf15..22e97a6 100644 --- a/lua/u/logger.lua +++ b/lua/u/logger.lua @@ -7,7 +7,7 @@ function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u -- Logger class -------------------------------------------------------------------------------- ---- @class Logger +--- @class u.Logger --- @field name string --- @field private fd number local Logger = {} diff --git a/lua/u/opkeymap.lua b/lua/u/opkeymap.lua index 18b84ef..fd6d2d3 100644 --- a/lua/u/opkeymap.lua +++ b/lua/u/opkeymap.lua @@ -1,24 +1,17 @@ local Range = require 'u.range' -local vim_repeat = require 'u.repeat' ----@type fun(range: Range): nil|(fun():any) +--- @type fun(range: u.Range): nil|(fun():any) local __U__OpKeymapOpFunc_rhs = nil --- This is the global utility function used for operatorfunc --- in opkeymap ----@type nil|fun(range: Range): fun():any|nil ----@param ty 'line'|'char'|'block' +--- @type nil|fun(range: u.Range): fun():any|nil +--- @param ty 'line'|'char'|'block' -- selene: allow(unused_variable) function __U__OpKeymapOpFunc(ty) if __U__OpKeymapOpFunc_rhs ~= nil then local range = Range.from_op_func(ty) - local repeat_inject = __U__OpKeymapOpFunc_rhs(range) - - vim_repeat.set(function() - vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc' - if repeat_inject ~= nil and type(repeat_inject) == 'function' then repeat_inject() end - vim_repeat.native_repeat() - end) + __U__OpKeymapOpFunc_rhs(range) end end @@ -28,12 +21,17 @@ end --- g@: tells vim to way for a motion, and then call operatorfunc. --- 2. The operatorfunc is set to a lua function that computes the range being operated over, that --- then calls the original passed callback with said range. ----@param mode string|string[] ----@param lhs string ----@param rhs fun(range: Range): nil|(fun():any) This function may return another function, which is called whenever the operator is repeated ----@param opts? vim.keymap.set.Opts +--- @param mode string|string[] +--- @param lhs string +--- @param rhs fun(range: u.Range): nil +--- @diagnostic disable-next-line: undefined-doc-name +--- @param opts? vim.keymap.set.Opts local function opkeymap(mode, lhs, rhs, opts) vim.keymap.set(mode, lhs, function() + -- We don't need to wrap the operation in a repeat, because expr mappings are + -- repeated seamlessly by Vim anyway. In addition, the u.repeat:`.` mapping will + -- set IS_REPEATING to true, so that callbacks can check if they should used cached + -- values. __U__OpKeymapOpFunc_rhs = rhs vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc' return 'g@' diff --git a/lua/u/pos.lua b/lua/u/pos.lua index d8c7a42..4ae4fc1 100644 --- a/lua/u/pos.lua +++ b/lua/u/pos.lua @@ -1,10 +1,10 @@ local MAX_COL = vim.v.maxcol ---@param buf number ----@param lnum number -local function line_text(buf, lnum) return vim.api.nvim_buf_get_lines(buf, lnum, lnum + 1, false)[1] end +---@param lnum number 1-based +local function line_text(buf, lnum) return vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] end ----@class Pos +---@class u.Pos ---@field buf number buffer number ---@field lnum number 1-based line index ---@field col number 1-based column index @@ -13,10 +13,10 @@ local Pos = {} Pos.MAX_COL = MAX_COL ---@param buf? number ----@param lnum number ----@param col number +---@param lnum number 1-based +---@param col number 1-based ---@param off? number ----@return Pos +---@return u.Pos function Pos.new(buf, lnum, col, off) if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end if off == nil then off = 0 end @@ -44,103 +44,114 @@ function Pos.new(buf, lnum, col, off) 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.__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 a.lnum == b.lnum and a.col == b.col end +function Pos.__eq(a, b) return Pos.is(a) and Pos.is(b) and a.buf == b.buf 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 + 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 + return x:next(-y) +end ---@param name string ----@return Pos +---@return u.Pos function Pos.from_pos(name) local p = vim.fn.getpos(name) - local col = p[3] - if col ~= MAX_COL then col = col - 1 end - return Pos.new(p[1], p[2] - 1, col, p[4]) + return Pos.new(p[1], p[2], p[3], p[4]) end +function Pos:is_invalid() return self.buf == 0 and self.lnum == 0 and self.col == 0 and self.off == 0 end + function Pos:clone() return Pos.new(self.buf, self.lnum, self.col, self.off) end ---@return boolean function Pos:is_col_max() return self.col == MAX_COL end ----@return number[] -function Pos:as_vim() return { self.buf, self.lnum, self.col, self.off } end - --- Normalize the position to a real position (take into account vim.v.maxcol). function Pos:as_real() + local maxlen = #line_text(self.buf, self.lnum) local col = self.col - if self:is_col_max() then + if col > maxlen then -- We could use utilities in this file to get the given line, but -- since this is a low-level function, we are going to optimize and -- use the API directly: - col = #line_text(self.buf, self.lnum) - 1 + col = maxlen end return Pos.new(self.buf, self.lnum, col, self.off) end ----@param pos string -function Pos:save_to_pos(pos) - if pos == '.' then - vim.api.nvim_win_set_cursor(0, { self.lnum + 1, self.col }) - return - end +function Pos:as_vim() return { self.buf, self.lnum, self.col, self.off } end - local p = self:as_real() - vim.fn.setpos(pos, { p.buf, p.lnum + 1, p.col + 1, p.off }) -end +---@param pos string +function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.buf, self.lnum, self.col, self.off }) end ---@param mark string function Pos:save_to_mark(mark) local p = self:as_real() - vim.api.nvim_buf_set_mark(p.buf, mark, p.lnum + 1, p.col, {}) + vim.api.nvim_buf_set_mark(p.buf, mark, p.lnum, p.col, {}) end ---@return string function Pos:char() local line = line_text(self.buf, self.lnum) if line == nil then return '' end - return line:sub(self.col + 1, self.col + 1) + return line:sub(self.col, self.col) end +function Pos:line() return line_text(self.buf, self.lnum) end + ---@param dir? -1|1 ---@param must? boolean ----@return Pos|nil +---@return u.Pos|nil function Pos:next(dir, must) if must == nil then must = false end if dir == nil or dir == 1 then -- Next: local num_lines = vim.api.nvim_buf_line_count(self.buf) - local last_line = line_text(self.buf, num_lines - 1) -- buf:line0(-1) - if self.lnum == num_lines - 1 and self.col == (#last_line - 1) then + local last_line = line_text(self.buf, num_lines) + if self.lnum == num_lines and self.col == #last_line then if must then error 'error in Pos:next(): Pos:next() returned nil' end return nil end local col = self.col + 1 local line = self.lnum - local line_max_col = #line_text(self.buf, self.lnum) - 1 + local line_max_col = #line_text(self.buf, self.lnum) if col > line_max_col then - col = 0 + col = 1 line = line + 1 end return Pos.new(self.buf, line, col, self.off) else -- Previous: - if self.col == 0 and self.lnum == 0 then + if self.col == 1 and self.lnum == 1 then if must then error 'error in Pos:next(): Pos:next() returned nil' end return nil end local col = self.col - 1 local line = self.lnum - local prev_line_max_col = #(line_text(self.buf, self.lnum - 1) or '') - 1 - if col < 0 then - col = math.max(prev_line_max_col, 0) + local prev_line_max_col = #(line_text(self.buf, self.lnum - 1) or '') + if col < 1 then + col = math.max(prev_line_max_col, 1) line = line - 1 end return Pos.new(self.buf, line, col, self.off) @@ -155,7 +166,7 @@ function Pos:must_next(dir) end ---@param dir -1|1 ----@param predicate fun(p: Pos): boolean +---@param predicate fun(p: u.Pos): boolean ---@param test_current? boolean function Pos:next_while(dir, predicate, test_current) if test_current and not predicate(self) then return end @@ -169,14 +180,14 @@ function Pos:next_while(dir, predicate, test_current) end ---@param dir -1|1 ----@param predicate string|fun(p: Pos): boolean +---@param predicate string|fun(p: u.Pos): boolean function Pos:find_next(dir, predicate) if type(predicate) == 'string' then local s = predicate predicate = function(p) return s == p:char() end end - ---@type Pos|nil + ---@type u.Pos|nil local curr = self while curr ~= nil do if predicate(curr) then return curr end @@ -187,8 +198,8 @@ end --- Finds the matching bracket/paren for the current position. ---@param max_chars? number|nil ----@param invocations? Pos[] ----@return Pos|nil +---@param invocations? u.Pos[] +---@return u.Pos|nil function Pos:find_match(max_chars, invocations) if invocations == nil then invocations = {} end if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then return nil end @@ -202,11 +213,16 @@ function Pos:find_match(max_chars, invocations) if not is_opener and not is_closer then return nil end local i, _ = vim.iter(is_opener and openers or closers):enumerate():find(function(_, c2) return c == c2 end) + -- Store the character we will be looking for: local c_match = (is_opener and closers or openers)[i] - ---@type Pos|nil + ---@type u.Pos|nil local cur = self - ---@return Pos|nil + --- `adv` is a helper that moves the current position backward or forward, + --- depending on whether we are looking for an opener or a closer. It returns + --- nil if 1) the watch-dog `max_chars` falls bellow 0, or 2) if we have gone + --- beyond the beginning/end of the file. + ---@return u.Pos|nil local function adv() if cur == nil then return nil end @@ -218,7 +234,7 @@ function Pos:find_match(max_chars, invocations) return cur:next(is_opener and 1 or -1) end - -- scan until we find a match: + -- scan until we find `c_match`: cur = adv() while cur ~= nil and cur:char() ~= c_match do cur = adv() diff --git a/lua/u/range.lua b/lua/u/range.lua index f179548..7a9e489 100644 --- a/lua/u/range.lua +++ b/lua/u/range.lua @@ -1,5 +1,4 @@ local Pos = require 'u.pos' -local State = require 'u.state' local orig_on_yank = (vim.hl or vim.highlight).on_yank local on_yank_enabled = true; @@ -8,16 +7,16 @@ local on_yank_enabled = true; return orig_on_yank(opts) end ----@class Range ----@field start Pos ----@field stop Pos|nil +---@class u.Range +---@field start u.Pos +---@field stop u.Pos|nil ---@field mode 'v'|'V' local Range = {} ----@param start Pos ----@param stop Pos|nil +---@param start u.Pos +---@param stop u.Pos|nil ---@param mode? 'v'|'V' ----@return Range +---@return u.Range function Range.new(start, stop, mode) if stop ~= nil and stop < start then start, stop = stop, start @@ -25,7 +24,7 @@ function Range.new(start, stop, mode) local r = { start = start, stop = stop, mode = mode or 'v' } local function str() - ---@param p Pos + ---@param p u.Pos local function posstr(p) if p == nil then return 'nil' @@ -51,7 +50,7 @@ end ---@param lpos string ---@param rpos string ----@return Range +---@return u.Range function Range.from_marks(lpos, rpos) local start = Pos.from_pos(lpos) local stop = Pos.from_pos(rpos) @@ -72,15 +71,17 @@ function Range.from_buf_text(buf) if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end local num_lines = vim.api.nvim_buf_line_count(buf) - local start = Pos.new(buf, 0, 0) - local stop = Pos.new(buf, num_lines - 1, Pos.MAX_COL) + local start = Pos.new(buf, 1, 1) + local stop = Pos.new(buf, num_lines, Pos.MAX_COL) return Range.new(start, stop, 'V') end +-- TODO: make 1-based ---@param buf? number ---@param line number 0-based line index function Range.from_line(buf, line) return Range.from_lines(buf, line, line) end +-- TODO: make 1-based ---@param buf? number ---@param start_line number 0-based line index ---@param stop_line number 0-based line index @@ -90,12 +91,12 @@ function Range.from_lines(buf, start_line, stop_line) local num_lines = vim.api.nvim_buf_line_count(buf) stop_line = num_lines + stop_line end - return Range.new(Pos.new(buf, start_line, 0), Pos.new(buf, stop_line, Pos.MAX_COL), 'V') + return Range.new(Pos.new(buf, start_line + 1, 1), Pos.new(buf, stop_line + 1, Pos.MAX_COL), 'V') end ---@param text_obj string ----@param opts? { buf?: number; contains_cursor?: boolean; pos?: Pos, user_defined?: boolean } ----@return Range|nil +---@param opts? { buf?: number; contains_cursor?: boolean; pos?: u.Pos, user_defined?: boolean } +---@return u.Range|nil function Range.from_text_object(text_obj, opts) opts = opts or {} if opts.buf == nil then opts.buf = vim.api.nvim_get_current_buf() end @@ -108,52 +109,58 @@ function Range.from_text_object(text_obj, opts) local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type) local cursor = Pos.from_pos '.' - -- Yank, then read '[ and '] to know the bounds: - ---@type { start: Pos; stop: Pos } - local positions + --- @type u.Pos + local start + --- @type u.Pos + local stop + vim.api.nvim_buf_call(opts.buf, function() - positions = State.run(0, function(s) - s:track_winview() - s:track_register '"' - s:track_pos '.' - s:track_pos "'[" - s:track_pos "']" + local original_state = { + winview = vim.fn.winsaveview(), + regquote = vim.fn.getreg '"', + posdot = vim.fn.getpos '.', + poslb = vim.fn.getpos "'[", + posrb = vim.fn.getpos "']", + } - if opts.pos ~= nil then opts.pos:save_to_pos '.' end + if opts.pos ~= nil then opts.pos:save_to_pos '.' end - local null_pos = Pos.new(0, 0, 0, 0) - null_pos:save_to_pos "'[" - null_pos:save_to_pos "']" + Pos.invalid():save_to_pos "'[" + Pos.invalid():save_to_pos "']" - local prev_on_yank_enabled = on_yank_enabled - on_yank_enabled = false - vim.cmd { - cmd = 'normal', - bang = not opts.user_defined, - args = { '""y' .. text_obj }, - mods = { silent = true }, - } - on_yank_enabled = prev_on_yank_enabled + local prev_on_yank_enabled = on_yank_enabled + on_yank_enabled = false + vim.cmd { + cmd = 'normal', + bang = not opts.user_defined, + args = { '""y' .. text_obj }, + mods = { silent = true }, + } + on_yank_enabled = prev_on_yank_enabled - local start = Pos.from_pos "'[" - local stop = Pos.from_pos "']" + start = Pos.from_pos "'[" + stop = Pos.from_pos "']" - if - -- I have no idea why, but when yanking `i"`, the stop-mark is - -- placed on the ending quote. For other text-objects, the stop- - -- mark is placed before the closing character. - (is_quote and selection_type == 'i' and stop:char() == obj_type) - -- *Sigh*, this also sometimes happens for `it` as well. - or (text_obj == 'it' and stop:char() == '<') - then - stop = stop:next(-1) or stop - end - return { start = start, stop = stop } - end) + -- Restore original state: + vim.fn.winrestview(original_state.winview) + vim.fn.setreg('"', original_state.regquote) + vim.fn.setpos('.', original_state.posdot) + vim.fn.setpos("'[", original_state.poslb) + vim.fn.setpos("']", original_state.posrb) + + if + -- I have no idea why, but when yanking `i"`, the stop-mark is + -- placed on the ending quote. For other text-objects, the stop- + -- mark is placed before the closing character. + (is_quote and selection_type == 'i' and stop:char() == obj_type) + -- *Sigh*, this also sometimes happens for `it` as well. + or (text_obj == 'it' and stop:char() == '<') + then + stop = stop:next(-1) or stop + end end) - local start = positions.start - local stop = positions.stop - if start == stop and start.lnum == 0 and start.col == 0 and start.off == 0 then return nil end + + if start == stop and start:is_invalid() then return nil end if opts.contains_cursor and not Range.new(start, stop):contains(cursor) then return nil end if is_quote and selection_type == 'a' then @@ -189,11 +196,11 @@ end --- Get range information from command arguments. ---@param args unknown ----@return Range|nil +---@return u.Range|nil function Range.from_cmd_args(args) ---@type 'v'|'V' local mode - ---@type nil|Pos + ---@type nil|u.Pos local start local stop if args.range == 0 then @@ -201,59 +208,42 @@ function Range.from_cmd_args(args) else start = Pos.from_pos "'<" stop = Pos.from_pos "'>" - if stop:is_col_max() then - mode = 'V' - else - mode = 'v' - end + mode = stop:is_col_max() and 'V' or 'v' end return Range.new(start, stop, mode) end --- function Range.find_nearest_brackets() - local a = Range.from_text_object('a<', { contains_cursor = true }) - local b = Range.from_text_object('a[', { contains_cursor = true }) - local c = Range.from_text_object('a(', { contains_cursor = true }) - local d = Range.from_text_object('a{', { contains_cursor = true }) - return Range.smallest { a, b, c, d } + return Range.smallest { + Range.from_text_object('a<', { contains_cursor = true }), + Range.from_text_object('a[', { contains_cursor = true }), + Range.from_text_object('a(', { contains_cursor = true }), + Range.from_text_object('a{', { contains_cursor = true }), + } end function Range.find_nearest_quotes() - local a = Range.from_text_object([[a']], { contains_cursor = true }) - if a ~= nil and a:is_empty() then a = nil end - local b = Range.from_text_object([[a"]], { contains_cursor = true }) - if b ~= nil and b:is_empty() then b = nil end - local c = Range.from_text_object([[a`]], { contains_cursor = true }) - if c ~= nil and c:is_empty() then c = nil end - return Range.smallest { a, b, c } + return Range.smallest { + Range.from_text_object([[a']], { contains_cursor = true }), + Range.from_text_object([[a"]], { contains_cursor = true }), + Range.from_text_object([[a`]], { contains_cursor = true }), + } end ----@param ranges (Range|nil)[] +---@param ranges (u.Range|nil)[] function Range.smallest(ranges) - ---@type Range[] - local new_ranges = {} - for _, r in pairs(ranges) do - if r ~= nil then table.insert(new_ranges, r) end - end - ranges = new_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 max_start = ranges[1].start - local min_stop = ranges[1].stop - local result = ranges[1] - + local smallest = ranges[1] for _, r in ipairs(ranges) do local start, stop = r.start, r.stop - if start > max_start and stop < min_stop then - max_start = start - min_stop = stop - result = r - end + if start > smallest.start and stop < smallest.stop then smallest = r end end - - return result + return smallest end function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) end @@ -266,7 +256,7 @@ function Range:to_linewise() local r = self:clone() r.mode = 'V' - r.start.col = 0 + r.start.col = 1 if r.stop ~= nil then r.stop.col = Pos.MAX_COL end return r @@ -298,19 +288,13 @@ function Range:trim_stop() return r end ----@param p Pos +---@param p u.Pos function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end ---@return string[] function Range:lines() if self:is_empty() then return {} end - - local lines = {} - for i = 0, self.stop.lnum - self.start.lnum do - local line = self:line0(i) - if line ~= nil then table.insert(lines, line.text()) end - end - return lines + return vim.fn.getregion(self.start:as_vim(), self.stop:as_vim(), { type = self.mode }) end ---@return string @@ -321,34 +305,17 @@ function Range:text() return vim.fn.join(self:lines(), '\n') end 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():Range; text: fun():string }|nil -function Range:line0(l) - if l < 0 then return self:line0(self:line_count() + l) end +---@return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil +function Range:line(l) + if l < 0 then l = self:line_count() + l + 1 end if l > self:line_count() then return end - local line = vim.api.nvim_buf_get_lines(self.start.buf, self.start.lnum + l, self.start.lnum + l + 1, false)[1] - if line == nil then return end + local line_indices = vim.fn.getregionpos(self.start:as_vim(), self.stop:as_vim(), { type = self.mode }) + local line_bounds = line_indices[l] - local start = 0 - local stop = #line - 1 - if l == 0 then start = self.start.col end - if l == self.stop.lnum - self.start.lnum then stop = self.stop.col end - if stop == Pos.MAX_COL then stop = #line - 1 end - local lnum = self.start.lnum + l - - return { - line = line, - idx0 = { start = start, stop = stop }, - lnum = lnum, - range = function() - return Range.new( - Pos.new(self.start.buf, lnum, start, self.start.off), - Pos.new(self.start.buf, lnum, stop, self.stop.off), - 'v' - ) - end, - text = function() return line:sub(start + 1, stop + 1) end, - } + local start = Pos.new(unpack(line_bounds[1])) + local stop = Pos.new(unpack(line_bounds[2])) + return Range.new(start, stop) end ---@param replacement nil|string|string[] @@ -357,51 +324,55 @@ function Range:replace(replacement) if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end local buf = self.start.buf - -- convert to start-inclusive, stop-exclusive coordinates: - local start_lnum, stop_lnum = self.start.lnum, (self.stop and self.stop.lnum or self.start.lnum) + 1 - local start_col, stop_col = self.start.col, (self.stop and self.stop.col or self.start.col) + 1 - local replace_type = (self:is_empty() and 'insert') or (self.mode == 'v' and 'region') or 'lines' - ---@param alnum number - ---@param acol number - ---@param blnum number - ---@param bcol number - local function set_text(alnum, acol, blnum, bcol, repl) - -- row indices are end-inclusive, and column indices are end-exclusive. - vim.api.nvim_buf_set_text(buf, alnum, acol, blnum, bcol, repl) - + local function update_stop_non_linewise() local new_last_line_num = self.start.lnum + #replacement - 1 local new_last_col = #(replacement[#replacement] or '') - if new_last_line_num == start_lnum then new_last_col = new_last_col + start_col - 1 end - + if new_last_line_num == self.start.lnum then new_last_col = new_last_col + self.start.col - 1 end self.stop = Pos.new(buf, new_last_line_num, new_last_col) end - - ---@param alnum number - ---@param blnum number - local function set_lines(alnum, blnum, repl) - -- indexing is zero-based, end-exclusive - vim.api.nvim_buf_set_lines(buf, alnum, blnum, false, repl) - - if #repl == 0 then + local function update_stop_linewise() + if #replacement == 0 then self.stop = nil else - local new_last_line_num = start_lnum + #replacement - 1 - self.stop = Pos.new(self.start.buf, new_last_line_num, Pos.MAX_COL, self.stop.off) + local new_last_line_num = self.start.lnum - 1 + #replacement - 1 + self.stop = Pos.new(buf, new_last_line_num + 1, Pos.MAX_COL, self.stop.off) end self.mode = 'v' end if replace_type == 'insert' then - set_text(start_lnum, start_col, start_lnum, start_col, replacement) + -- To insert text at a given `(row, column)` location, use `start_row = + -- end_row = row` and `start_col = end_col = col`. + vim.api.nvim_buf_set_text( + buf, + self.start.lnum - 1, + self.start.col - 1, + self.start.lnum - 1, + self.start.col - 1, + replacement + ) + update_stop_non_linewise() elseif replace_type == 'region' then -- Fixup the bounds: - local last_line = vim.api.nvim_buf_get_lines(buf, stop_lnum - 1, stop_lnum, false)[1] or '' - local max_col = #last_line - set_text(start_lnum, start_col, stop_lnum - 1, math.min(stop_col, max_col), replacement) + local max_col = #self.stop:line() + + -- Indexing is zero-based. Row indices are end-inclusive, and column indices + -- are end-exclusive. + vim.api.nvim_buf_set_text( + buf, + self.start.lnum - 1, + self.start.col - 1, + self.stop.lnum - 1, + math.min(self.stop.col, max_col), + replacement + ) + update_stop_non_linewise() elseif replace_type == 'lines' then - set_lines(start_lnum, stop_lnum, replacement) + -- Indexing is zero-based, end-exclusive. + vim.api.nvim_buf_set_lines(buf, self.start.lnum - 1, self.stop.lnum, true, replacement) + update_stop_linewise() else error 'unreachable' end @@ -461,21 +432,11 @@ function Range:set_visual_selection() if vim.api.nvim_get_current_buf() ~= self.start.buf then vim.api.nvim_set_current_buf(self.start.buf) end - State.run(self.start.buf, function(s) - s:track_mark 'a' - s:track_mark 'b' - - self.start:save_to_mark 'a' - self.stop:save_to_mark 'b' - local mode = self.mode - - local normal_cmd_args = '' - if vim.api.nvim_get_mode().mode == 'n' then normal_cmd_args = normal_cmd_args .. mode end - normal_cmd_args = normal_cmd_args .. '`ao`b' - vim.cmd { cmd = 'normal', args = { normal_cmd_args }, bang = true } - - return nil - end) + local curr_mode = vim.fn.mode() + if curr_mode ~= self.mode then vim.fn.feedkeys(self.mode, 'x') end + self.start:save_to_pos '.' + vim.fn.feedkeys('o', 'x') + self.stop:save_to_pos '.' end ---@param group string @@ -490,33 +451,31 @@ function Range:highlight(group, opts) if not opts.on_macro and in_macro then return { clear = function() end } end local ns = vim.api.nvim_create_namespace '' - State.run(self.start.buf, function(s) - if not in_macro then s:track_winview() end - (vim.hl or vim.highlight).range( - self.start.buf, - ns, - group, - { self.start.lnum, self.start.col }, - { self.stop.lnum, self.stop.col }, - { - inclusive = true, - priority = opts.priority, - regtype = self.mode, - } - ) - - return nil - end) + local winview = vim.fn.winsaveview(); + (vim.hl or vim.highlight).range( + self.start.buf, + ns, + group, + { self.start.lnum - 1, self.start.col - 1 }, + { self.stop.lnum - 1, self.stop.col - 1 }, + { + inclusive = true, + priority = opts.priority, + timeout = opts.timeout, + regtype = self.mode, + } + ) + if not in_macro then vim.fn.winrestview(winview) end vim.cmd.redraw() - local function clear() - vim.api.nvim_buf_clear_namespace(self.start.buf, ns, self.start.lnum, self.stop.lnum + 1) - vim.cmd.redraw() - end - if opts.timeout ~= nil then vim.defer_fn(clear, opts.timeout) end - - return { ns = ns, clear = clear } + return { + ns = ns, + clear = function() + vim.api.nvim_buf_clear_namespace(self.start.buf, ns, self.start.lnum - 1, self.stop.lnum) + vim.cmd.redraw() + end, + } end return Range diff --git a/lua/u/renderer.lua b/lua/u/renderer.lua index 3f71a8f..36c4980 100644 --- a/lua/u/renderer.lua +++ b/lua/u/renderer.lua @@ -21,7 +21,7 @@ end -- Renderer {{{ --- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any } ---- @class Renderer +--- @class u.Renderer --- @field bufnr number --- @field ns number --- @field changedtick number @@ -377,7 +377,7 @@ end -- }}} -- }}} -- TreeBuilder {{{ ---- @class TreeBuilder +--- @class u.TreeBuilder --- @field private nodes Node[] local TreeBuilder = {} TreeBuilder.__index = TreeBuilder @@ -389,7 +389,7 @@ function TreeBuilder.new() end --- @param nodes Tree ---- @return TreeBuilder +--- @return u.TreeBuilder function TreeBuilder:put(nodes) table.insert(self.nodes, nodes) return self @@ -398,7 +398,7 @@ end --- @param name string --- @param attributes? table --- @param children? Node | Node[] ---- @return TreeBuilder +--- @return u.TreeBuilder function TreeBuilder:put_h(name, attributes, children) local tag = M.h(name, attributes, children) table.insert(self.nodes, tag) @@ -406,7 +406,7 @@ function TreeBuilder:put_h(name, attributes, children) end --- @param fn fun(TreeBuilder): any ---- @return TreeBuilder +--- @return u.TreeBuilder function TreeBuilder:nest(fn) local nested_writer = TreeBuilder.new() fn(nested_writer) diff --git a/lua/u/repeat.lua b/lua/u/repeat.lua index 06a50fe..71755c0 100644 --- a/lua/u/repeat.lua +++ b/lua/u/repeat.lua @@ -1,61 +1,39 @@ local M = {} -local function _normal(cmd) vim.cmd { cmd = 'normal', args = { cmd }, bang = true } end +local IS_REPEATING = false +--- @type function +local REPEAT_ACTION = nil -M.native_repeat = function() _normal '.' end -M.native_undo = function() _normal 'u' end +local function is_repeatable_last_mutator() return vim.b.changedtick <= (vim.b.my_changedtick or 0) end -local function update_ts() vim.b.tt_changedtick = vim.b.changedtick end - ----@param cmd? string|fun():unknown -function M.set(cmd) - update_ts() - if cmd ~= nil then vim.b.tt_repeatcmd = cmd end +--- @param f fun() +function M.run_repeatable(f) + REPEAT_ACTION = f + REPEAT_ACTION() + vim.b.my_changedtick = vim.b.changedtick end -local function tt_was_last_repeatable() - local ts, tt_ts = vim.b.changedtick, vim.b.tt_changedtick - return tt_ts ~= nil and ts <= tt_ts -end - ----@generic T ----@param cmd string|fun():T ----@return T -function M.run(cmd) - M.set(cmd) - local result = cmd() - update_ts() - return result -end - -function M.do_repeat() - local tt_cmd = vim.b.tt_repeatcmd - if not tt_was_last_repeatable() or (type(tt_cmd) ~= 'function' and type(tt_cmd) ~= 'string') then - return M.native_repeat() - end - - -- execute the cached command: - local count = vim.api.nvim_get_vvar 'count1' - if type(tt_cmd) == 'string' then - _normal(count .. tt_cmd --[[@as string]]) - else - local last_return - for _ = 1, count do - last_return = M.run(tt_cmd --[[@as fun():any]]) - end - return last_return - end -end - -function M.undo() - local tt_was_last_repeatable_before_undo = tt_was_last_repeatable() - M.native_undo() - if tt_was_last_repeatable_before_undo then update_ts() end -end +function M.is_repeating() return IS_REPEATING end function M.setup() - vim.keymap.set('n', '.', M.do_repeat) - vim.keymap.set('n', 'u', M.undo) + vim.keymap.set('n', '.', function() + IS_REPEATING = true + for _ = 1, vim.v.count1 do + if is_repeatable_last_mutator() and type(REPEAT_ACTION) == 'function' then + M.run_repeatable(REPEAT_ACTION) + else + vim.cmd { cmd = 'normal', args = { '.' }, bang = true } + end + end + IS_REPEATING = false + end) + vim.keymap.set('n', 'u', function() + local was_repeatable_last_mutator = is_repeatable_last_mutator() + for _ = 1, vim.v.count1 do + vim.cmd { cmd = 'normal', args = { 'u' }, bang = true } + end + if was_repeatable_last_mutator then vim.b.my_changedtick = vim.b.changedtick end + end) end return M diff --git a/lua/u/state.lua b/lua/u/state.lua deleted file mode 100644 index f71e182..0000000 --- a/lua/u/state.lua +++ /dev/null @@ -1,90 +0,0 @@ ----@class State ----@field buf number ----@field registers table ----@field marks table ----@field positions table ----@field keymaps { mode: string; lhs: any, rhs: any, buffer?: number }[] ----@field global_options table ----@field win_view vim.fn.winsaveview.ret|nil -local State = {} - ----@param buf number ----@return State -function State.new(buf) - if buf == 0 then buf = vim.api.nvim_get_current_buf() end - local s = { buf = buf, registers = {}, marks = {}, positions = {}, keymaps = {}, global_options = {} } - setmetatable(s, { __index = State }) - return s -end - ----@generic T ----@param buf number ----@param f fun(s: State):T ----@return T -function State.run(buf, f) - local s = State.new(buf) - local ok, result = pcall(f, s) - s:restore() - if not ok then error(result) end - return result -end - ----@param buf number ----@param f fun(s: State, callback: fun(): any):any ----@param callback fun():any -function State.run_async(buf, f, callback) - local s = State.new(buf) - f(s, function() - s:restore() - callback() - end) -end - -function State:track_keymap(mode, lhs) - local old = - -- Look up the mapping in buffer-local maps: - vim.iter(vim.api.nvim_buf_get_keymap(self.buf, mode)):find(function(map) return map.lhs == lhs end) - -- Look up the mapping in global maps: - or vim.iter(vim.api.nvim_get_keymap(mode)):find(function(map) return map.lhs == lhs end) - - -- Did we find a mapping? - if old == nil then return end - - -- Track it: - table.insert(self.keymaps, { mode = mode, lhs = lhs, rhs = old.rhs or old.callback, buffer = old.buffer }) -end - ----@param reg string -function State:track_register(reg) self.registers[reg] = vim.fn.getreg(reg) end - ----@param mark string -function State:track_mark(mark) self.marks[mark] = vim.api.nvim_buf_get_mark(self.buf, mark) end - ----@param pos string -function State:track_pos(pos) self.positions[pos] = vim.fn.getpos(pos) end - ----@param nm string -function State:track_global_option(nm) self.global_options[nm] = vim.go[nm] end - -function State:track_winview() self.win_view = vim.fn.winsaveview() end - -function State:restore() - for reg, val in pairs(self.registers) do - vim.fn.setreg(reg, val) - end - for mark, val in pairs(self.marks) do - vim.api.nvim_buf_set_mark(self.buf, mark, val[1], val[2], {}) - end - for pos, val in pairs(self.positions) do - vim.fn.setpos(pos, val) - end - for _, map in ipairs(self.keymaps) do - vim.keymap.set(map.mode, map.lhs, map.rhs, { buffer = map.buffer }) - end - for nm, val in pairs(self.global_options) do - vim.go[nm] = val - end - if self.win_view ~= nil then vim.fn.winrestview(self.win_view) end -end - -return State diff --git a/lua/u/tracker.lua b/lua/u/tracker.lua index d49c4d5..f4c3563 100644 --- a/lua/u/tracker.lua +++ b/lua/u/tracker.lua @@ -6,7 +6,7 @@ M.debug = false -- class Signal -------------------------------------------------------------------------------- ---- @class Signal +--- @class u.Signal --- @field name? string --- @field private changing boolean --- @field private value any @@ -18,7 +18,7 @@ Signal.__index = Signal --- @param value any --- @param name? string ---- @return Signal +--- @return u.Signal function Signal:new(value, name) local obj = setmetatable({ name = name, @@ -83,7 +83,7 @@ function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end --- @generic U --- @param fn fun(value: T): U ---- @return Signal -- +--- @return u.Signal -- function Signal:map(fn) local mapped_signal = M.create_memo(function() local value = self:get() @@ -92,13 +92,13 @@ function Signal:map(fn) return mapped_signal end ---- @return Signal +--- @return u.Signal function Signal:clone() return self:map(function(x) return x end) end --- @param fn fun(value: T): boolean ---- @return Signal -- +--- @return u.Signal -- function Signal:filter(fn) local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil) local unsubscribe_from_self = self:subscribe(function(value) @@ -109,7 +109,7 @@ function Signal:filter(fn) end --- @param ms number ---- @return Signal -- +--- @return u.Signal -- function Signal:debounce(ms) local function set_timeout(timeout, callback) local timer = (vim.uv or vim.loop).new_timer() @@ -123,15 +123,15 @@ function Signal:debounce(ms) local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil) - --- @type { - -- queued: { value: T, ts: number }[] - -- timer?: uv_timer_t - -- } + --- @diagnostic disable-next-line: undefined-doc-name + --- @type { queued: { value: T, ts: number }[]; timer?: uv_timer_t; } local state = { queued = {}, timer = nil } local function clear_timeout() if state.timer == nil then return end pcall(function() + --- @diagnostic disable-next-line: undefined-field state.timer:stop() + --- @diagnostic disable-next-line: undefined-field state.timer:close() end) state.timer = nil @@ -198,13 +198,13 @@ end CURRENT_CONTEXT = nil ---- @class ExecutionContext ---- @field signals table +--- @class u.ExecutionContext +--- @field signals table local ExecutionContext = {} M.ExecutionContext = ExecutionContext ExecutionContext.__index = ExecutionContext ---- @return ExecutionContext +--- @return u.ExecutionContext function ExecutionContext:new() return setmetatable({ signals = {}, @@ -215,7 +215,7 @@ end function ExecutionContext.current() return CURRENT_CONTEXT end --- @param fn function ---- @param ctx ExecutionContext +--- @param ctx u.ExecutionContext function ExecutionContext:run(fn, ctx) local oldCtx = CURRENT_CONTEXT CURRENT_CONTEXT = ctx @@ -258,14 +258,14 @@ end --- @param value any --- @param name? string ---- @return Signal +--- @return u.Signal function M.create_signal(value, name) return Signal:new(value, name) end --- @param fn function --- @param name? string ---- @return Signal +--- @return u.Signal function M.create_memo(fn, name) - --- @type Signal + --- @type u.Signal local result local unsubscribe = M.create_effect(function() local value = fn() diff --git a/lua/u/utils.lua b/lua/u/utils.lua index f0f4242..7a6332d 100644 --- a/lua/u/utils.lua +++ b/lua/u/utils.lua @@ -6,7 +6,7 @@ local M = {} ---@alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string } ---@alias KeyMaps table } ----@alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: Range|nil } +---@alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: u.Range|nil } --- @generic T --- @param x `T` @@ -50,7 +50,7 @@ function M.ucmd(name, cmd, opts) end ---@param key_seq string ----@param fn fun(key_seq: string):Range|Pos|nil +---@param fn fun(key_seq: string):u.Range|u.Pos|nil ---@param opts? { buffer: number|nil } function M.define_text_object(key_seq, fn, opts) local Range = require 'u.range' @@ -64,7 +64,7 @@ function M.define_text_object(key_seq, fn, opts) if Range.is(range_or_pos) and range_or_pos:is_empty() then range_or_pos = range_or_pos.start end if Range.is(range_or_pos) then - local range = range_or_pos --[[@as Range]] + local range = range_or_pos --[[@as u.Range]] range:set_visual_selection() else vim.cmd { cmd = 'normal', args = { '' }, bang = true } @@ -73,8 +73,6 @@ function M.define_text_object(key_seq, fn, opts) vim.keymap.set({ 'x' }, key_seq, handle_visual, opts and { buffer = opts.buffer } or nil) local function handle_normal() - local State = require 'u.state' - -- enter visual mode: vim.cmd { cmd = 'normal', args = { 'v' }, bang = true } @@ -85,40 +83,21 @@ function M.define_text_object(key_seq, fn, opts) if Range.is(range_or_pos) then range_or_pos:set_visual_selection() elseif Pos.is(range_or_pos) then - local p = range_or_pos --[[@as Pos]] - State.run(0, function(s) - s:track_global_option 'eventignore' - vim.go.eventignore = 'all' + local p = range_or_pos --[[@as u.Pos]] - -- insert a single space, so we can select it: - vim.api.nvim_buf_set_text(0, p.lnum, p.col, p.lnum, p.col, { ' ' }) - -- select the space: - Range.new(p, p, 'v'):set_visual_selection() - end) + local original_eventignore = vim.go.eventignore + vim.go.eventignore = 'all' + -- insert a single space, so we can select it: + vim.api.nvim_buf_set_text(0, p.lnum, p.col, p.lnum, p.col, { ' ' }) + vim.go.eventignore = original_eventignore + + -- select the space: + Range.new(p, p, 'v'):set_visual_selection() end end vim.keymap.set({ 'o' }, key_seq, handle_normal, opts and { buffer = opts.buffer } or nil) end ----@type fun(): nil|(fun():any) -local __U__RepeatableOpFunc_rhs = nil - ---- This is the global utility function used for operatorfunc ---- in repeatablemap ----@type nil|fun(range: Range): fun():any|nil --- selene: allow(unused_variable) -function __U__RepeatableOpFunc() - if __U__RepeatableOpFunc_rhs ~= nil then __U__RepeatableOpFunc_rhs() end -end - -function M.repeatablemap(mode, lhs, rhs, opts) - vim.keymap.set(mode, lhs, function() - __U__RepeatableOpFunc_rhs = rhs - vim.o.operatorfunc = 'v:lua.__U__RepeatableOpFunc' - return 'g@ ' - end, vim.tbl_extend('force', opts or {}, { expr = true })) -end - function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end return M diff --git a/lux.toml b/lux.toml new file mode 100644 index 0000000..45d42c6 --- /dev/null +++ b/lux.toml @@ -0,0 +1,19 @@ +package = "u.nvim" +version = "0.1.0" +lua = ">=5.1" + +[description] +summary = "" +maintainer = "jrop" +labels = [ "library", "neovim", "neovim-plugin", "range", "utility" ] + + +[dependencies] +# Add your dependencies here +# `busted = ">=2.0"` + +[run] +args = [ "src/main.lua" ] + +[build] +type = "builtin" \ No newline at end of file diff --git a/spec/pos_spec.lua b/spec/pos_spec.lua index 9526d16..8420311 100644 --- a/spec/pos_spec.lua +++ b/spec/pos_spec.lua @@ -4,12 +4,12 @@ local withbuf = loadfile './spec/withbuf.lua'() describe('Pos', function() it('get a char from a given position', function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() - assert.are.same('a', Pos.new(nil, 0, 0):char()) - assert.are.same('d', Pos.new(nil, 0, 2):char()) - assert.are.same('f', Pos.new(nil, 0, 3):char()) - assert.are.same('a', Pos.new(nil, 2, 0):char()) - assert.are.same('', Pos.new(nil, 3, 0):char()) - assert.are.same('o', Pos.new(nil, 4, 2):char()) + assert.are.same('a', Pos.new(nil, 1, 1):char()) + assert.are.same('d', Pos.new(nil, 1, 3):char()) + assert.are.same('f', Pos.new(nil, 1, 4):char()) + assert.are.same('a', Pos.new(nil, 3, 1):char()) + assert.are.same('', Pos.new(nil, 4, 1):char()) + assert.are.same('o', Pos.new(nil, 5, 3):char()) end) end) @@ -23,47 +23,47 @@ describe('Pos', function() it('get the next position', function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() -- line 1: a => s - assert.are.same(Pos.new(nil, 0, 1), Pos.new(nil, 0, 0):next()) + assert.are.same(Pos.new(nil, 1, 2), Pos.new(nil, 1, 1):next()) -- line 1: d => f - assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 0, 2):next()) + assert.are.same(Pos.new(nil, 1, 4), Pos.new(nil, 1, 3):next()) -- line 1 => 2 - assert.are.same(Pos.new(nil, 1, 0), Pos.new(nil, 0, 3):next()) + assert.are.same(Pos.new(nil, 2, 1), Pos.new(nil, 1, 4):next()) -- line 3 => 4 - assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 2, 0):next()) + assert.are.same(Pos.new(nil, 4, 1), Pos.new(nil, 3, 1):next()) -- line 4 => 5 - assert.are.same(Pos.new(nil, 4, 0), Pos.new(nil, 3, 0):next()) + assert.are.same(Pos.new(nil, 5, 1), Pos.new(nil, 4, 1):next()) -- end returns nil - assert.are.same(nil, Pos.new(nil, 4, 2):next()) + assert.are.same(nil, Pos.new(nil, 5, 3):next()) end) end) it('get the previous position', function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() -- line 1: s => a - assert.are.same(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1):next(-1)) + assert.are.same(Pos.new(nil, 1, 1), Pos.new(nil, 1, 2):next(-1)) -- line 1: f => d - assert.are.same(Pos.new(nil, 0, 2), Pos.new(nil, 0, 3):next(-1)) + assert.are.same(Pos.new(nil, 1, 3), Pos.new(nil, 1, 4):next(-1)) -- line 2 => 1 - assert.are.same(Pos.new(nil, 0, 3), Pos.new(nil, 1, 0):next(-1)) + assert.are.same(Pos.new(nil, 1, 4), Pos.new(nil, 2, 1):next(-1)) -- line 4 => 3 - assert.are.same(Pos.new(nil, 2, 0), Pos.new(nil, 3, 0):next(-1)) + assert.are.same(Pos.new(nil, 3, 1), Pos.new(nil, 4, 1):next(-1)) -- line 5 => 4 - assert.are.same(Pos.new(nil, 3, 0), Pos.new(nil, 4, 0):next(-1)) + assert.are.same(Pos.new(nil, 4, 1), Pos.new(nil, 5, 1):next(-1)) -- beginning returns nil - assert.are.same(nil, Pos.new(nil, 0, 0):next(-1)) + assert.are.same(nil, Pos.new(nil, 1, 1):next(-1)) end) end) it('find matching brackets', function() withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function() -- outer parens are matched: - assert.are.same(Pos.new(nil, 0, 19), Pos.new(nil, 0, 5):find_match()) + assert.are.same(Pos.new(nil, 1, 20), Pos.new(nil, 1, 6):find_match()) -- outer parens are matched (backward): - assert.are.same(Pos.new(nil, 0, 5), Pos.new(nil, 0, 19):find_match()) + assert.are.same(Pos.new(nil, 1, 6), Pos.new(nil, 1, 20):find_match()) -- no potential match returns nil - assert.are.same(nil, Pos.new(nil, 0, 0):find_match()) + assert.are.same(nil, Pos.new(nil, 1, 1):find_match()) -- watchdog expires before an otherwise valid match is found: - assert.are.same(nil, Pos.new(nil, 0, 5):find_match(2)) + assert.are.same(nil, Pos.new(nil, 1, 6):find_match(2)) end) end) end) diff --git a/spec/range_spec.lua b/spec/range_spec.lua index 30a4d11..c257433 100644 --- a/spec/range_spec.lua +++ b/spec/range_spec.lua @@ -28,7 +28,7 @@ describe('Range', function() it('get from positions: v in single line', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v') + local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v') local lines = range:lines() assert.are.same({ 'ine' }, lines) @@ -39,7 +39,7 @@ describe('Range', function() it('get from positions: v across multiple lines', function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v') + local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 3, 5), 'v') local lines = range:lines() assert.are.same({ 'quick brown fox', 'jumps' }, lines) end) @@ -47,7 +47,7 @@ describe('Range', function() it('get from positions: V', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, Pos.MAX_COL), 'V') + local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, Pos.MAX_COL), 'V') local lines = range:lines() assert.are.same({ 'line one' }, lines) @@ -58,7 +58,7 @@ describe('Range', function() it('get from positions: V across multiple lines', function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 0), Pos.new(nil, 2, Pos.MAX_COL), 'V') + local range = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, Pos.MAX_COL), 'V') local lines = range:lines() assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines) end) @@ -88,7 +88,7 @@ describe('Range', function() it('replace within line', function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 8), 'v') + local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 9), 'v') range:replace 'quack' local text = Range.from_line(nil, 1):text() @@ -98,7 +98,7 @@ describe('Range', function() it('delete within line', function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v') + local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 10), 'v') range:replace '' local text = Range.from_line(nil, 1):text() @@ -106,7 +106,7 @@ describe('Range', function() end) withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 1, 9), 'v') + local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 2, 10), 'v') range:replace(nil) local text = Range.from_line(nil, 1):text() @@ -116,7 +116,7 @@ describe('Range', function() it('replace across multiple lines: v', function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() - local range = Range.new(Pos.new(nil, 1, 4), Pos.new(nil, 2, 4), 'v') + local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 3, 5), 'v') range:replace 'plane flew' local lines = Range.from_buf_text():lines() @@ -258,22 +258,18 @@ describe('Range', function() end) end) - it('line0', function() + it('line', function() withbuf({ 'this is a {', 'block', '} here', }, function() - local range = Range.new(Pos.new(0, 0, 5), Pos.new(0, 1, 4), 'v') - local lfirst = range:line0(0) - assert.are.same(5, lfirst.idx0.start) - assert.are.same(10, lfirst.idx0.stop) - assert.are.same(0, lfirst.lnum) - assert.are.same('is a {', lfirst.text()) - assert.are.same('is a {', lfirst.range():text()) - assert.are.same(Pos.new(0, 0, 5), lfirst.range().start) - assert.are.same(Pos.new(0, 0, 10), lfirst.range().stop) - assert.are.same('block', range:line0(1).text()) + local range = Range.new(Pos.new(0, 1, 6), Pos.new(0, 2, 5), 'v') + local lfirst = assert(range:line(1), 'lfirst null') + assert.are.same('is a {', lfirst:text()) + assert.are.same(Pos.new(0, 1, 6), lfirst.start) + assert.are.same(Pos.new(0, 1, 11), lfirst.stop) + assert.are.same('block', range:line(2):text()) end) end) @@ -297,16 +293,16 @@ describe('Range', function() vim.cmd.normal 'v' -- enter visual mode vim.cmd.normal 'l' -- select one character to the right local range = Range.from_vtext() - assert.are.same(range.start, Pos.new(nil, 0, 2)) - assert.are.same(range.stop, Pos.new(nil, 0, 3)) + assert.are.same(range.start, Pos.new(nil, 1, 3)) + assert.are.same(range.stop, Pos.new(nil, 1, 4)) assert.are.same(range.mode, 'v') end) end) it('from_op_func', function() withbuf({ 'line one', 'and line two' }, function() - local a = Pos.new(nil, 0, 0) - local b = Pos.new(nil, 1, 1) + local a = Pos.new(nil, 1, 1) + local b = Pos.new(nil, 2, 2) a:save_to_pos "'[" b:save_to_pos "']" @@ -317,7 +313,7 @@ describe('Range', function() range = Range.from_op_func 'line' assert.are.same(range.start, a) - assert.are.same(range.stop, Pos.new(nil, 1, Pos.MAX_COL)) + assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL)) assert.are.same(range.mode, 'V') end) end) @@ -325,8 +321,8 @@ describe('Range', function() it('from_cmd_args', function() local args = { range = 1 } withbuf({ 'line one', 'and line two' }, function() - local a = Pos.new(nil, 0, 0) - local b = Pos.new(nil, 1, 1) + local a = Pos.new(nil, 1, 1) + local b = Pos.new(nil, 2, 2) a:save_to_pos "'<" b:save_to_pos "'>" @@ -341,25 +337,25 @@ describe('Range', function() withbuf({ [[the "quick" brown fox]] }, function() vim.fn.setpos('.', { 0, 1, 5, 0 }) local range = Range.find_nearest_quotes() - assert.are.same(range.start, Pos.new(nil, 0, 4)) - assert.are.same(range.stop, Pos.new(nil, 0, 10)) + assert.are.same(range.start, Pos.new(nil, 1, 5)) + assert.are.same(range.stop, Pos.new(nil, 1, 11)) end) withbuf({ [[the 'quick' brown fox]] }, function() vim.fn.setpos('.', { 0, 1, 5, 0 }) local range = Range.find_nearest_quotes() - assert.are.same(range.start, Pos.new(nil, 0, 4)) - assert.are.same(range.stop, Pos.new(nil, 0, 10)) + assert.are.same(range.start, Pos.new(nil, 1, 5)) + assert.are.same(range.stop, Pos.new(nil, 1, 11)) end) end) it('smallest', function() - local r1 = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v') - local r2 = Range.new(Pos.new(nil, 0, 2), Pos.new(nil, 0, 4), 'v') - local r3 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 5), 'v') + local r1 = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v') + local r2 = Range.new(Pos.new(nil, 1, 3), Pos.new(nil, 1, 5), 'v') + local r3 = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 6), 'v') local smallest = Range.smallest { r1, r2, r3 } - assert.are.same(smallest.start, Pos.new(nil, 0, 1)) - assert.are.same(smallest.stop, Pos.new(nil, 0, 3)) + assert.are.same(smallest.start, Pos.new(nil, 1, 2)) + assert.are.same(smallest.stop, Pos.new(nil, 1, 4)) end) it('clone', function() @@ -381,9 +377,9 @@ describe('Range', function() it('to_linewise()', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v') + local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 2, 4), 'v') local linewise_range = range:to_linewise() - assert.are.same(linewise_range.start.col, 0) + assert.are.same(linewise_range.start.col, 1) assert.are.same(linewise_range.stop.col, Pos.MAX_COL) assert.are.same(linewise_range.mode, 'V') end) @@ -391,56 +387,56 @@ describe('Range', function() it('is_empty', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 0), nil, 'v') + local range = Range.new(Pos.new(nil, 1, 1), nil, 'v') assert.is_true(range:is_empty()) - local range2 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 1), 'v') + local range2 = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 2), 'v') assert.is_false(range2:is_empty()) end) end) it('trim_start', function() withbuf({ ' line one', 'line two' }, function() - local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v') + local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 10), 'v') local trimmed = range:trim_start() - assert.are.same(trimmed.start, Pos.new(nil, 0, 3)) -- should be after the spaces + assert.are.same(trimmed.start, Pos.new(nil, 1, 4)) -- should be after the spaces end) end) it('trim_stop', function() withbuf({ 'line one ', 'line two' }, function() - local range = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 9), 'v') + local range = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 10), 'v') local trimmed = range:trim_stop() - assert.are.same(trimmed.stop, Pos.new(nil, 0, 7)) -- should be before the spaces + assert.are.same(trimmed.stop, Pos.new(nil, 1, 8)) -- should be before the spaces end) end) it('contains', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v') - local pos = Pos.new(nil, 0, 2) + local range = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v') + local pos = Pos.new(nil, 1, 3) assert.is_true(range:contains(pos)) - pos = Pos.new(nil, 0, 4) -- outside of range + pos = Pos.new(nil, 1, 5) -- outside of range assert.is_false(range:contains(pos)) end) end) it('shrink', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v') + local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v') local shrunk = range:shrink(1) - assert.are.same(shrunk.start, Pos.new(nil, 0, 2)) - assert.are.same(shrunk.stop, Pos.new(nil, 1, 2)) + assert.are.same(shrunk.start, Pos.new(nil, 2, 4)) + assert.are.same(shrunk.stop, Pos.new(nil, 3, 4)) end) end) it('must_shrink', function() withbuf({ 'line one', 'and line two' }, function() - local range = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 1, 3), 'v') + local range = Range.new(Pos.new(nil, 2, 3), Pos.new(nil, 3, 5), 'v') local shrunk = range:must_shrink(1) - assert.are.same(shrunk.start, Pos.new(nil, 0, 2)) - assert.are.same(shrunk.stop, Pos.new(nil, 1, 2)) + 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') end) @@ -451,22 +447,24 @@ describe('Range', function() local range = Range.from_lines(nil, 0, 1) range:set_visual_selection() - assert.are.same(Pos.from_pos 'v', Pos.new(nil, 0, 0)) - assert.are.same(Pos.from_pos '.', Pos.new(nil, 1, 11)) + assert.are.same(Pos.from_pos 'v', Pos.new(nil, 1, 1)) + -- Since the selection is 'V' (instead of 'v'), the end + -- selects one character past the end: + assert.are.same(Pos.from_pos '.', Pos.new(nil, 2, 13)) end) end) it('selections set to past the EOL should not error', function() withbuf({ 'Rg SET NAMES' }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 12), 'v') + local r = Range.new(Pos.new(b, 1, 4), Pos.new(b, 1, 13), 'v') r:replace 'bleh' assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) end) withbuf({ 'Rg SET NAMES' }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 0, 3), Pos.new(b, 0, 11), 'v') + local r = Range.new(Pos.new(b, 1, 4), Pos.new(b, 1, 12), 'v') r:replace 'bleh' assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) end) @@ -475,7 +473,7 @@ describe('Range', function() it('replace updates Range.stop: same line', function() withbuf({ 'The quick brown fox jumps over the lazy dog' }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 0, 4), Pos.new(b, 0, 8), 'v') + 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)) @@ -491,11 +489,12 @@ describe('Range', function() 'over the lazy dog', }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 0, 20), Pos.new(b, 1, 3), 'v') + local r = Range.new(Pos.new(b, 1, 21), Pos.new(b, 2, 4), 'v') 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({ '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)) @@ -511,7 +510,7 @@ describe('Range', function() 'the lazy dog', }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V') + local r = Range.new(Pos.new(b, 2, 1), Pos.new(b, 4, Pos.MAX_COL), 'V') assert.are.same({ 'fox', 'jumps', 'over' }, r:lines()) r:replace { 'bleh1', 'bleh2' } @@ -540,7 +539,7 @@ describe('Range', function() 'the lazy dog', }, function() local b = vim.api.nvim_get_current_buf() - local r = Range.new(Pos.new(b, 1, 0), Pos.new(b, 3, Pos.MAX_COL), 'V') + local r = Range.new(Pos.new(b, 2, 1), Pos.new(b, 4, Pos.MAX_COL), 'V') assert.are.same({ 'fox', 'jumps', 'over' }, r:lines()) r:replace(nil)