1-based indexing rewrite
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 14s

This commit is contained in:
Jonathan Apodaca 2025-04-11 13:20:46 -06:00
parent d9bb01be8b
commit 0ee6caa7ba
26 changed files with 730 additions and 874 deletions

View File

@ -11,5 +11,6 @@ jobs:
- uses: rhysd/action-setup-vim@v1 - uses: rhysd/action-setup-vim@v1
with: with:
neovim: true neovim: true
version: v0.10.1 version: v0.11.0
arch: 'x86_64'
- run: make test - run: make test

1
.gitignore vendored
View File

@ -1 +1,2 @@
/.lux/
*.src.rock *.src.rock

2
.luacheckrc Normal file
View File

@ -0,0 +1,2 @@
-- :vim set ft=lua
globals = { "vim" }

View File

@ -3,7 +3,8 @@ PLENARY_DIR=~/.local/share/nvim/site/pack/test/opt/plenary.nvim
all: lint test all: lint test
lint: lint:
selene . lua-language-server --check=lua/u/ --checklevel=Hint
lux check
fmt: fmt:
stylua . stylua .

View File

@ -217,7 +217,20 @@ buf:render {
### A note on indices ### A note on indices
<blockquote>
<del>
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. 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.
</del>
</blockquote>
<br />
<b>This has changed in v2</b>. 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 ### 1. Creating a Range
@ -225,8 +238,8 @@ The `Range` utility is the main feature upon which most other things in this lib
```lua ```lua
local Range = require 'u.range' local Range = require 'u.range'
local start = Pos.new(0, 0, 0) -- Line 1, first column local start = Pos.new(0, 1, 1) -- Line 1, first column
local stop = Pos.new(0, 2, 0) -- Line 3, 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') -- charwise selection
Range.new(start, stop, 'V') -- linewise selection Range.new(start, stop, 'V') -- linewise selection
@ -236,15 +249,15 @@ This is usually not how you want to obtain a `Range`, however. Usually you want
```lua ```lua
-- get the first line in a buffer: -- get the first line in a buffer:
Range.from_line(0, 0) Range.from_line(bufnr, 1)
-- Text Objects (any text object valid in your configuration is supported): -- Text Objects (any text object valid in your configuration is supported):
-- get the word the cursor is on: -- get the word the cursor is on:
Range.from_text_object('iw') Range.from_txtobj('iw')
-- get the WORD the cursor is on: -- get the WORD the cursor is on:
Range.from_text_object('iW') Range.from_txtobj('iW')
-- get the "..." the cursor is within: -- get the "..." the cursor is within:
Range.from_text_object('a"') Range.from_txtobj('a"')
-- Get the currently visually selected text: -- Get the currently visually selected text:
-- NOTE: this does NOT work within certain contexts; more specialized utilities are more appropriate in certain circumstances -- NOTE: this does NOT work within certain contexts; more specialized utilities are more appropriate in certain circumstances
@ -254,7 +267,7 @@ Range.from_vtext()
-- Get the operated on text obtained from a motion: -- Get the operated on text obtained from a motion:
-- (HINT: use the opkeymap utility to make this less verbose) -- (HINT: use the opkeymap utility to make this less verbose)
-- --
---@param ty 'char'|'line'|'block' --- @param ty 'char'|'line'|'block'
function MyOpFunc(ty) function MyOpFunc(ty)
local range = Range.from_op_func(ty) local range = Range.from_op_func(ty)
-- do something with the range -- do something with the range
@ -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 = ... local range = ...
range:lines() -- get the lines in the range's region range:lines() -- get the lines in the range's region
range:text() -- get the text (i.e., string) 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:line(1) -- get the first line within this range
range:line0(-1) -- get the last line within this range range:line(-1) -- get the last line within this range
-- replace with new contents: -- replace with new contents:
range:replace { range:replace {
'replacement line 1', 'replacement line 1',
@ -333,11 +346,11 @@ Simply by returning a `Range` or a `Pos`, you can easily and quickly define
your own text objects: your own text objects:
```lua ```lua
local utils = require 'u.utils' local txtobj = require 'u.txtobj'
local Range = require 'u.range' local Range = require 'u.range'
-- Select whole file: -- Select whole file:
utils.define_text_object('ag', function() txtobj.define('ag', function()
return Range.from_buf_text() return Range.from_buf_text()
end) end)
``` ```
@ -357,11 +370,11 @@ buf:set_var('...', ...)
buf:all() -- returns a Range representing the entire buffer buf:all() -- returns a Range representing the entire buffer
buf:is_empty() -- returns true if the buffer has no text buf:is_empty() -- returns true if the buffer has no text
buf:append_line '...' buf:append_line '...'
buf:line0(0) -- returns a Range representing the first line in the buffer buf:line(1) -- returns a Range representing the first line in the buffer
buf:line0(-1) -- returns a Range representing the last line in the buffer buf:line(-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 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: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 buf:txtobj('iw') -- returns a Range representing the text object 'iw' in the give buffer
``` ```
## License (MIT) ## License (MIT)

View File

@ -16,8 +16,8 @@ vim.api.nvim_create_autocmd('VimResized', {
}) })
--- @param low number --- @param low number
---@param x number --- @param x number
---@param high number --- @param high number
local function clamp(low, x, high) local function clamp(low, x, high)
x = math.max(low, x) x = math.max(low, x)
x = math.min(x, high) x = math.min(x, high)
@ -63,7 +63,7 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
--- @generic T --- @generic T
--- @param opts SelectOpts<T> --- @param opts SelectOpts<T>
function M.create_picker(opts) function M.create_picker(opts) -- {{{
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i' local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
local stopinsert = not is_in_insert_mode local stopinsert = not is_in_insert_mode
@ -145,17 +145,19 @@ function M.create_picker(opts)
col = s_w_input_coords:get().col, col = s_w_input_coords:get().col,
relative = 'editor', relative = 'editor',
focusable = true, focusable = true,
border = 'rounded', border = vim.o.winborder or 'rounded',
} }
local w_input_buf = Buffer.create(false, true) local w_input_buf = Buffer.create(false, true)
local w_input = vim.api.nvim_open_win(w_input_buf.buf, false, w_input_cfg) local w_input = vim.api.nvim_open_win(w_input_buf.bufnr, false, w_input_cfg)
vim.wo[w_input].number = false vim.wo[w_input][0].cursorline = false
vim.wo[w_input].relativenumber = false vim.wo[w_input][0].list = false
vim.wo[w_input][0].number = false
vim.wo[w_input][0].relativenumber = false
-- The following option is a signal to other plugins like 'cmp' to not mess -- The following option is a signal to other plugins like 'cmp' to not mess
-- with this buffer: -- with this buffer:
vim.bo[w_input_buf.buf].buftype = 'prompt' vim.bo[w_input_buf.bufnr].buftype = 'prompt'
vim.fn.prompt_setprompt(w_input_buf.buf, '') vim.fn.prompt_setprompt(w_input_buf.bufnr, '')
vim.api.nvim_set_current_win(w_input) vim.api.nvim_set_current_win(w_input)
tracker.create_effect(safe_wrap(function() tracker.create_effect(safe_wrap(function()
@ -174,10 +176,10 @@ function M.create_picker(opts)
border = 'rounded', border = 'rounded',
} }
local w_list_buf = Buffer.create(false, true) local w_list_buf = Buffer.create(false, true)
local w_list = vim.api.nvim_open_win(w_list_buf.buf, false, w_list_cfg) local w_list = vim.api.nvim_open_win(w_list_buf.bufnr, false, w_list_cfg)
vim.wo[w_list].number = false vim.wo[w_list][0].number = false
vim.wo[w_list].relativenumber = false vim.wo[w_list][0].relativenumber = false
vim.wo[w_list].scrolloff = 0 vim.wo[w_list][0].scrolloff = 0
tracker.create_effect(safe_wrap(function() tracker.create_effect(safe_wrap(function()
-- update window position/size every time the editor is resized: -- update window position/size every time the editor is resized:
w_list_cfg = vim.tbl_deep_extend('force', w_list_cfg, s_w_list_coords:get()) w_list_cfg = vim.tbl_deep_extend('force', w_list_cfg, s_w_list_coords:get())
@ -222,12 +224,18 @@ function M.create_picker(opts)
local filter_text = vim.trim(s_filter_text:get()):lower() local filter_text = vim.trim(s_filter_text:get()):lower()
local filter_pattern = '' local filter_pattern = ''
local use_plain_pattern = false
if #formatted_items > 250 and #filter_text <= 3 then if #formatted_items > 250 and #filter_text <= 3 then
filter_pattern = filter_text:gsub('%.', '%%.') filter_pattern = filter_text
use_plain_pattern = true
elseif #formatted_items > 1000 then
filter_pattern = filter_text
use_plain_pattern = true
else else
filter_pattern = '(' filter_pattern = '('
.. vim.iter(vim.split(filter_text, '')):map(function(c) return c .. '.*' end):join '' .. vim.iter(vim.split(filter_text, '')):map(function(c) return c .. '.*' end):join ''
.. ')' .. ')'
use_plain_pattern = false
end end
filter_pattern = filter_pattern:lower() filter_pattern = filter_pattern:lower()
@ -245,7 +253,14 @@ function M.create_picker(opts)
local formatted_as_string = Renderer.markup_to_string({ tree = inf.formatted }):lower() local formatted_as_string = Renderer.markup_to_string({ tree = inf.formatted }):lower()
formatted_strings[inf.orig_idx] = formatted_as_string formatted_strings[inf.orig_idx] = formatted_as_string
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern) if use_plain_pattern then
local x, y = formatted_as_string:find(filter_pattern, 1, true)
if x ~= nil and y ~= nil then
matches[inf.orig_idx] = formatted_as_string:sub(x, y)
end
else
matches[inf.orig_idx] = string.match(formatted_as_string, filter_pattern)
end
return matches[inf.orig_idx] ~= nil return matches[inf.orig_idx] ~= nil
end) end)
@ -329,7 +344,7 @@ function M.create_picker(opts)
-- "invalid window ID" errors): -- "invalid window ID" errors):
H.unsubscribe_render_effect() H.unsubscribe_render_effect()
-- buftype=prompt buffers are not "temporary", so delete the buffer manually: -- buftype=prompt buffers are not "temporary", so delete the buffer manually:
vim.api.nvim_buf_delete(w_input_buf.buf, { force = true }) vim.api.nvim_buf_delete(w_input_buf.bufnr, { force = true })
-- The following is not needed, since the buffer is deleted above: -- The following is not needed, since the buffer is deleted above:
-- vim.api.nvim_win_close(w_input, false) -- vim.api.nvim_win_close(w_input, false)
vim.api.nvim_win_close(w_list, false) vim.api.nvim_win_close(w_list, false)
@ -370,9 +385,9 @@ function M.create_picker(opts)
-- --
-- Events -- Events
-- --
vim.keymap.set('i', '<Esc>', function() H.finish(true) end, { buffer = w_input_buf.buf }) vim.keymap.set('i', '<Esc>', function() H.finish(true) end, { buffer = w_input_buf.bufnr })
vim.keymap.set('i', '<CR>', function() H.finish() end, { buffer = w_input_buf.buf }) vim.keymap.set('i', '<CR>', function() H.finish() end, { buffer = w_input_buf.bufnr })
local function action_next_line() local function action_next_line()
local max_line = #s_filtered_items:get() local max_line = #s_filtered_items:get()
@ -382,8 +397,8 @@ function M.create_picker(opts)
end end
s_cursor_index:set(next_cursor_index) s_cursor_index:set(next_cursor_index)
end end
vim.keymap.set('i', '<C-n>', safe_wrap(action_next_line), { buffer = w_input_buf.buf, desc = 'Picker: next' }) vim.keymap.set('i', '<C-n>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
vim.keymap.set('i', '<Down>', safe_wrap(action_next_line), { buffer = w_input_buf.buf, desc = 'Picker: next' }) vim.keymap.set('i', '<Down>', safe_wrap(action_next_line), { buffer = w_input_buf.bufnr, desc = 'Picker: next' })
local function action_prev_line() local function action_prev_line()
local max_line = #s_filtered_items:get() local max_line = #s_filtered_items:get()
@ -391,8 +406,8 @@ function M.create_picker(opts)
if next_cursor_index - s_top_offset:get() < 1 then s_top_offset:set(s_top_offset:get() - 1) end if next_cursor_index - s_top_offset:get() < 1 then s_top_offset:set(s_top_offset:get() - 1) end
s_cursor_index:set(next_cursor_index) s_cursor_index:set(next_cursor_index)
end end
vim.keymap.set('i', '<C-p>', safe_wrap(action_prev_line), { buffer = w_input_buf.buf, desc = 'Picker: previous' }) vim.keymap.set('i', '<C-p>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
vim.keymap.set('i', '<Up>', safe_wrap(action_prev_line), { buffer = w_input_buf.buf, desc = 'Picker: previous' }) vim.keymap.set('i', '<Up>', safe_wrap(action_prev_line), { buffer = w_input_buf.bufnr, desc = 'Picker: previous' })
vim.keymap.set( vim.keymap.set(
'i', 'i',
@ -410,11 +425,11 @@ function M.create_picker(opts)
end end
action_next_line() action_next_line()
end), end),
{ buffer = w_input_buf.buf } { buffer = w_input_buf.bufnr }
) )
for key, fn in pairs(opts.mappings or {}) do for key, fn in pairs(opts.mappings or {}) do
vim.keymap.set('i', key, safe_wrap(function() return fn(controller) end), { buffer = w_input_buf.buf }) vim.keymap.set('i', key, safe_wrap(function() return fn(controller) end), { buffer = w_input_buf.bufnr })
end end
-- Render: -- Render:
@ -458,7 +473,7 @@ function M.create_picker(opts)
return safe_run(function() return s_items_raw:get() end) return safe_run(function() return s_items_raw:get() end)
end end
---@param items T[] --- @param items T[]
function controller.set_items(items) function controller.set_items(items)
return safe_run(function() s_items_raw:set(items) end) return safe_run(function() s_items_raw:set(items) end)
end end
@ -478,7 +493,7 @@ function M.create_picker(opts)
end end
--- @param indicies number[] --- @param indicies number[]
---@param ephemeral? boolean --- @param ephemeral? boolean
function controller.set_selected_indices(indicies, ephemeral) function controller.set_selected_indices(indicies, ephemeral)
return safe_run(function() return safe_run(function()
if ephemeral == nil then ephemeral = false end if ephemeral == nil then ephemeral = false end
@ -505,7 +520,7 @@ function M.create_picker(opts)
end end
return controller --[[@as SelectController]] return controller --[[@as SelectController]]
end end -- }}}
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- END create_picker -- END create_picker
@ -522,6 +537,7 @@ function M.register_ui_select()
--- @param items `T`[] --- @param items `T`[]
--- @param opts { prompt?: string, kind?: any, format_item?: fun(item: T):string } --- @param opts { prompt?: string, kind?: any, format_item?: fun(item: T):string }
--- @param cb fun(item: T|nil):any --- @param cb fun(item: T|nil):any
--- @diagnostic disable-next-line: duplicate-set-field
function vim.ui.select(items, opts, cb) function vim.ui.select(items, opts, cb)
M.create_picker { M.create_picker {
items = items, items = items,
@ -552,7 +568,7 @@ end
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
--- @param opts? { limit?: number } --- @param opts? { limit?: number }
function M.files(opts) function M.files(opts) -- {{{
opts = opts or {} opts = opts or {}
opts.limit = opts.limit or 10000 opts.limit = opts.limit or 10000
@ -706,6 +722,7 @@ function M.files(opts)
if #lines >= opts.limit then if #lines >= opts.limit then
if not job_inf.notified_over_limit then if not job_inf.notified_over_limit then
vim.notify('Picker list is too large (truncating list to ' .. opts.limit .. ' items)', vim.log.levels.WARN) vim.notify('Picker list is too large (truncating list to ' .. opts.limit .. ' items)', vim.log.levels.WARN)
pcall(vim.fn.jobstop, job_inf.id)
job_inf.notified_over_limit = true job_inf.notified_over_limit = true
end end
return return
@ -726,10 +743,10 @@ function M.files(opts)
set_lines_as_items_state() set_lines_as_items_state()
end), end),
}) })
end end -- }}}
function M.buffers() function M.buffers() -- {{{
local cwd = vim.fn.getcwd(0, 0) local cwd = vim.fn.getcwd()
-- ensure that `cwd` ends with a trailing slash: -- ensure that `cwd` ends with a trailing slash:
if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end if cwd[#cwd] ~= '/' then cwd = cwd .. '/' end
@ -831,10 +848,10 @@ function M.buffers()
end, end,
}, },
} }
end end -- }}}
local IS_CODE_SYMBOL_RUNNING = false local IS_CODE_SYMBOL_RUNNING = false
function M.lsp_code_symbols() function M.lsp_code_symbols() -- {{{
if IS_CODE_SYMBOL_RUNNING then return end if IS_CODE_SYMBOL_RUNNING then return end
IS_CODE_SYMBOL_RUNNING = true IS_CODE_SYMBOL_RUNNING = true
@ -881,13 +898,12 @@ function M.lsp_code_symbols()
-- Kick off the async operation: -- Kick off the async operation:
vim.lsp.buf.document_symbol { on_list = STEPS._1_on_symbols } vim.lsp.buf.document_symbol { on_list = STEPS._1_on_symbols }
end end -- }}}
function M.setup() function M.setup()
utils.ucmd('Files', M.files) utils.ucmd('Files', M.files)
utils.ucmd('Buffers', M.buffers) utils.ucmd('Buffers', M.buffers)
utils.ucmd('Lspcodesymbols', M.lsp_code_symbols) utils.ucmd('Lspcodesymbols', M.lsp_code_symbols)
M.register_ui_select()
end end
return M return M

