-- Author: Georgi Kirilov
--
-- You can contact me via email to the posteo.net domain.
-- The local-part is the Z code for "Place a competent operator on this circuit."
require("vis")
local vis = 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(handler)
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("<C-a>", toggle(increment), ord_next, nil, "Toggle/increment word or number", true)
operator_new("<C-x>", toggle(decrement), ord_next, nil, "Toggle/decrement word or number", true)
operator_new("~", toggle(case, true), nil, VIS_MOVE_CHAR_NEXT, "Toggle case of character or selection")
operator_new("g~", toggle(case), nil, nil, "Toggle-case operator")
operator_new("gu", toggle(string.lower), nil, nil, "Lower-case operator")
operator_new("gU", toggle(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