aboutsummaryrefslogtreecommitdiffstats
path: root/init.lua
blob: b810bd99c9d518cdea89e448103367530770ba63 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
-- 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("<C-a>", increment,    ord_next,   nil,                "Toggle/increment word or number", true)
	operator_new("<C-x>", 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