View File

@ -4,7 +4,7 @@ local Range = require 'u.range'
local M = {} local M = {}
--- @param bracket_range Range --- @param bracket_range u.Range
--- @param left string --- @param left string
--- @param right string --- @param right string
local function split(bracket_range, left, right) local function split(bracket_range, left, right)
@ -52,7 +52,7 @@ local function split(bracket_range, left, right)
bracket_range:replace(code.lines) bracket_range:replace(code.lines)
end end
--- @param bracket_range Range --- @param bracket_range u.Range
--- @param left string --- @param left string
--- @param right string --- @param right string
local function join(bracket_range, left, right) local function join(bracket_range, left, right)

View File

@ -53,7 +53,7 @@ local function prompt_for_bounds()
end end
end end
--- @param range Range --- @param range u.Range
--- @param bounds { left: string; right: string } --- @param bounds { left: string; right: string }
local function do_surround(range, bounds) local function do_surround(range, bounds)
local left = bounds.left local left = bounds.left
@ -69,7 +69,7 @@ local function do_surround(range, bounds)
range:replace(left .. range:text() .. right) range:replace(left .. range:text() .. right)
elseif range.mode == 'V' then elseif range.mode == 'V' then
local buf = Buffer.current() 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.bufnr)
-- write the left bound at the current indent level: -- write the left bound at the current indent level:
cw:write(left) 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 if not vim_repeat.is_repeating() then hl = range:highlight('IncSearch', { priority = 999 }) end
local bounds = prompt_for_bounds() local bounds = prompt_for_bounds()
if bounds == nil then if hl then hl.clear() end
if hl then hl.clear() end if bounds == nil then return end
return
end
do_surround(range, bounds) do_surround(range, bounds)
end end
@ -141,7 +139,7 @@ function M.setup()
local from_c = vim.fn.nr2char(from_cn) local from_c = vim.fn.nr2char(from_cn)
local from = surrounds[from_c] or { left = from_c, right = from_c } local from = surrounds[from_c] or { left = from_c, right = from_c }
local function get_fresh_arange() local function get_fresh_arange()
local arange = Range.from_text_object('a' .. from_c, { user_defined = true }) local arange = Range.from_txtobj('a' .. from_c, { user_defined = true })
if arange == nil then return end if arange == nil then return end
if from_c == 'q' then if from_c == 'q' then
from.left = arange.start:char() from.left = arange.start:char()
@ -166,13 +164,9 @@ function M.setup()
hl_clear() hl_clear()
if to == nil then return end 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 if from_c == 't' then
-- For tags, we want to replace the inner text, not the tag: -- For tags, we want to replace the inner text, not the tag:
local irange = Range.from_text_object('i' .. from_c, { user_defined = true }) local irange = Range.from_txtobj('i' .. from_c, { user_defined = true })
if arange == nil or irange == nil then return end if arange == nil or irange == nil then return end
local lrange = Range.new(arange.start, irange.start:must_next(-1)) local lrange = Range.new(arange.start, irange.start:must_next(-1))
@ -182,7 +176,7 @@ function M.setup()
lrange:replace(to.left) lrange:replace(to.left)
else else
-- replace `from.right` with `to.right`: -- 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) .. '$') local from_right_match = last_line:match(vim.pesc(from.right) .. '$')
if from_right_match then if from_right_match then
local match_start = arange.stop:clone() local match_start = arange.stop:clone()
@ -191,7 +185,7 @@ function M.setup()
end end
-- replace `from.left` with `to.left`: -- 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)) local from_left_match = first_line:match('^' .. vim.pesc(from.left))
if from_left_match then if from_left_match then
local match_end = arange.start:clone() local match_end = arange.start:clone()
@ -210,8 +204,8 @@ function M.setup()
CACHED_DELETE_FROM = txt_obj CACHED_DELETE_FROM = txt_obj
local buf = Buffer.current() local buf = Buffer.current()
local irange = Range.from_text_object('i' .. txt_obj) local irange = Range.from_txtobj('i' .. txt_obj)
local arange = Range.from_text_object('a' .. txt_obj) local arange = Range.from_txtobj('a' .. txt_obj)
if arange == nil or irange == nil then return end if arange == nil or irange == nil then return end
local starting_cursor_pos = arange.start:clone() local starting_cursor_pos = arange.start:clone()
@ -223,28 +217,19 @@ function M.setup()
-- Dedenting moves the cursor, so we need to set the cursor to a consistent starting spot: -- Dedenting moves the cursor, so we need to set the cursor to a consistent starting spot:
arange.start:save_to_pos '.' arange.start:save_to_pos '.'
-- Dedenting also changed the inner text, so re-acquire it: -- Dedenting also changed the inner text, so re-acquire it:
arange = Range.from_text_object('a' .. txt_obj) arange = Range.from_txtobj('a' .. txt_obj)
irange = Range.from_text_object('i' .. txt_obj) irange = Range.from_txtobj('i' .. txt_obj)
if arange == nil or irange == nil then return end -- should never be true if arange == nil or irange == nil then return end -- should never be true
arange:replace(irange:lines()) arange:replace(irange:lines())
-- `arange:replace(..)` updates its own `stop` position, so we will use
local final_range = Range.new( -- `arange` as the final resulting range that holds the modified text
arange.start,
Pos.new(
arange.stop.buf,
irange.start.lnum + (arange.stop.lnum + arange.start.lnum),
arange.stop.col,
arange.stop.off
),
irange.mode
)
-- delete last line, if it is empty: -- delete last line, if it is empty:
local last = buf:line0(final_range.stop.lnum) local last = buf:line(arange.stop.lnum)
if last:text():match '^%s*$' then last:replace(nil) end if last:text():match '^%s*$' then last:replace(nil) end
-- delete first line, if it is empty: -- delete first line, if it is empty:
local first = buf:line0(final_range.start.lnum) local first = buf:line(arange.start.lnum)
if first:text():match '^%s*$' then first:replace(nil) end if first:text():match '^%s*$' then first:replace(nil) end
else else
-- trim start: -- trim start:

View File

