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
|