aboutsummaryrefslogblamecommitdiffstats
path: root/init.lua
blob: c69e248b9437e67d9d00bd45aab84469b7e990c7 (plain) (tree)
1
2
3
4
5
6
7
8
9
10







                                                                                 

               
                                                                                                 
 

                    
                       


                                                                                                             
                 



                                                                        

           

                             
 



                                                
                                                               

                                                                          








                                                            

                                                                               
                                                                                                  




                                                              













                                                                     
                   





                                                                       


















                                                                                                               






                                                                                                           
                                            




                                                              



                                                       
                                                                         















                                                              
                                                        
                            
                                                                
           


                              
                                     
                               


                                                
                                                       

                   






                                                            
                       


                                                
                                                         





                                                                                                                            
                                                    




                                                

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