@ -1,4 +1,4 @@
local utils = require 'u.utils' local txtobj = require 'u.txtobj'
local Pos = require 'u.pos' 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'
@ -7,52 +7,33 @@ local M = {}
function M.setup() function M.setup()
-- Select whole file: -- Select whole file:
utils.define_text_object('ag', function() return Buffer.current():all() end) txtobj.define('ag', function() return Buffer.current():all() end)
-- Select current line: -- Select current line:
utils.define_text_object('a.', function() txtobj.define('a.', function() return Buffer.current():line(Pos.from_pos('.').lnum) end)
local lnum = Pos.from_pos('.').lnum
return Buffer.current():line0(lnum)
end)
-- Select the nearest quote: -- Select the nearest quote:
utils.define_text_object('aq', function() return Range.find_nearest_quotes() end) txtobj.define('aq', function() return Range.find_nearest_quotes() end)
utils.define_text_object('iq', function() txtobj.define('iq', function()
local range = Range.find_nearest_quotes() local range = Range.find_nearest_quotes()
if range == nil then return end if range == nil then return end
return range:shrink(1) return range:shrink(1)
end) end)
---Selects the next quote object (searches forward) ---Selects the next quote object (searches forward)
---@param q string --- @param q string
local function define_quote_obj(q) local function define_quote_obj(q)
local function select_around() local function select_around() return Range.from_txtobj('a' .. q) end
-- Operator mappings are effectively running in visual mode, the way
-- `define_text_object` is implemented, so feed the keys `a${q}` to vim
-- to select the appropriate text-object
vim.cmd { cmd = 'normal', args = { 'a' .. q }, bang = true }
-- Now check on the visually selected text: txtobj.define('a' .. q, function() return select_around() end)
local range = Range.from_vtext() txtobj.define('i' .. q, function()
if range:is_empty() then return range.start end local range = select_around()
range.start = range.start:find_next(1, q) or range.start if range == nil or range:is_empty() then return range end
range.stop = range.stop:find_next(-1, q) or range.stop
return range
end
utils.define_text_object('a' .. q, function() return select_around() end) local start_next = range.start:next(1) or range.start
utils.define_text_object('i' .. q, function() local stop_prev = range.stop:next(-1)
local range_or_pos = select_around() if start_next > stop_prev then return Range.new(start_next) end
if Range.is(range_or_pos) then return range:shrink(1) or range
local start_next = range_or_pos.start:next(1)
local stop_prev = range_or_pos.stop:next(-1)
if start_next > stop_prev then return start_next end
local range = range_or_pos:shrink(1)
return range
else
return range_or_pos
end
end) end)
end end
define_quote_obj [["]] define_quote_obj [["]]
@ -60,36 +41,26 @@ function M.setup()
define_quote_obj [[`]] define_quote_obj [[`]]
---Selects the "last" quote object (searches backward) ---Selects the "last" quote object (searches backward)
---@param q string --- @param q string
local function define_last_quote_obj(q) local function define_last_quote_obj(q)
local function select_around() local function select_around()
local curr = Pos.from_pos('.'):find_next(-1, q) local curr = Pos.from_pos('.'):find_next(-1, q)
if not curr then return end if not curr then return end
-- Reset visual selection to current context: -- Reset visual selection to current context:
Range.new(curr, curr):set_visual_selection() curr:save_to_pos '.'
vim.cmd.normal('a' .. q) return Range.from_txtobj('a' .. q)
local range = Range.from_vtext()
if range:is_empty() then return range.start end
range.start = range.start:find_next(1, q) or range.start
range.stop = range.stop:find_next(-1, q) or range.stop
return range
end end
utils.define_text_object('al' .. q, function() return select_around() end) txtobj.define('al' .. q, function() return select_around() end)
utils.define_text_object('il' .. q, function() txtobj.define('il' .. q, function()
local range_or_pos = select_around() local range = select_around()
if range_or_pos == nil then return end if range == nil or range:is_empty() then return range end
if Range.is(range_or_pos) then local start_next = range.start:next(1) or range.start
local start_next = range_or_pos.start:next(1) local stop_prev = range.stop:next(-1)
local stop_prev = range_or_pos.stop:next(-1) if start_next > stop_prev then return Range.new(start_next) end
if start_next > stop_prev then return start_next end
local range = range_or_pos:shrink(1) return range:shrink(1) or range
return range
else
return range_or_pos
end
end) end)
end end
define_last_quote_obj [["]] define_last_quote_obj [["]]
@ -111,8 +82,8 @@ function M.setup()
local keybinds = { ... } local keybinds = { ... }
table.insert(keybinds, b) table.insert(keybinds, b)
for _, k in ipairs(keybinds) do for _, k in ipairs(keybinds) do
utils.define_text_object('al' .. k, function() return select_around() end) txtobj.define('al' .. k, function() return select_around() end)
utils.define_text_object('il' .. k, function() txtobj.define('il' .. k, function()
local range = select_around() local range = select_around()
return range and range:shrink(1) return range and range:shrink(1)
end) end)

5
lua/u/.luarc.json Normal file
View File

@ -0,0 +1,5 @@
{
"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json",
"diagnostics.globals": ["assert", "vim"],
"runtime.version": "LuaJIT"
}

View File

@ -1,29 +1,29 @@
local Range = require 'u.range' local Range = require 'u.range'
local Renderer = require 'u.renderer'.Renderer local Renderer = require 'u.renderer'.Renderer
---@class Buffer --- @class u.Buffer
---@field buf number --- @field bufnr number
---@field private renderer Renderer --- @field private renderer u.Renderer
local Buffer = {} local Buffer = {}
---@param buf? number --- @param bufnr? number
---@return Buffer --- @return u.Buffer
function Buffer.from_nr(buf) function Buffer.from_nr(bufnr)
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
local renderer = Renderer.new(buf) local renderer = Renderer.new(bufnr)
return setmetatable({ return setmetatable({
buf = buf, bufnr = bufnr,
renderer = renderer, renderer = renderer,
}, { __index = Buffer }) }, { __index = Buffer })
end end
---@return Buffer --- @return u.Buffer
function Buffer.current() return Buffer.from_nr(0) end function Buffer.current() return Buffer.from_nr(0) end
---@param listed boolean --- @param listed boolean
---@param scratch 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.create(listed, scratch) return Buffer.from_nr(vim.api.nvim_create_buf(listed, scratch)) end
function Buffer:set_tmp_options() function Buffer:set_tmp_options()
@ -32,52 +32,53 @@ function Buffer:set_tmp_options()
self:set_option('buftype', 'nowrite') self:set_option('buftype', 'nowrite')
end end
---@param nm string --- @param nm string
function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.buf }) end function Buffer:get_option(nm) return vim.api.nvim_get_option_value(nm, { buf = self.bufnr }) end
---@param nm string --- @param nm string
function Buffer:set_option(nm, val) return vim.api.nvim_set_option_value(nm, val, { buf = self.buf }) end function Buffer:set_option(nm, val) return vim.api.nvim_set_option_value(nm, val, { buf = self.bufnr }) end
---@param nm string --- @param nm string
function Buffer:get_var(nm) return vim.api.nvim_buf_get_var(self.buf, nm) end function Buffer:get_var(nm) return vim.api.nvim_buf_get_var(self.bufnr, nm) end
---@param nm string --- @param nm string
function Buffer:set_var(nm, val) return vim.api.nvim_buf_set_var(self.buf, nm, val) end function Buffer:set_var(nm, val) return vim.api.nvim_buf_set_var(self.bufnr, nm, val) end
function Buffer:line_count() return vim.api.nvim_buf_line_count(self.buf) end function Buffer:line_count() return vim.api.nvim_buf_line_count(self.bufnr) end
function Buffer:all() return Range.from_buf_text(self.buf) end function Buffer:all() return Range.from_buf_text(self.bufnr) 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 --- @param line string
function Buffer:append_line(line) function Buffer:append_line(line)
local start = -1 local start = -1
if self:is_empty() then start = -2 end if self:is_empty() then start = -2 end
vim.api.nvim_buf_set_lines(self.buf, start, -1, false, { line }) vim.api.nvim_buf_set_lines(self.bufnr, start, -1, false, { line })
end end
---@param num number 0-based line index --- @param num number 1-based line index
function Buffer:line0(num) function Buffer:line(num)
if num < 0 then return self:line0(self:line_count() + num) end if num < 0 then num = self:line_count() + num + 1 end
return Range.from_line(self.buf, num) return Range.from_line(self.bufnr, num)
end end
---@param start number 0-based line index --- @param start number 1-based line index
---@param stop number 0-based line index --- @param stop number 1-based line index
function Buffer:lines(start, stop) return Range.from_lines(self.buf, start, stop) end function Buffer:lines(start, stop) return Range.from_lines(self.bufnr, start, stop) end
---@param txt_obj string --- @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) function Buffer:txtobj(txt_obj, opts)
opts = vim.tbl_extend('force', opts or {}, { buf = self.buf }) opts = vim.tbl_extend('force', opts or {}, { bufnr = self.bufnr })
return Range.from_text_object(txt_obj, opts) return Range.from_txtobj(txt_obj, opts)
end end
--- @param event string|string[] --- @param event string|string[]
--- @diagnostic disable-next-line: undefined-doc-name
--- @param opts vim.api.keyset.create_autocmd --- @param opts vim.api.keyset.create_autocmd
function Buffer:autocmd(event, opts) function Buffer:autocmd(event, opts)
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.buf })) vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
end end
--- @param tree Tree --- @param tree Tree

View File

