-- SPDX-License-Identifier: GPL-3.0-or-later -- © 2020 Georgi Kirilov require("vis") require("lpeg") local l = lpeg local C, Cc, Ct, Cmt, Cp, Cg, P, R, S, V = l.C, l.Cc, l.Ct, l.Cmt, l.Cp, l.Cg, l.P, l.R, l.S, l.V local progname = ... local M = {config = {}} -- Inverted and unrolled config table. -- With this table format, make_shift() can use the same logic for both numeric and word increment/decrement. local lookup = {} -- Pattern that matches any one of the words listed in the config table. local ordinal_words local count -- copied from vis.h local VIS_MOVE_CHAR_NEXT = 17 local dec_digit = R"09" local hex_digit = R("09", "af", "AF") local zeros = P"0"^0 / function(c) return #c end local function at_or_after(_, _, pos, start, finish, is_number) if pos >= start and pos < finish or is_number and pos < start then return true, start, finish end end local function ordinal(win, pos) local selection for s in win:selections_iterator() do if s.pos == pos then selection = s break end end local line = win.file.lines[selection.line] local num = S"-+"^-1 * ((P"0x" + "0X") * hex_digit^1 + dec_digit^1) local patt = Cp() * num * Cp() * Cc(true) + Cp() * ordinal_words * Cp() local start, finish = P{Cmt(Cc(selection.col) * patt, at_or_after) + 1 * V(1)}:match(line) if not (start and finish) then return end local line_begin = selection.pos - selection.col + 1 return line_begin + start - 1, line_begin + finish - 1 end local function toggle(func, motion) return function(file, range, pos) local word = file:content(range) local toggled = func(word, count) if toggled then file:delete(range) file:insert(range.start, toggled) return motion and range.finish or range.start end return pos end end local function make_shift(shift) local upper return function(word, delta) local number = tonumber(word) local iter = number and {number} or lookup[word] if not iter then return end local binary = iter[2] and #iter[2] == 2 local neighbor, rotate = shift(iter[1], iter[2], delta) if number then local num = Ct(S"-+"^-1 * (Cg(P"0x" + "0X", "base") * zeros * C(hex_digit^1)^-1 + zeros * C(dec_digit^1)^-1)) local groups = num:match(word) local digits = groups[2] and #groups[2] or 0 local sign = neighbor < 0 and "-" or "" local has_letters = groups[2] and groups[2]:find"%a" if has_letters then upper = groups[2]:find"%u" elseif groups.base == "0X" then upper = true end local hexfmt = upper and "%X" or "%x" local abs = string.format(groups.base and hexfmt or "%d", math.abs(neighbor)) local dzero = #tostring(abs) - digits local base = groups.base or "" return sign .. base .. string.rep("0", groups[1] > 0 and groups[1] - dzero or 0) .. abs end return iter[2][neighbor] or binary and iter[2][rotate] end end local increment = make_shift(function(i, _, delta) return i + (delta or 1), 1 end) local decrement = make_shift(function(i, options, delta) return i - (delta or 1), options and #options end) local function case(str) return str:gsub("%a", function(char) local lower = char:lower() return char == lower and char:upper() or lower end) end local function h(msg) return string.format("|@%s| %s", progname, msg) end local function operator_new(key, handler, object, motion, help, novisual) local id = vis:operator_register(toggle(handler, motion)) if id < 0 then return false end local function binding() vis:operator(id) if vis.mode == vis.modes.OPERATOR_PENDING then if object then count = vis.count vis.count = nil vis:textobject(object) elseif motion then vis:motion(motion) end end end vis:map(vis.modes.NORMAL, key, binding, h(help)) if not novisual then vis:map(vis.modes.VISUAL, key, binding, h(help)) end end local function preprocess(tbl) local cfg, ord = {}, P(false) local longer_first = {} for _, options in ipairs(tbl) do for i, key in ipairs(options) do cfg[key] = {i, options} table.insert(longer_first, key) end end table.sort(longer_first, function(f, s) local flen, slen = #f, #s return flen > slen or flen == slen and f < s end) for _, key in ipairs(longer_first) do ord = ord + key end return cfg, ord end vis.events.subscribe(vis.events.INIT, function() local ord_next = vis:textobject_register(ordinal) operator_new("", increment, ord_next, nil, "Toggle/increment word or number", true) operator_new("", decrement, ord_next, nil, "Toggle/decrement word or number", true) operator_new("~", case, nil, VIS_MOVE_CHAR_NEXT, "Toggle case of character or selection") operator_new("g~", case, nil, nil, "Toggle-case operator") operator_new("gu", string.lower, nil, nil, "Lower-case operator") operator_new("gU", string.upper, nil, nil, "Upper-case operator") lookup, ordinal_words = preprocess(M.config) vis:map(vis.modes.NORMAL, "g~~", "g~il") vis:map(vis.modes.NORMAL, "guu", "guil") vis:map(vis.modes.NORMAL, "gUU", "gUil") vis:map(vis.modes.VISUAL, "u", "gu") vis:map(vis.modes.VISUAL, "U", "gU") end) return M