diff --git a/examples/splitjoin.lua b/examples/splitjoin.lua new file mode 100644 index 0000000..d772992 --- /dev/null +++ b/examples/splitjoin.lua @@ -0,0 +1,86 @@ +local vim_repeat = require 'u.repeat' +local CodeWriter = require 'u.codewriter' +local Range = require 'u.range' + +local M = {} + +---@param bracket_range Range +---@param left string +---@param right string +local function split(bracket_range, left, right) + local code = CodeWriter.from_pos(bracket_range.start) + code:write_raw(left) + + local curr = bracket_range.start:next() + if curr == nil then return end + local last_start = curr + + -- Sanity check: if we "skipped" past the start/stop of the overall range, then something is wrong: + -- This can happen with greater-/less- than signs that are mathematical, and not brackets: + while curr > bracket_range.start and curr < bracket_range.stop do + if vim.tbl_contains({ '{', '[', '(', '<' }, curr:char()) then + -- skip over bracketed groups: + local next = curr:find_match() + if next == nil then break end + curr = next + else + if vim.tbl_contains({ ',', ';' }, curr:char()) then + -- accumulate item: + local item = vim.trim(Range.new(last_start, curr):text()) + if item ~= '' then code:indent():write(item) end + + local next_last_start = curr:next() + if next_last_start == nil then break end + last_start = next_last_start + end + + -- Prepare for next iteration: + local next = curr:next() + if next == nil then break end + curr = next + end + end + + -- accumulate last, unfinished item: + local pos_before_right = bracket_range.stop:must_next(-1) + if last_start < pos_before_right then + local item = vim.trim(Range.new(last_start, pos_before_right):text()) + if item ~= '' then code:indent():write(item) end + end + + code:write(right) + bracket_range:replace(code.lines) +end + +---@param bracket_range Range +---@param left string +---@param right string +local function join(bracket_range, left, right) + local inner_range = Range.new(bracket_range.start:must_next(), bracket_range.stop:must_next(-1), bracket_range.mode) + local newline = vim + .iter(inner_range:lines()) + :map(function(l) return vim.trim(l) end) + :filter(function(l) return l ~= '' end) + :join ' ' + bracket_range:replace { left .. newline .. right } +end + +local function splitjoin() + local bracket_range = Range.find_nearest_brackets() + if bracket_range == nil then return end + local lines = bracket_range:lines() + local left = lines[1]:sub(1, 1) -- left bracket + local right = lines[#lines]:sub(-1, -1) -- right bracket + + if #lines == 1 then + split(bracket_range, left, right) + else + join(bracket_range, left, right) + end +end + +function M.setup() + vim.keymap.set('n', 'gS', function() vim_repeat.run(splitjoin) end) +end + +return M diff --git a/examples/surround.lua b/examples/surround.lua new file mode 100644 index 0000000..f9aa76f --- /dev/null +++ b/examples/surround.lua @@ -0,0 +1,240 @@ +local vim_repeat = require 'u.repeat' +local opkeymap = require 'u.opkeymap' +local Pos = require 'u.pos' +local Range = require 'u.range' +local Buffer = require 'u.buffer' +local CodeWriter = require 'u.codewriter' + +local M = {} + +local surrounds = { + [')'] = { left = '(', right = ')' }, + ['('] = { left = '( ', right = ' )' }, + [']'] = { left = '[', right = ']' }, + ['['] = { left = '[ ', right = ' ]' }, + ['}'] = { left = '{', right = '}' }, + ['{'] = { left = '{ ', right = ' }' }, + ['>'] = { left = '<', right = '>' }, + ['<'] = { left = '< ', right = ' >' }, + ["'"] = { left = "'", right = "'" }, + ['"'] = { left = '"', right = '"' }, + ['`'] = { left = '`', right = '`' }, +} + +---@return { left: string; right: string }|nil +local function prompt_for_bounds() + local cn = vim.fn.getchar() + -- Check for non-printable characters: + if type(cn) ~= 'number' or cn < 32 or cn > 126 then return end + local c = vim.fn.nr2char(cn) + + if c == '<' then + -- Surround with a tag: + vim.keymap.set('c', '>', '>') + local tag = '<' .. vim.fn.input '<' + if tag == '<' then return end + vim.keymap.del('c', '>') + local endtag = ']*' .. '>' + -- selene: allow(global_usage) + return { left = tag, right = endtag } + else + -- Default surround: + return (surrounds)[c] or { left = c, right = c } + end +end + +---@param range Range +---@param bounds { left: string; right: string } +local function do_surround(range, bounds) + local left = bounds.left + local right = bounds.right + if range.mode == 'V' then + -- If we are surrounding multiple lines, we don't care about + -- space-padding: + left = vim.trim(left) + right = vim.trim(right) + end + + if range.mode == 'v' then + range:replace(left .. range:text() .. right) + elseif range.mode == 'V' then + local buf = Buffer.current() + local cw = CodeWriter.from_line(buf:line0(range.start.lnum):text(), buf.buf) + + -- write the left bound at the current indent level: + cw:write(left) + + local curr_ident_prefix = cw.indent_str:rep(cw.indent_level) + cw:indent(function(cw2) + for _, line in ipairs(range:lines()) do + -- trim the current indent prefix from the line: + if line:sub(1, #curr_ident_prefix) == curr_ident_prefix then + -- + line = line:sub(#curr_ident_prefix + 1) + end + + cw2:write(line) + end + end) + + -- write the right bound at the current indent level: + cw:write(right) + + range:replace(cw.lines) + end + + range.start:save_to_pos '.' +end + +function M.setup() + require('u.repeat').setup() + + -- Visual + vim.keymap.set('v', 'S', function() + local c = vim.fn.getcharstr() + local range = Range.from_vtext() + local bounds = surrounds[c] or { left = c, right = c } + vim_repeat.run(function() + do_surround(range, bounds) + -- this is a visual mapping: end in normal mode: + vim.cmd { cmd = 'normal', args = { '' }, bang = true } + end) + end, { noremap = true, silent = true }) + + -- Change + vim.keymap.set('n', 'cs', function() + local from_cn = vim.fn.getchar() + -- Check for non-printable characters: + if from_cn < 32 or from_cn > 126 then return end + local from_c = vim.fn.nr2char(from_cn) + local from = surrounds[from_c] or { left = from_c, right = from_c } + + local arange = Range.from_text_object('a' .. from_c, { user_defined = true }) + if arange == nil then return nil end + local hl_info1 = Range.new(arange.start, arange.start, 'v'):highlight('IncSearch', { priority = 999 }) + local hl_info2 = Range.new(arange.stop, arange.stop, 'v'):highlight('IncSearch', { priority = 999 }) + local hl_clear = function() + hl_info1.clear() + hl_info2.clear() + end + + local to = prompt_for_bounds() + hl_clear() + if to == nil then return end + + vim_repeat.run(function() + if from_c == 't' then + -- For tags, we want to replace the inner text, not the tag: + local irange = Range.from_text_object('i' .. from_c, { user_defined = true }) + if arange == nil or irange == nil then return nil end + + local lrange = Range.new(arange.start, irange.start:must_next(-1)) + local rrange = Range.new(irange.stop:must_next(1), arange.stop) + + rrange:replace(to.right) + lrange:replace(to.left) + else + -- replace `from.right` with `to.right`: + local last_line = arange:line0(-1).text() --[[@as string]] + local from_right_match = last_line:match(vim.pesc(from.right) .. '$') + if from_right_match then + local match_start = arange.stop:clone() + match_start.col = match_start.col - #from_right_match + 1 + Range.new(match_start, arange.stop):replace(to.right) + end + + -- replace `from.left` with `to.left`: + local first_line = arange:line0(0).text() --[[@as string]] + local from_left_match = first_line:match('^' .. vim.pesc(from.left)) + if from_left_match then + local match_end = arange.start:clone() + match_end.col = match_end.col + #from_left_match - 1 + Range.new(arange.start, match_end):replace(to.left) + end + end + end) + end, { noremap = true, silent = true }) + + -- Delete + vim.keymap.set('n', 'ds', function() + local txt_obj = vim.fn.getcharstr() + vim_repeat.run(function() + local buf = Buffer.current() + local irange = Range.from_text_object('i' .. txt_obj) + local arange = Range.from_text_object('a' .. txt_obj) + if arange == nil or irange == nil then return nil end + local starting_cursor_pos = arange.start:clone() + + -- Now, replace `arange` with the content of `irange`. If `arange` was multiple lines, + -- dedent the contents first, and operate in linewise mode + if arange.start.lnum ~= arange.stop.lnum then + -- Auto dedent: + vim.cmd.normal(' stop_prev then return start_next end + + local range = range_or_pos:shrink(1) + return range + else + return range_or_pos + end + end) + end + define_quote_obj [["]] + define_quote_obj [[']] + define_quote_obj [[`]] + + ---Selects the "last" quote object (searches backward) + ---@param q string + local function define_last_quote_obj(q) + local function select_around() + local curr = Pos.from_pos('.'):find_next(-1, q) + if not curr then return end + -- Reset visual selection to current context: + Range.new(curr, curr):set_visual_selection() + vim.cmd.normal('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 + + utils.define_text_object('al' .. q, function() return select_around() end) + utils.define_text_object('il' .. q, function() + local range_or_pos = select_around() + if range_or_pos == nil then return end + + if Range.is(range_or_pos) then + 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 + define_last_quote_obj [["]] + define_last_quote_obj [[']] + define_last_quote_obj [[`]] + + -- Selects the "last" bracket object (searches backward): + local function define_last_bracket_obj(b, ...) + local function select_around() + local curr = Pos.from_pos('.'):find_next(-1, b) + if not curr then return end + + local other = curr:find_match(1000) + if not other then return end + + return Range.new(curr, other) + end + + local keybinds = { ... } + table.insert(keybinds, b) + for _, k in ipairs(keybinds) do + utils.define_text_object('al' .. k, function() return select_around() end) + utils.define_text_object('il' .. k, function() + local range = select_around() + return range and range:shrink(1) + end) + end + end + define_last_bracket_obj('}', 'B') + define_last_bracket_obj ']' + define_last_bracket_obj(')', 'b') + define_last_bracket_obj '>' +end +return M