@ -1,14 +1,14 @@
local Buffer = require 'u.buffer' local Buffer = require 'u.buffer'
---@class CodeWriter --- @class u.CodeWriter
---@field lines string[] --- @field lines string[]
---@field indent_level number --- @field indent_level number
---@field indent_str string --- @field indent_str string
local CodeWriter = {} local CodeWriter = {}
---@param indent_level? number --- @param indent_level? number
---@param indent_str? string --- @param indent_str? string
---@return CodeWriter --- @return u.CodeWriter
function CodeWriter.new(indent_level, indent_str) function CodeWriter.new(indent_level, indent_str)
if indent_level == nil then indent_level = 0 end if indent_level == nil then indent_level = 0 end
if indent_str == nil then indent_str = ' ' end if indent_str == nil then indent_str = ' ' end
@ -22,20 +22,20 @@ function CodeWriter.new(indent_level, indent_str)
return cw return cw
end end
---@param p Pos --- @param p u.Pos
function CodeWriter.from_pos(p) function CodeWriter.from_pos(p)
local line = Buffer.from_nr(p.buf):line0(p.lnum):text() local line = Buffer.from_nr(p.bufnr):line(p.lnum):text()
return CodeWriter.from_line(line, p.buf) return CodeWriter.from_line(line, p.bufnr)
end end
---@param line string --- @param line string
---@param buf? number --- @param bufnr? number
function CodeWriter.from_line(line, buf) function CodeWriter.from_line(line, bufnr)
if buf == nil then buf = vim.api.nvim_get_current_buf() end if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
local ws = line:match '^%s*' local ws = line:match '^%s*'
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = buf }) local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = buf }) local shiftwidth = vim.api.nvim_get_option_value('shiftwidth', { buf = bufnr })
local indent_level = 0 local indent_level = 0
local indent_str = '' local indent_str = ''
@ -52,16 +52,16 @@ function CodeWriter.from_line(line, buf)
return CodeWriter.new(indent_level, indent_str) return CodeWriter.new(indent_level, indent_str)
end end
---@param line string --- @param line string
function CodeWriter:write_raw(line) function CodeWriter:write_raw(line)
if line:find '\n' then error 'line contains newline character' end if line:find '\n' then error 'line contains newline character' end
table.insert(self.lines, line) table.insert(self.lines, line)
end end
---@param line string --- @param line string
function CodeWriter:write(line) self:write_raw(self.indent_str:rep(self.indent_level) .. line) end 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) function CodeWriter:indent(f)
local cw = { local cw = {
lines = self.lines, lines = self.lines,

View File

@ -7,7 +7,7 @@ function M.file_for_name(name) return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u
-- Logger class -- Logger class
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
--- @class Logger --- @class u.Logger
--- @field name string --- @field name string
--- @field private fd number --- @field private fd number
local Logger = {} local Logger = {}

View File

@ -1,24 +1,17 @@
local Range = require 'u.range' 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 local __U__OpKeymapOpFunc_rhs = nil
--- This is the global utility function used for operatorfunc --- This is the global utility function used for operatorfunc
--- in opkeymap --- in opkeymap
---@type nil|fun(range: Range): fun():any|nil --- @type nil|fun(range: u.Range): fun():any|nil
---@param ty 'line'|'char'|'block' --- @param ty 'line'|'char'|'block'
-- selene: allow(unused_variable) -- selene: allow(unused_variable)
function __U__OpKeymapOpFunc(ty) function __U__OpKeymapOpFunc(ty)
if __U__OpKeymapOpFunc_rhs ~= nil then if __U__OpKeymapOpFunc_rhs ~= nil then
local range = Range.from_op_func(ty) local range = Range.from_op_func(ty)
local repeat_inject = __U__OpKeymapOpFunc_rhs(range) __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)
end end
end end
@ -28,12 +21,17 @@ end
--- g@: tells vim to way for a motion, and then call operatorfunc. --- 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 --- 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. --- then calls the original passed callback with said range.
---@param mode string|string[] --- @param mode string|string[]
---@param lhs 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 rhs fun(range: u.Range): nil
---@param opts? vim.keymap.set.Opts --- @diagnostic disable-next-line: undefined-doc-name
--- @param opts? vim.keymap.set.Opts
local function opkeymap(mode, lhs, rhs, opts) local function opkeymap(mode, lhs, rhs, opts)
vim.keymap.set(mode, lhs, function() 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 __U__OpKeymapOpFunc_rhs = rhs
vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc' vim.o.operatorfunc = 'v:lua.__U__OpKeymapOpFunc'
return 'g@' return 'g@'

View File

@ -1,27 +1,27 @@
local MAX_COL = vim.v.maxcol local MAX_COL = vim.v.maxcol
---@param buf number --- @param bufnr number
---@param lnum number --- @param lnum number 1-based
local function line_text(buf, lnum) return vim.api.nvim_buf_get_lines(buf, lnum, lnum + 1, false)[1] end local function line_text(bufnr, lnum) return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1] end
---@class Pos --- @class u.Pos
---@field buf number buffer number --- @field bufnr number buffer number
---@field lnum number 1-based line index --- @field lnum number 1-based line index
---@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.MAX_COL = MAX_COL Pos.MAX_COL = MAX_COL
---@param buf? number --- @param bufnr? number
---@param lnum number --- @param lnum number 1-based
---@param col number --- @param col number 1-based
---@param off? number --- @param off? number
---@return Pos --- @return u.Pos
function Pos.new(buf, lnum, col, off) function Pos.new(bufnr, lnum, col, off)
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
if off == nil then off = 0 end if off == nil then off = 0 end
local pos = { local pos = {
buf = buf, bufnr = bufnr,
lnum = lnum, lnum = lnum,
col = col, col = col,
off = off, off = off,
@ -29,9 +29,9 @@ function Pos.new(buf, lnum, col, off)
local function str() local function str()
if pos.off ~= 0 then if pos.off ~= 0 then
return string.format('Pos(%d:%d){buf=%d, off=%d}', pos.lnum, pos.col, pos.buf, pos.off) return string.format('Pos(%d:%d){bufnr=%d, off=%d}', pos.lnum, pos.col, pos.bufnr, pos.off)
else else
return string.format('Pos(%d:%d){buf=%d}', pos.lnum, pos.col, pos.buf) return string.format('Pos(%d:%d){bufnr=%d}', pos.lnum, pos.col, pos.bufnr)
end end
end end
setmetatable(pos, { setmetatable(pos, {
@ -44,119 +44,130 @@ function Pos.new(buf, lnum, col, off)
return pos return pos
end end
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
function Pos.is(x) function Pos.is(x)
if not type(x) == 'table' then return false end
local mt = getmetatable(x) local mt = getmetatable(x)
return mt and mt.__index == Pos return mt and mt.__index == Pos
end 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
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.bufnr == b.bufnr and a.lnum == b.lnum and a.col == b.col end
function Pos.__add(x, y)
---@param name string if type(x) == 'number' then
---@return Pos x, y = y, x
function Pos.from_pos(name) end
local p = vim.fn.getpos(name) if not Pos.is(x) or type(y) ~= 'number' then return nil end
local col = p[3] return x:next(y)
if col ~= MAX_COL then col = col - 1 end end
return Pos.new(p[1], p[2] - 1, col, p[4]) 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 end
function Pos:clone() return Pos.new(self.buf, self.lnum, self.col, self.off) end --- @param name string
--- @return u.Pos
function Pos.from_pos(name)
local p = vim.fn.getpos(name)
return Pos.new(p[1], p[2], p[3], p[4])
end
---@return boolean function Pos:is_invalid() return self.bufnr == 0 and self.lnum == 0 and self.col == 0 and self.off == 0 end
function Pos:clone() return Pos.new(self.bufnr, self.lnum, self.col, self.off) end
--- @return boolean
function Pos:is_col_max() return self.col == MAX_COL end 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). --- Normalize the position to a real position (take into account vim.v.maxcol).
function Pos:as_real() function Pos:as_real()
local maxlen = #line_text(self.bufnr, self.lnum)
local col = self.col 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 -- 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 -- since this is a low-level function, we are going to optimize and
-- use the API directly: -- use the API directly:
col = #line_text(self.buf, self.lnum) - 1 col = maxlen
end end
return Pos.new(self.buf, self.lnum, col, self.off) return Pos.new(self.bufnr, self.lnum, col, self.off)
end end
---@param pos string function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
function Pos:save_to_pos(pos)
if pos == '.' then
vim.api.nvim_win_set_cursor(0, { self.lnum + 1, self.col })
return
end
local p = self:as_real() --- @param pos string
vim.fn.setpos(pos, { p.buf, p.lnum + 1, p.col + 1, p.off }) function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
end
---@param mark string --- @param mark string
function Pos:save_to_mark(mark) function Pos:save_to_mark(mark)
local p = self:as_real() 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.bufnr, mark, p.lnum, p.col - 1, {})
end end
---@return string --- @return string
function Pos:char() function Pos:char()
local line = line_text(self.buf, self.lnum) local line = line_text(self.bufnr, self.lnum)
if line == nil then return '' end if line == nil then return '' end
return line:sub(self.col + 1, self.col + 1) return line:sub(self.col, self.col)
end end
---@param dir? -1|1 function Pos:line() return line_text(self.bufnr, self.lnum) end
---@param must? boolean
---@return Pos|nil --- @param dir? -1|1
--- @param must? boolean
--- @return u.Pos|nil
function Pos:next(dir, must) function Pos:next(dir, must)
if must == nil then must = false end if must == nil then must = false end
if dir == nil or dir == 1 then if dir == nil or dir == 1 then
-- Next: -- Next:
local num_lines = vim.api.nvim_buf_line_count(self.buf) local num_lines = vim.api.nvim_buf_line_count(self.bufnr)
local last_line = line_text(self.buf, num_lines - 1) -- buf:line0(-1) local last_line = line_text(self.bufnr, num_lines)
if self.lnum == num_lines - 1 and self.col == (#last_line - 1) then if self.lnum == num_lines and self.col == #last_line then
if must then error 'error in Pos:next(): Pos:next() returned nil' end if must then error 'error in Pos:next(): Pos:next() returned nil' end
return nil return nil
end end
local col = self.col + 1 local col = self.col + 1
local line = self.lnum local line = self.lnum
local line_max_col = #line_text(self.buf, self.lnum) - 1 local line_max_col = #line_text(self.bufnr, self.lnum)
if col > line_max_col then if col > line_max_col then
col = 0 col = 1
line = line + 1 line = line + 1
end end
return Pos.new(self.buf, line, col, self.off) return Pos.new(self.bufnr, line, col, self.off)
else else
-- Previous: -- 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 if must then error 'error in Pos:next(): Pos:next() returned nil' end
return nil return nil
end end
local col = self.col - 1 local col = self.col - 1
local line = self.lnum local line = self.lnum
local prev_line_max_col = #(line_text(self.buf, self.lnum - 1) or '') - 1 local prev_line_max_col = #(line_text(self.bufnr, self.lnum - 1) or '')
if col < 0 then if col < 1 then
col = math.max(prev_line_max_col, 0) col = math.max(prev_line_max_col, 1)
line = line - 1 line = line - 1
end end
return Pos.new(self.buf, line, col, self.off) return Pos.new(self.bufnr, line, col, self.off)
end end
end end
---@param dir? -1|1 --- @param dir? -1|1
function Pos:must_next(dir) function Pos:must_next(dir)
local next = self:next(dir, true) local next = self:next(dir, true)
if next == nil then error 'unreachable' end if next == nil then error 'unreachable' end
return next return next
end end
---@param dir -1|1 --- @param dir -1|1
---@param predicate fun(p: Pos): boolean --- @param predicate fun(p: u.Pos): boolean
---@param test_current? boolean --- @param test_current? boolean
function Pos:next_while(dir, predicate, test_current) function Pos:next_while(dir, predicate, test_current)
if test_current and not predicate(self) then return end if test_current and not predicate(self) then return end
local curr = self local curr = self
@ -168,15 +179,15 @@ function Pos:next_while(dir, predicate, test_current)
return curr return curr
end end
---@param dir -1|1 --- @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) function Pos:find_next(dir, predicate)
if type(predicate) == 'string' then if type(predicate) == 'string' then
local s = predicate local s = predicate
predicate = function(p) return s == p:char() end predicate = function(p) return s == p:char() end
end end
---@type Pos|nil --- @type u.Pos|nil
local curr = self local curr = self
while curr ~= nil do while curr ~= nil do
if predicate(curr) then return curr end if predicate(curr) then return curr end
@ -186,9 +197,9 @@ function Pos:find_next(dir, predicate)
end end
--- Finds the matching bracket/paren for the current position. --- Finds the matching bracket/paren for the current position.
---@param max_chars? number|nil --- @param max_chars? number|nil
---@param invocations? Pos[] --- @param invocations? u.Pos[]
---@return Pos|nil --- @return u.Pos|nil
function Pos:find_match(max_chars, invocations) function Pos:find_match(max_chars, invocations)
if invocations == nil then invocations = {} end if invocations == nil then invocations = {} end
if vim.tbl_contains(invocations, function(p) return self == p end, { predicate = true }) then return nil 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 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) 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] local c_match = (is_opener and closers or openers)[i]
---@type Pos|nil --- @type u.Pos|nil
local cur = self 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() local function adv()
if cur == nil then return nil end 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) return cur:next(is_opener and 1 or -1)
end end
-- scan until we find a match: -- scan until we find `c_match`:
cur = adv() cur = adv()
while cur ~= nil and cur:char() ~= c_match do while cur ~= nil and cur:char() ~= c_match do
cur = adv() cur = adv()
@ -236,4 +252,10 @@ function Pos:find_match(max_chars, invocations)
return cur return cur
end end
--- @param lines string|string[]
function Pos:insert_before(lines)
if type(lines) == 'string' then lines = vim.split(lines, '\n') end
vim.api.nvim_buf_set_text(self.bufnr, self.lnum - 1, self.col - 1, self.lnum - 1, self.col - 1, lines)
end
return Pos return Pos

View File

@ -1,5 +1,4 @@
local Pos = require 'u.pos' local Pos = require 'u.pos'
local State = require 'u.state'
local orig_on_yank = (vim.hl or vim.highlight).on_yank local orig_on_yank = (vim.hl or vim.highlight).on_yank
local on_yank_enabled = true; local on_yank_enabled = true;
@ -8,16 +7,16 @@ local on_yank_enabled = true;
return orig_on_yank(opts) return orig_on_yank(opts)
end end
---@class Range --- @class u.Range
---@field start Pos --- @field start u.Pos
---@field stop Pos|nil --- @field stop u.Pos|nil
---@field mode 'v'|'V' --- @field mode 'v'|'V'
local Range = {} local Range = {}
---@param start Pos --- @param start u.Pos
---@param stop Pos|nil --- @param stop u.Pos|nil
---@param mode? 'v'|'V' --- @param mode? 'v'|'V'
---@return Range --- @return u.Range
function Range.new(start, stop, mode) function Range.new(start, stop, mode)
if stop ~= nil and stop < start then if stop ~= nil and stop < start then
start, stop = stop, start 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 r = { start = start, stop = stop, mode = mode or 'v' }
local function str() local function str()
---@param p Pos --- @param p u.Pos
local function posstr(p) local function posstr(p)
if p == nil then if p == nil then
return 'nil' return 'nil'
@ -38,7 +37,7 @@ function Range.new(start, stop, mode)
local _1 = posstr(r.start) local _1 = posstr(r.start)
local _2 = posstr(r.stop) local _2 = posstr(r.stop)
return string.format('Range{buf=%d, mode=%s, start=%s, stop=%s}', r.start.buf, r.mode, _1, _2) return string.format('Range{bufnr=%d, mode=%s, start=%s, stop=%s}', r.start.bufnr, r.mode, _1, _2)
end end
setmetatable(r, { __index = Range, __tostring = str }) setmetatable(r, { __index = Range, __tostring = str })
return r return r
@ -49,14 +48,14 @@ function Range.is(x)
return mt and mt.__index == Range return mt and mt.__index == Range
end end
---@param lpos string --- @param lpos string
---@param rpos string --- @param rpos string
---@return Range --- @return u.Range
function Range.from_marks(lpos, rpos) function Range.from_marks(lpos, rpos)
local start = Pos.from_pos(lpos) local start = Pos.from_pos(lpos)
local stop = Pos.from_pos(rpos) local stop = Pos.from_pos(rpos)
---@type 'v'|'V' --- @type 'v'|'V'
local mode local mode
if stop:is_col_max() then if stop:is_col_max() then
mode = 'V' mode = 'V'
@ -67,93 +66,99 @@ function Range.from_marks(lpos, rpos)
return Range.new(start, stop, mode) return Range.new(start, stop, mode)
end end
---@param buf? number --- @param bufnr? number
function Range.from_buf_text(buf) function Range.from_buf_text(bufnr)
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
local num_lines = vim.api.nvim_buf_line_count(buf) local num_lines = vim.api.nvim_buf_line_count(bufnr)
local start = Pos.new(buf, 0, 0) local start = Pos.new(bufnr, 1, 1)
local stop = Pos.new(buf, num_lines - 1, Pos.MAX_COL) local stop = Pos.new(bufnr, num_lines, Pos.MAX_COL)
return Range.new(start, stop, 'V') return Range.new(start, stop, 'V')
end end
---@param buf? number --- @param bufnr? number
---@param line number 0-based line index --- @param line number 1-based line index
function Range.from_line(buf, line) return Range.from_lines(buf, line, line) end function Range.from_line(bufnr, line) return Range.from_lines(bufnr, line, line) end
---@param buf? number --- @param bufnr? number
---@param start_line number 0-based line index --- @param start_line number based line index
---@param stop_line number 0-based line index --- @param stop_line number based line index
function Range.from_lines(buf, start_line, stop_line) function Range.from_lines(bufnr, start_line, stop_line)
if buf == nil or buf == 0 then buf = vim.api.nvim_get_current_buf() end if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
if stop_line < 0 then if stop_line < 0 then
local num_lines = vim.api.nvim_buf_line_count(buf) local num_lines = vim.api.nvim_buf_line_count(bufnr)
stop_line = num_lines + stop_line stop_line = num_lines + stop_line + 1
end end
return Range.new(Pos.new(buf, start_line, 0), Pos.new(buf, stop_line, Pos.MAX_COL), 'V') return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, Pos.MAX_COL), 'V')
end end
---@param text_obj string --- @param text_obj string
---@param opts? { buf?: number; contains_cursor?: boolean; pos?: Pos, user_defined?: boolean } --- @param opts? { bufnr?: number; contains_cursor?: boolean; pos?: u.Pos, user_defined?: boolean }
---@return Range|nil --- @return u.Range|nil
function Range.from_text_object(text_obj, opts) function Range.from_txtobj(text_obj, opts)
opts = opts or {} opts = opts or {}
if opts.buf == nil then opts.buf = vim.api.nvim_get_current_buf() end if opts.bufnr == nil then opts.bufnr = vim.api.nvim_get_current_buf() end
if opts.contains_cursor == nil then opts.contains_cursor = false end if opts.contains_cursor == nil then opts.contains_cursor = false end
if opts.user_defined == nil then opts.user_defined = false end if opts.user_defined == nil then opts.user_defined = false end
---@type "a" | "i" --- @type "a" | "i"
local selection_type = text_obj:sub(1, 1) local selection_type = text_obj:sub(1, 1)
local obj_type = text_obj:sub(#text_obj, #text_obj) local obj_type = text_obj:sub(#text_obj, #text_obj)
local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type) local is_quote = vim.tbl_contains({ "'", '"', '`' }, obj_type)
local cursor = Pos.from_pos '.' local cursor = Pos.from_pos '.'
-- Yank, then read '[ and '] to know the bounds: --- @type u.Pos
---@type { start: Pos; stop: Pos } local start
local positions --- @type u.Pos
vim.api.nvim_buf_call(opts.buf, function() local stop
positions = State.run(0, function(s)
s:track_winview()
s:track_register '"'
s:track_pos '.'
s:track_pos "'["
s:track_pos "']"
if opts.pos ~= nil then opts.pos:save_to_pos '.' end vim.api.nvim_buf_call(opts.bufnr, function()
local original_state = {
winview = vim.fn.winsaveview(),
regquote = vim.fn.getreg '"',
posdot = vim.fn.getpos '.',
poslb = vim.fn.getpos "'[",
posrb = vim.fn.getpos "']",
}
local null_pos = Pos.new(0, 0, 0, 0) if opts.pos ~= nil then opts.pos:save_to_pos '.' end
null_pos:save_to_pos "'["
null_pos:save_to_pos "']"
local prev_on_yank_enabled = on_yank_enabled Pos.invalid():save_to_pos "'["
on_yank_enabled = false Pos.invalid():save_to_pos "']"
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 prev_on_yank_enabled = on_yank_enabled
local stop = Pos.from_pos "']" 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
if start = Pos.from_pos "'["
-- I have no idea why, but when yanking `i"`, the stop-mark is stop = Pos.from_pos "']"
-- placed on the ending quote. For other text-objects, the stop-
-- mark is placed before the closing character. -- Restore original state:
(is_quote and selection_type == 'i' and stop:char() == obj_type) vim.fn.winrestview(original_state.winview)
-- *Sigh*, this also sometimes happens for `it` as well. vim.fn.setreg('"', original_state.regquote)
or (text_obj == 'it' and stop:char() == '<') vim.fn.setpos('.', original_state.posdot)
then vim.fn.setpos("'[", original_state.poslb)
stop = stop:next(-1) or stop vim.fn.setpos("']", original_state.posrb)
end
return { start = start, stop = stop } if
end) -- 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) end)
local start = positions.start
local stop = positions.stop if start == stop and start:is_invalid() then return nil end
if start == stop and start.lnum == 0 and start.col == 0 and start.off == 0 then return nil end
if opts.contains_cursor and not Range.new(start, stop):contains(cursor) 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 if is_quote and selection_type == 'a' then
@ -178,7 +183,7 @@ end
--- Get range information from the current text range being operated on --- Get range information from the current text range being operated on
--- as defined by an operator-pending function. Infers line-wise vs. char-wise --- as defined by an operator-pending function. Infers line-wise vs. char-wise
--- based on the type, as given by the operator-pending function. --- based on the type, as given by the operator-pending function.
---@param type 'line'|'char'|'block' --- @param type 'line'|'char'|'block'
function Range.from_op_func(type) function Range.from_op_func(type)
if type == 'block' then error 'block motions not supported' end if type == 'block' then error 'block motions not supported' end
@ -188,12 +193,12 @@ function Range.from_op_func(type)
end end
--- Get range information from command arguments. --- Get range information from command arguments.
---@param args unknown --- @param args unknown
---@return Range|nil --- @return u.Range|nil
function Range.from_cmd_args(args) function Range.from_cmd_args(args)
---@type 'v'|'V' --- @type 'v'|'V'
local mode local mode
---@type nil|Pos --- @type nil|u.Pos
local start local start
local stop local stop
if args.range == 0 then if args.range == 0 then
@ -201,59 +206,42 @@ function Range.from_cmd_args(args)
else else
start = Pos.from_pos "'<" start = Pos.from_pos "'<"
stop = Pos.from_pos "'>" stop = Pos.from_pos "'>"
if stop:is_col_max() then mode = stop:is_col_max() and 'V' or 'v'
mode = 'V'
else
mode = 'v'
end
end end
return Range.new(start, stop, mode) return Range.new(start, stop, mode)
end end
--- ---
function Range.find_nearest_brackets() function Range.find_nearest_brackets()
local a = Range.from_text_object('a<', { contains_cursor = true }) return Range.smallest {
local b = Range.from_text_object('a[', { contains_cursor = true }) Range.from_txtobj('a<', { contains_cursor = true }),
local c = Range.from_text_object('a(', { contains_cursor = true }) Range.from_txtobj('a[', { contains_cursor = true }),
local d = Range.from_text_object('a{', { contains_cursor = true }) Range.from_txtobj('a(', { contains_cursor = true }),
return Range.smallest { a, b, c, d } Range.from_txtobj('a{', { contains_cursor = true }),
}
end end
function Range.find_nearest_quotes() function Range.find_nearest_quotes()
local a = Range.from_text_object([[a']], { contains_cursor = true }) return Range.smallest {
if a ~= nil and a:is_empty() then a = nil end Range.from_txtobj([[a']], { contains_cursor = true }),
local b = Range.from_text_object([[a"]], { contains_cursor = true }) Range.from_txtobj([[a"]], { contains_cursor = true }),
if b ~= nil and b:is_empty() then b = nil end Range.from_txtobj([[a`]], { contains_cursor = true }),
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 }
end end
---@param ranges (Range|nil)[] --- @param ranges (u.Range|nil)[]
function Range.smallest(ranges) function Range.smallest(ranges)
---@type Range[] --- @type u.Range[]
local new_ranges = {} ranges = vim.iter(ranges):filter(function(r) return r ~= nil and not r:is_empty() end):totable()
for _, r in pairs(ranges) do
if r ~= nil then table.insert(new_ranges, r) end
end
ranges = new_ranges
if #ranges == 0 then return nil end if #ranges == 0 then return nil end
-- find smallest match -- find smallest match
local max_start = ranges[1].start local smallest = ranges[1]
local min_stop = ranges[1].stop
local result = ranges[1]
for _, r in ipairs(ranges) do for _, r in ipairs(ranges) do
local start, stop = r.start, r.stop local start, stop = r.start, r.stop
if start > max_start and stop < min_stop then if start > smallest.start and stop < smallest.stop then smallest = r end
max_start = start
min_stop = stop
result = r
end
end end
return smallest
return result
end end
function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) end function Range:clone() return Range.new(self.start:clone(), self.stop ~= nil and self.stop:clone() or nil, self.mode) end
@ -266,7 +254,7 @@ function Range:to_linewise()
local r = self:clone() local r = self:clone()
r.mode = 'V' r.mode = 'V'
r.start.col = 0 r.start.col = 1
if r.stop ~= nil then r.stop.col = Pos.MAX_COL end if r.stop ~= nil then r.stop.col = Pos.MAX_COL end
return r return r
@ -298,116 +286,97 @@ function Range:trim_stop()
return r return r
end 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 function Range:contains(p) return not self:is_empty() and p >= self.start and p <= self.stop end
---@return string[] --- @return string[]
function Range:lines() function Range:lines()
if self:is_empty() then return {} end if self:is_empty() then return {} end
return vim.fn.getregion(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
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
end end
---@return string --- @return string
function Range:text() return vim.fn.join(self:lines(), '\n') end function Range:text() return vim.fn.join(self:lines(), '\n') end
---@param i number 1-based --- @param i number 1-based
---@param j? number 1-based --- @param j? number 1-based
function Range:sub(i, j) return self:text():sub(i, j) end function Range:sub(i, j) return self:text():sub(i, j) end
---@param l number --- @param l number
---@return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():Range; text: fun():string }|nil --- @return { line: string; idx0: { start: number; stop: number; }; lnum: number; range: fun():u.Range; text: fun():string }|nil
function Range:line0(l) function Range:line(l)
if l < 0 then return self:line0(self:line_count() + l) end if l < 0 then l = self:line_count() + l + 1 end
if l > self:line_count() then return 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] local line_indices = vim.fn.getregionpos(self.start:as_vim(), self.stop:as_vim(), { type = self.mode })
if line == nil then return end local line_bounds = line_indices[l]
local start = 0 local start = Pos.new(unpack(line_bounds[1]))
local stop = #line - 1 local stop = Pos.new(unpack(line_bounds[2]))
if l == 0 then start = self.start.col end return Range.new(start, stop)
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,
}
end end
---@param replacement nil|string|string[] --- @param replacement nil|string|string[]
function Range:replace(replacement) function Range:replace(replacement)
if replacement == nil then replacement = {} end if replacement == nil then replacement = {} end
if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end if type(replacement) == 'string' then replacement = vim.fn.split(replacement, '\n') end
local buf = self.start.buf local bufnr = self.start.bufnr
-- 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' local replace_type = (self:is_empty() and 'insert') or (self.mode == 'v' and 'region') or 'lines'
---@param alnum number local function update_stop_non_linewise()
---@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 new_last_line_num = self.start.lnum + #replacement - 1 local new_last_line_num = self.start.lnum + #replacement - 1
local new_last_col = #(replacement[#replacement] or '') 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(bufnr, new_last_line_num, new_last_col)
self.stop = Pos.new(buf, new_last_line_num, new_last_col)
end end
local function update_stop_linewise()
---@param alnum number if #replacement == 0 then
---@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
self.stop = nil self.stop = nil
else else
local new_last_line_num = start_lnum + #replacement - 1 local new_last_line_num = self.start.lnum - 1 + #replacement - 1
self.stop = Pos.new(self.start.buf, new_last_line_num, Pos.MAX_COL, self.stop.off) self.stop = Pos.new(bufnr, new_last_line_num + 1, Pos.MAX_COL, self.stop.off)
end end
self.mode = 'v' self.mode = 'v'
end end
if replace_type == 'insert' then 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(
bufnr,
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 elseif replace_type == 'region' then
-- Fixup the bounds: -- Fixup the bounds:
local last_line = vim.api.nvim_buf_get_lines(buf, stop_lnum - 1, stop_lnum, false)[1] or '' local max_col = #self.stop:line()
local max_col = #last_line
set_text(start_lnum, start_col, stop_lnum - 1, math.min(stop_col, max_col), replacement) -- Indexing is zero-based. Row indices are end-inclusive, and column indices
-- are end-exclusive.
vim.api.nvim_buf_set_text(
bufnr,
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 elseif replace_type == 'lines' then
set_lines(start_lnum, stop_lnum, replacement) -- Indexing is zero-based, end-exclusive.
vim.api.nvim_buf_set_lines(bufnr, self.start.lnum - 1, self.stop.lnum, true, replacement)
update_stop_linewise()
else else
error 'unreachable' error 'unreachable'
end end
end end
---@param amount number --- @param amount number
function Range:shrink(amount) function Range:shrink(amount)
local start = self.start local start = self.start
local stop = self.stop local stop = self.stop
@ -425,15 +394,15 @@ function Range:shrink(amount)
return Range.new(start, stop, self.mode) return Range.new(start, stop, self.mode)
end end
---@param amount number --- @param amount number
function Range:must_shrink(amount) function Range:must_shrink(amount)
local shrunk = self:shrink(amount) local shrunk = self:shrink(amount)
if shrunk == nil or shrunk:is_empty() then error 'error in Range:must_shrink: Range:shrink() returned nil' end if shrunk == nil or shrunk:is_empty() then error 'error in Range:must_shrink: Range:shrink() returned nil' end
return shrunk return shrunk
end end
---@param left string --- @param left string
---@param right string --- @param right string
function Range:save_to_pos(left, right) function Range:save_to_pos(left, right)
if self:is_empty() then if self:is_empty() then
self.start:save_to_pos(left) self.start:save_to_pos(left)
@ -444,8 +413,8 @@ function Range:save_to_pos(left, right)
end end
end end
---@param left string --- @param left string
---@param right string --- @param right string
function Range:save_to_marks(left, right) function Range:save_to_marks(left, right)
if self:is_empty() then if self:is_empty() then
self.start:save_to_mark(left) self.start:save_to_mark(left)
@ -458,28 +427,20 @@ end
function Range:set_visual_selection() function Range:set_visual_selection()
if self:is_empty() then return end 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
if vim.api.nvim_get_current_buf() ~= self.start.buf then vim.api.nvim_set_current_buf(self.start.buf) end local curr_mode = vim.fn.mode()
if curr_mode ~= self.mode then vim.cmd.normal { args = { self.mode }, bang = true } end
State.run(self.start.buf, function(s) self.start:save_to_pos '.'
s:track_mark 'a' vim.cmd.normal { args = { 'o' }, bang = true }
s:track_mark 'b' self.stop:save_to_pos '.'
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)
end end
---@param group string --- @param group string
---@param opts? { timeout?: number, priority?: number, on_macro?: boolean } --- @param opts? { timeout?: number, priority?: number, on_macro?: boolean }
function Range:highlight(group, opts) function Range:highlight(group, opts)
if self:is_empty() then return end if self:is_empty() then return end
@ -490,33 +451,31 @@ function Range:highlight(group, opts)
if not opts.on_macro and in_macro then return { clear = function() end } end if not opts.on_macro and in_macro then return { clear = function() end } end
local ns = vim.api.nvim_create_namespace '' 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( local winview = vim.fn.winsaveview();
self.start.buf, (vim.hl or vim.highlight).range(
ns, self.start.bufnr,
group, ns,
{ self.start.lnum, self.start.col }, group,
{ self.stop.lnum, self.stop.col }, { self.start.lnum - 1, self.start.col - 1 },
{ { self.stop.lnum - 1, self.stop.col - 1 },
inclusive = true, {
priority = opts.priority, inclusive = true,
regtype = self.mode, priority = opts.priority,
} timeout = opts.timeout,
) regtype = self.mode,
}
return nil )
end) if not in_macro then vim.fn.winrestview(winview) end
vim.cmd.redraw() vim.cmd.redraw()
local function clear() return {
vim.api.nvim_buf_clear_namespace(self.start.buf, ns, self.start.lnum, self.stop.lnum + 1) ns = ns,
vim.cmd.redraw() clear = function()
end vim.api.nvim_buf_clear_namespace(self.start.bufnr, ns, self.start.lnum - 1, self.stop.lnum)
if opts.timeout ~= nil then vim.defer_fn(clear, opts.timeout) end vim.cmd.redraw()
end,
return { ns = ns, clear = clear } }
end end
return Range return Range

View File

@ -21,7 +21,7 @@ end
-- Renderer {{{ -- Renderer {{{
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any } --- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
--- @class Renderer --- @class u.Renderer
--- @field bufnr number --- @field bufnr number
--- @field ns number --- @field ns number
--- @field changedtick number --- @field changedtick number
@ -377,7 +377,7 @@ end -- }}}
-- }}} -- }}}
-- TreeBuilder {{{ -- TreeBuilder {{{
--- @class TreeBuilder --- @class u.TreeBuilder
--- @field private nodes Node[] --- @field private nodes Node[]
local TreeBuilder = {} local TreeBuilder = {}
TreeBuilder.__index = TreeBuilder TreeBuilder.__index = TreeBuilder
@ -389,7 +389,7 @@ function TreeBuilder.new()
end end
--- @param nodes Tree --- @param nodes Tree
--- @return TreeBuilder --- @return u.TreeBuilder
function TreeBuilder:put(nodes) function TreeBuilder:put(nodes)
table.insert(self.nodes, nodes) table.insert(self.nodes, nodes)
return self return self
@ -398,7 +398,7 @@ end
--- @param name string --- @param name string
--- @param attributes? table<string, any> --- @param attributes? table<string, any>
--- @param children? Node | Node[] --- @param children? Node | Node[]
--- @return TreeBuilder --- @return u.TreeBuilder
function TreeBuilder:put_h(name, attributes, children) function TreeBuilder:put_h(name, attributes, children)
local tag = M.h(name, attributes, children) local tag = M.h(name, attributes, children)
table.insert(self.nodes, tag) table.insert(self.nodes, tag)
@ -406,7 +406,7 @@ function TreeBuilder:put_h(name, attributes, children)
end end
--- @param fn fun(TreeBuilder): any --- @param fn fun(TreeBuilder): any
--- @return TreeBuilder --- @return u.TreeBuilder
function TreeBuilder:nest(fn) function TreeBuilder:nest(fn)
local nested_writer = TreeBuilder.new() local nested_writer = TreeBuilder.new()
fn(nested_writer) fn(nested_writer)

View File

@ -1,61 +1,39 @@
local M = {} 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 local function is_repeatable_last_mutator() return vim.b.changedtick <= (vim.b.my_changedtick or 0) end
M.native_undo = function() _normal 'u' end
local function update_ts() vim.b.tt_changedtick = vim.b.changedtick end --- @param f fun()
function M.run_repeatable(f)
---@param cmd? string|fun():unknown REPEAT_ACTION = f
function M.set(cmd) REPEAT_ACTION()
update_ts() vim.b.my_changedtick = vim.b.changedtick
if cmd ~= nil then vim.b.tt_repeatcmd = cmd end
end end
local function tt_was_last_repeatable() function M.is_repeating() return IS_REPEATING end
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.setup() function M.setup()
vim.keymap.set('n', '.', M.do_repeat) vim.keymap.set('n', '.', function()
vim.keymap.set('n', 'u', M.undo) 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 end
return M return M

View File

@ -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<string, any>
---@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

View File

@ -6,7 +6,7 @@ M.debug = false
-- class Signal -- class Signal
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
--- @class Signal --- @class u.Signal
--- @field name? string --- @field name? string
--- @field private changing boolean --- @field private changing boolean
--- @field private value any --- @field private value any
@ -18,7 +18,7 @@ Signal.__index = Signal
--- @param value any --- @param value any
--- @param name? string --- @param name? string
--- @return Signal --- @return u.Signal
function Signal:new(value, name) function Signal:new(value, name)
local obj = setmetatable({ local obj = setmetatable({
name = name, name = name,
@ -83,7 +83,7 @@ function Signal:schedule_update(fn) self:schedule_set(fn(self.value)) end
--- @generic U --- @generic U
--- @param fn fun(value: T): U --- @param fn fun(value: T): U
--- @return Signal --<U> --- @return u.Signal --<U>
function Signal:map(fn) function Signal:map(fn)
local mapped_signal = M.create_memo(function() local mapped_signal = M.create_memo(function()
local value = self:get() local value = self:get()
@ -92,13 +92,13 @@ function Signal:map(fn)
return mapped_signal return mapped_signal
end end
--- @return Signal --- @return u.Signal
function Signal:clone() function Signal:clone()
return self:map(function(x) return x end) return self:map(function(x) return x end)
end end
--- @param fn fun(value: T): boolean --- @param fn fun(value: T): boolean
--- @return Signal -- <T> --- @return u.Signal -- <T>
function Signal:filter(fn) function Signal:filter(fn)
local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil) local filtered_signal = M.create_signal(nil, self.name and self.name .. ':filtered' or nil)
local unsubscribe_from_self = self:subscribe(function(value) local unsubscribe_from_self = self:subscribe(function(value)
@ -109,7 +109,7 @@ function Signal:filter(fn)
end end
--- @param ms number --- @param ms number
--- @return Signal -- <T> --- @return u.Signal -- <T>
function Signal:debounce(ms) function Signal:debounce(ms)
local function set_timeout(timeout, callback) local function set_timeout(timeout, callback)
local timer = (vim.uv or vim.loop).new_timer() 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) local filtered = M.create_signal(self.value, self.name and self.name .. ':debounced' or nil)
--- @type { --- @diagnostic disable-next-line: undefined-doc-name
-- queued: { value: T, ts: number }[] --- @type { queued: { value: T, ts: number }[]; timer?: uv_timer_t; }
-- timer?: uv_timer_t
-- }
local state = { queued = {}, timer = nil } local state = { queued = {}, timer = nil }
local function clear_timeout() local function clear_timeout()
if state.timer == nil then return end if state.timer == nil then return end
pcall(function() pcall(function()
--- @diagnostic disable-next-line: undefined-field
state.timer:stop() state.timer:stop()
--- @diagnostic disable-next-line: undefined-field
state.timer:close() state.timer:close()
end) end)
state.timer = nil state.timer = nil
@ -198,13 +198,13 @@ end
CURRENT_CONTEXT = nil CURRENT_CONTEXT = nil
--- @class ExecutionContext --- @class u.ExecutionContext
--- @field signals table<Signal, boolean> --- @field signals table<u.Signal, boolean>
local ExecutionContext = {} local ExecutionContext = {}
M.ExecutionContext = ExecutionContext M.ExecutionContext = ExecutionContext
ExecutionContext.__index = ExecutionContext ExecutionContext.__index = ExecutionContext
--- @return ExecutionContext --- @return u.ExecutionContext
function ExecutionContext:new() function ExecutionContext:new()
return setmetatable({ return setmetatable({
signals = {}, signals = {},
@ -215,7 +215,7 @@ end
function ExecutionContext.current() return CURRENT_CONTEXT end function ExecutionContext.current() return CURRENT_CONTEXT end
--- @param fn function --- @param fn function
--- @param ctx ExecutionContext --- @param ctx u.ExecutionContext
function ExecutionContext:run(fn, ctx) function ExecutionContext:run(fn, ctx)
local oldCtx = CURRENT_CONTEXT local oldCtx = CURRENT_CONTEXT
CURRENT_CONTEXT = ctx CURRENT_CONTEXT = ctx
@ -258,14 +258,14 @@ end
--- @param value any --- @param value any
--- @param name? string --- @param name? string
--- @return Signal --- @return u.Signal
function M.create_signal(value, name) return Signal:new(value, name) end function M.create_signal(value, name) return Signal:new(value, name) end
--- @param fn function --- @param fn function
--- @param name? string --- @param name? string
--- @return Signal --- @return u.Signal
function M.create_memo(fn, name) function M.create_memo(fn, name)
--- @type Signal --- @type u.Signal
local result local result
local unsubscribe = M.create_effect(function() local unsubscribe = M.create_effect(function()
local value = fn() local value = fn()

45
lua/u/txtobj.lua Normal file
View File

@ -0,0 +1,45 @@
local Range = require 'u.range'
local M = {}
local ESC = vim.api.nvim_replace_termcodes("<Esc>", true, false, true)
--- @param key_seq string
--- @param fn fun(key_seq: string):u.Range|nil
--- @param opts? { buffer: number|nil }
function M.define(key_seq, fn, opts)
if opts ~= nil and opts.buffer == 0 then opts.buffer = vim.api.nvim_get_current_buf() end
local function handle_visual()
local range = fn(key_seq)
if range == nil or range:is_empty() then
vim.cmd.normal(ESC)
return
end
range:set_visual_selection()
end
vim.keymap.set({ 'x' }, key_seq, handle_visual, opts and { buffer = opts.buffer } or nil)
local function handle_normal()
local range = fn(key_seq)
if range == nil then return end
if not range:is_empty() then
range:set_visual_selection()
else
local original_eventignore = vim.go.eventignore
vim.go.eventignore = 'all'
-- insert a single space, so we can select it:
local p = range.start
p:insert_before ' '
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
return M

View File

@ -4,9 +4,9 @@ local M = {}
-- Types -- Types
-- --
---@alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string } --- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
---@alias KeyMaps table<string, fun(): any | string> } --- @alias KeyMaps table<string, fun(): any | string> }
---@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 --- @generic T
--- @param x `T` --- @param x `T`
@ -32,9 +32,9 @@ end
--- vim.print(args.info:lines()) --- vim.print(args.info:lines())
--- end, { nargs = '*', range = true }) --- end, { nargs = '*', range = true })
--- ``` --- ```
---@param name string --- @param name string
---@param cmd string | fun(args: CmdArgs): any --- @param cmd string | fun(args: CmdArgs): any
---@param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string } --- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
function M.ucmd(name, cmd, opts) function M.ucmd(name, cmd, opts)
local Range = require 'u.range' local Range = require 'u.range'
@ -49,76 +49,6 @@ function M.ucmd(name, cmd, opts)
vim.api.nvim_create_user_command(name, cmd2, opts or {}) vim.api.nvim_create_user_command(name, cmd2, opts or {})
end end
---@param key_seq string
---@param fn fun(key_seq: string):Range|Pos|nil
---@param opts? { buffer: number|nil }
function M.define_text_object(key_seq, fn, opts)
local Range = require 'u.range'
local Pos = require 'u.pos'
if opts ~= nil and opts.buffer == 0 then opts.buffer = vim.api.nvim_get_current_buf() end
local function handle_visual()
local range_or_pos = fn(key_seq)
if range_or_pos == nil then return end
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]]
range:set_visual_selection()
else
vim.cmd { cmd = 'normal', args = { '<Esc>' }, bang = true }
end
end
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 }
local range_or_pos = fn(key_seq)
if range_or_pos == nil then return end
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
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'
-- 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)
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 function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
return M return M

19
lux.toml Normal file
View File

@ -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"

View File

@ -6,7 +6,7 @@ describe('Buffer', function()
withbuf({}, function() withbuf({}, function()
local buf = Buffer.from_nr() local buf = Buffer.from_nr()
buf:all():replace 'bleh' buf:all():replace 'bleh'
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false) local actual_lines = vim.api.nvim_buf_get_lines(buf.bufnr, 0, -1, false)
assert.are.same({ 'bleh' }, actual_lines) assert.are.same({ 'bleh' }, actual_lines)
end) end)
end) end)
@ -18,8 +18,8 @@ describe('Buffer', function()
'three', 'three',
}, function() }, function()
local buf = Buffer.from_nr() local buf = Buffer.from_nr()
buf:lines(1, -2):replace 'too' buf:lines(2, -2):replace 'too'
local actual_lines = vim.api.nvim_buf_get_lines(buf.buf, 0, -1, false) local actual_lines = vim.api.nvim_buf_get_lines(buf.bufnr, 0, -1, false)
assert.are.same({ assert.are.same({
'one', 'one',
'too', 'too',

View File

@ -4,12 +4,12 @@ local withbuf = loadfile './spec/withbuf.lua'()
describe('Pos', function() describe('Pos', function()
it('get a char from a given position', function() it('get a char from a given position', function()
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
assert.are.same('a', Pos.new(nil, 0, 0):char()) assert.are.same('a', Pos.new(nil, 1, 1):char())
assert.are.same('d', Pos.new(nil, 0, 2):char()) assert.are.same('d', Pos.new(nil, 1, 3):char())
assert.are.same('f', Pos.new(nil, 0, 3):char()) assert.are.same('f', Pos.new(nil, 1, 4):char())
assert.are.same('a', Pos.new(nil, 2, 0):char()) assert.are.same('a', Pos.new(nil, 3, 1):char())
assert.are.same('', Pos.new(nil, 3, 0):char()) assert.are.same('', Pos.new(nil, 4, 1):char())
assert.are.same('o', Pos.new(nil, 4, 2):char()) assert.are.same('o', Pos.new(nil, 5, 3):char())
end) end)
end) end)
@ -23,47 +23,47 @@ describe('Pos', function()
it('get the next position', function() it('get the next position', function()
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
-- line 1: a => s -- 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 -- 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 -- 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 -- 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 -- 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 -- 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)
end) end)
it('get the previous position', function() it('get the previous position', function()
withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function() withbuf({ 'asdf', 'bleh', 'a', '', 'goo' }, function()
-- line 1: s => a -- 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 -- 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 -- 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 -- 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 -- 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 -- 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)
end) end)
it('find matching brackets', function() it('find matching brackets', function()
withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function() withbuf({ 'asdf ({} def <[{}]>) ;lkj' }, function()
-- outer parens are matched: -- 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): -- 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 -- 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: -- 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) end)
end) end)

View File

@ -28,7 +28,7 @@ describe('Range', function()
it('get from positions: v in single line', function() it('get from positions: v in single line', function()
withbuf({ 'line one', 'and line two' }, 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() local lines = range:lines()
assert.are.same({ 'ine' }, lines) assert.are.same({ 'ine' }, lines)
@ -39,7 +39,7 @@ describe('Range', function()
it('get from positions: v across multiple lines', 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() 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() local lines = range:lines()
assert.are.same({ 'quick brown fox', 'jumps' }, lines) assert.are.same({ 'quick brown fox', 'jumps' }, lines)
end) end)
@ -47,7 +47,7 @@ describe('Range', function()
it('get from positions: V', function() it('get from positions: V', function()
withbuf({ 'line one', 'and line two' }, 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() local lines = range:lines()
assert.are.same({ 'line one' }, lines) assert.are.same({ 'line one' }, lines)
@ -58,7 +58,7 @@ describe('Range', function()
it('get from positions: V across multiple lines', 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() 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() local lines = range:lines()
assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines) assert.are.same({ 'the quick brown fox', 'jumps over a lazy dog' }, lines)
end) end)
@ -66,7 +66,7 @@ describe('Range', function()
it('get from line', function() it('get from line', function()
withbuf({ 'line one', 'and line two' }, function() withbuf({ 'line one', 'and line two' }, function()
local range = Range.from_line(nil, 0) local range = Range.from_line(nil, 1)
local lines = range:lines() local lines = range:lines()
assert.are.same({ 'line one' }, lines) assert.are.same({ 'line one' }, lines)
@ -77,7 +77,7 @@ describe('Range', function()
it('get from lines', function() it('get from lines', function()
withbuf({ 'line one', 'and line two', 'and line 3' }, function() withbuf({ 'line one', 'and line two', 'and line 3' }, function()
local range = Range.from_lines(nil, 0, 1) local range = Range.from_lines(nil, 1, 2)
local lines = range:lines() local lines = range:lines()
assert.are.same({ 'line one', 'and line two' }, lines) assert.are.same({ 'line one', 'and line two' }, lines)
@ -88,35 +88,35 @@ describe('Range', function()
it('replace within line', function() it('replace within line', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post 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' range:replace 'quack'
local text = Range.from_line(nil, 1):text() local text = Range.from_line(nil, 2):text()
assert.are.same('the quack brown fox', text) assert.are.same('the quack brown fox', text)
end) end)
end) end)
it('delete within line', function() it('delete within line', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post 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 '' range:replace ''
local text = Range.from_line(nil, 1):text() local text = Range.from_line(nil, 2):text()
assert.are.same('the brown fox', text) assert.are.same('the brown fox', text)
end) end)
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post 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(nil) range:replace(nil)
local text = Range.from_line(nil, 1):text() local text = Range.from_line(nil, 2):text()
assert.are.same('the brown fox', text) assert.are.same('the brown fox', text)
end) end)
end) end)
it('replace across multiple lines: v', function() it('replace across multiple lines: v', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post 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, 2, 4), 'v') local range = Range.new(Pos.new(nil, 2, 5), Pos.new(nil, 3, 5), 'v')
range:replace 'plane flew' range:replace 'plane flew'
local lines = Range.from_buf_text():lines() local lines = Range.from_buf_text():lines()
@ -130,7 +130,7 @@ describe('Range', function()
it('replace a line', function() it('replace a line', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
local range = Range.from_line(nil, 1) local range = Range.from_line(nil, 2)
range:replace 'the rabbit' range:replace 'the rabbit'
local lines = Range.from_buf_text():lines() local lines = Range.from_buf_text():lines()
@ -145,7 +145,7 @@ describe('Range', function()
it('replace multiple lines', function() it('replace multiple lines', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
local range = Range.from_lines(nil, 1, 2) local range = Range.from_lines(nil, 2, 3)
range:replace 'the rabbit' range:replace 'the rabbit'
local lines = Range.from_buf_text():lines() local lines = Range.from_buf_text():lines()
@ -159,7 +159,7 @@ describe('Range', function()
it('delete single line', function() it('delete single line', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
local range = Range.from_line(nil, 1) local range = Range.from_line(nil, 2)
range:replace(nil) -- delete lines range:replace(nil) -- delete lines
local lines = Range.from_buf_text():lines() local lines = Range.from_buf_text():lines()
@ -173,7 +173,7 @@ describe('Range', function()
it('delete multiple lines', function() it('delete multiple lines', function()
withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function() withbuf({ 'pre line', 'the quick brown fox', 'jumps over a lazy dog', 'post line' }, function()
local range = Range.from_lines(nil, 1, 2) local range = Range.from_lines(nil, 2, 3)
range:replace(nil) -- delete lines range:replace(nil) -- delete lines
local lines = Range.from_buf_text():lines() local lines = Range.from_buf_text():lines()
@ -187,53 +187,53 @@ describe('Range', function()
it('text object: word', function() it('text object: word', function()
withbuf({ 'the quick brown fox' }, function() withbuf({ 'the quick brown fox' }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
assert.are.same('quick ', Range.from_text_object('aw'):text()) assert.are.same('quick ', Range.from_txtobj('aw'):text())
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
assert.are.same('quick', Range.from_text_object('iw'):text()) assert.are.same('quick', Range.from_txtobj('iw'):text())
end) end)
end) end)
it('text object: quote', function() it('text object: quote', function()
withbuf({ [[the "quick" brown fox]] }, function() withbuf({ [[the "quick" brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
assert.are.same('"quick"', Range.from_text_object('a"'):text()) assert.are.same('"quick"', Range.from_txtobj('a"'):text())
vim.fn.setpos('.', { 0, 1, 6, 0 }) vim.fn.setpos('.', { 0, 1, 6, 0 })
assert.are.same('quick', Range.from_text_object('i"'):text()) assert.are.same('quick', Range.from_txtobj('i"'):text())
end) end)
withbuf({ [[the 'quick' brown fox]] }, function() withbuf({ [[the 'quick' brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
assert.are.same("'quick'", Range.from_text_object([[a']]):text()) assert.are.same("'quick'", Range.from_txtobj([[a']]):text())
vim.fn.setpos('.', { 0, 1, 6, 0 }) vim.fn.setpos('.', { 0, 1, 6, 0 })
assert.are.same('quick', Range.from_text_object([[i']]):text()) assert.are.same('quick', Range.from_txtobj([[i']]):text())
end) end)
withbuf({ [[the `quick` brown fox]] }, function() withbuf({ [[the `quick` brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
assert.are.same('`quick`', Range.from_text_object([[a`]]):text()) assert.are.same('`quick`', Range.from_txtobj([[a`]]):text())
vim.fn.setpos('.', { 0, 1, 6, 0 }) vim.fn.setpos('.', { 0, 1, 6, 0 })
assert.are.same('quick', Range.from_text_object([[i`]]):text()) assert.are.same('quick', Range.from_txtobj([[i`]]):text())
end) end)
end) end)
it('text object: block', function() it('text object: block', function()
withbuf({ 'this is a {', 'block', '} here' }, function() withbuf({ 'this is a {', 'block', '} here' }, function()
vim.fn.setpos('.', { 0, 2, 1, 0 }) vim.fn.setpos('.', { 0, 2, 1, 0 })
assert.are.same('{\nblock\n}', Range.from_text_object('a{'):text()) assert.are.same('{\nblock\n}', Range.from_txtobj('a{'):text())
vim.fn.setpos('.', { 0, 2, 1, 0 }) vim.fn.setpos('.', { 0, 2, 1, 0 })
assert.are.same('block', Range.from_text_object('i{'):text()) assert.are.same('block', Range.from_txtobj('i{'):text())
end) end)
end) end)
it('text object: restores cursor position', function() it('text object: restores cursor position', function()
withbuf({ 'this is a {block} here' }, function() withbuf({ 'this is a {block} here' }, function()
vim.fn.setpos('.', { 0, 1, 13, 0 }) vim.fn.setpos('.', { 0, 1, 13, 0 })
assert.are.same('{block}', Range.from_text_object('a{'):text()) assert.are.same('{block}', Range.from_txtobj('a{'):text())
assert.are.same(vim.api.nvim_win_get_cursor(0), { 1, 12 }) assert.are.same(vim.api.nvim_win_get_cursor(0), { 1, 12 })
end) end)
end) end)
@ -258,22 +258,18 @@ describe('Range', function()
end) end)
end) end)
it('line0', function() it('line', function()
withbuf({ withbuf({
'this is a {', 'this is a {',
'block', 'block',
'} here', '} here',
}, function() }, function()
local range = Range.new(Pos.new(0, 0, 5), Pos.new(0, 1, 4), 'v') local range = Range.new(Pos.new(0, 1, 6), Pos.new(0, 2, 5), 'v')
local lfirst = range:line0(0) local lfirst = assert(range:line(1), 'lfirst null')
assert.are.same(5, lfirst.idx0.start) assert.are.same('is a {', lfirst:text())
assert.are.same(10, lfirst.idx0.stop) assert.are.same(Pos.new(0, 1, 6), lfirst.start)
assert.are.same(0, lfirst.lnum) assert.are.same(Pos.new(0, 1, 11), lfirst.stop)
assert.are.same('is a {', lfirst.text()) assert.are.same('block', range:line(2):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())
end) end)
end) end)
@ -297,16 +293,16 @@ describe('Range', function()
vim.cmd.normal 'v' -- enter visual mode vim.cmd.normal 'v' -- enter visual mode
vim.cmd.normal 'l' -- select one character to the right vim.cmd.normal 'l' -- select one character to the right
local range = Range.from_vtext() local range = Range.from_vtext()
assert.are.same(range.start, Pos.new(nil, 0, 2)) assert.are.same(range.start, Pos.new(nil, 1, 3))
assert.are.same(range.stop, Pos.new(nil, 0, 3)) assert.are.same(range.stop, Pos.new(nil, 1, 4))
assert.are.same(range.mode, 'v') assert.are.same(range.mode, 'v')
end) end)
end) end)
it('from_op_func', function() it('from_op_func', function()
withbuf({ 'line one', 'and line two' }, function() withbuf({ 'line one', 'and line two' }, function()
local a = Pos.new(nil, 0, 0) local a = Pos.new(nil, 1, 1)
local b = Pos.new(nil, 1, 1) local b = Pos.new(nil, 2, 2)
a:save_to_pos "'[" a:save_to_pos "'["
b:save_to_pos "']" b:save_to_pos "']"
@ -317,7 +313,7 @@ describe('Range', function()
range = Range.from_op_func 'line' range = Range.from_op_func 'line'
assert.are.same(range.start, a) 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') assert.are.same(range.mode, 'V')
end) end)
end) end)
@ -325,8 +321,8 @@ describe('Range', function()
it('from_cmd_args', function() it('from_cmd_args', function()
local args = { range = 1 } local args = { range = 1 }
withbuf({ 'line one', 'and line two' }, function() withbuf({ 'line one', 'and line two' }, function()
local a = Pos.new(nil, 0, 0) local a = Pos.new(nil, 1, 1)
local b = Pos.new(nil, 1, 1) local b = Pos.new(nil, 2, 2)
a:save_to_pos "'<" a:save_to_pos "'<"
b:save_to_pos "'>" b:save_to_pos "'>"
@ -341,25 +337,25 @@ describe('Range', function()
withbuf({ [[the "quick" brown fox]] }, function() withbuf({ [[the "quick" brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
local range = Range.find_nearest_quotes() local range = Range.find_nearest_quotes()
assert.are.same(range.start, Pos.new(nil, 0, 4)) assert.are.same(range.start, Pos.new(nil, 1, 5))
assert.are.same(range.stop, Pos.new(nil, 0, 10)) assert.are.same(range.stop, Pos.new(nil, 1, 11))
end) end)
withbuf({ [[the 'quick' brown fox]] }, function() withbuf({ [[the 'quick' brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 }) vim.fn.setpos('.', { 0, 1, 5, 0 })
local range = Range.find_nearest_quotes() local range = Range.find_nearest_quotes()
assert.are.same(range.start, Pos.new(nil, 0, 4)) assert.are.same(range.start, Pos.new(nil, 1, 5))
assert.are.same(range.stop, Pos.new(nil, 0, 10)) assert.are.same(range.stop, Pos.new(nil, 1, 11))
end) end)
end) end)
it('smallest', function() it('smallest', function()
local r1 = Range.new(Pos.new(nil, 0, 1), Pos.new(nil, 0, 3), 'v') local r1 = Range.new(Pos.new(nil, 1, 2), Pos.new(nil, 1, 4), 'v')
local r2 = Range.new(Pos.new(nil, 0, 2), Pos.new(nil, 0, 4), 'v') local r2 = Range.new(Pos.new(nil, 1, 3), Pos.new(nil, 1, 5), 'v')
local r3 = Range.new(Pos.new(nil, 0, 0), Pos.new(nil, 0, 5), 'v') local r3 = Range.new(Pos.new(nil, 1, 1), Pos.new(nil, 1, 6), 'v')
local smallest = Range.smallest { r1, r2, r3 } local smallest = Range.smallest { r1, r2, r3 }
assert.are.same(smallest.start, Pos.new(nil, 0, 1)) assert.are.same(smallest.start, Pos.new(nil, 1, 2))
assert.are.same(smallest.stop, Pos.new(nil, 0, 3)) assert.are.same(smallest.stop, Pos.new(nil, 1, 4))
end) end)
it('clone', function() it('clone', function()
@ -381,9 +377,9 @@ describe('Range', function()
it('to_linewise()', function() it('to_linewise()', function()
withbuf({ 'line one', 'and line two' }, 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() 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.stop.col, Pos.MAX_COL)
assert.are.same(linewise_range.mode, 'V') assert.are.same(linewise_range.mode, 'V')
end) end)
@ -391,56 +387,56 @@ describe('Range', function()
it('is_empty', function() it('is_empty', function()
withbuf({ 'line one', 'and line two' }, 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()) 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()) assert.is_false(range2:is_empty())
end) end)
end) end)
it('trim_start', function() it('trim_start', function()
withbuf({ ' line one', 'line two' }, 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() 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)
end) end)
it('trim_stop', function() it('trim_stop', function()
withbuf({ 'line one ', 'line two' }, 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() 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)
end) end)
it('contains', function() it('contains', function()
withbuf({ 'line one', 'and line two' }, 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 pos = Pos.new(nil, 0, 2) local pos = Pos.new(nil, 1, 3)
assert.is_true(range:contains(pos)) 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)) assert.is_false(range:contains(pos))
end) end)
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, 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) local shrunk = range:shrink(1)
assert.are.same(shrunk.start, Pos.new(nil, 0, 2)) assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2)) assert.are.same(shrunk.stop, Pos.new(nil, 3, 4))
end) end)
end) end)
it('must_shrink', function() it('must_shrink', function()
withbuf({ 'line one', 'and line two' }, 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) local shrunk = range:must_shrink(1)
assert.are.same(shrunk.start, Pos.new(nil, 0, 2)) assert.are.same(shrunk.start, Pos.new(nil, 2, 4))
assert.are.same(shrunk.stop, Pos.new(nil, 1, 2)) 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)
@ -448,25 +444,27 @@ describe('Range', function()
it('set_visual_selection', function() it('set_visual_selection', function()
withbuf({ 'line one', 'and line two' }, function() withbuf({ 'line one', 'and line two' }, function()
local range = Range.from_lines(nil, 0, 1) local range = Range.from_lines(nil, 1, 2)
range:set_visual_selection() range:set_visual_selection()
assert.are.same(Pos.from_pos 'v', Pos.new(nil, 0, 0)) assert.are.same(Pos.from_pos 'v', Pos.new(nil, 1, 1))
assert.are.same(Pos.from_pos '.', Pos.new(nil, 1, 11)) -- 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)
end) end)
it('selections set to past the EOL should not error', function() it('selections set to past the EOL should not error', function()
withbuf({ 'Rg SET NAMES' }, function() withbuf({ 'Rg SET NAMES' }, function()
local b = vim.api.nvim_get_current_buf() 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' r:replace 'bleh'
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
end) end)
withbuf({ 'Rg SET NAMES' }, function() withbuf({ 'Rg SET NAMES' }, function()
local b = vim.api.nvim_get_current_buf() 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' r:replace 'bleh'
assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false)) assert.are.same({ 'Rg bleh' }, vim.api.nvim_buf_get_lines(b, 0, -1, false))
end) end)
@ -475,7 +473,7 @@ describe('Range', function()
it('replace updates Range.stop: same line', function() it('replace updates Range.stop: same line', function()
withbuf({ 'The quick brown fox jumps over the lazy dog' }, function() withbuf({ 'The quick brown fox jumps over the lazy dog' }, function()
local b = vim.api.nvim_get_current_buf() 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' 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))
@ -491,11 +489,12 @@ describe('Range', function()
'over the lazy dog', 'over the lazy dog',
}, function() }, function()
local b = vim.api.nvim_get_current_buf() 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()) assert.are.same({ 'jumps', 'over' }, r:lines())
r:replace 'bleh1' 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' 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))
@ -511,7 +510,7 @@ describe('Range', function()
'the lazy dog', 'the lazy dog',
}, function() }, function()
local b = vim.api.nvim_get_current_buf() 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()) assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
r:replace { 'bleh1', 'bleh2' } r:replace { 'bleh1', 'bleh2' }
@ -540,7 +539,7 @@ describe('Range', function()
'the lazy dog', 'the lazy dog',
}, function() }, function()
local b = vim.api.nvim_get_current_buf() 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()) assert.are.same({ 'fox', 'jumps', 'over' }, r:lines())
r:replace(nil) r:replace(nil)