Squash commits
[vis-toggler.git] / init.lua
blob6880ce521be676266ebbf628ece6b4bb77853350
1 -- SPDX-License-Identifier: GPL-3.0-or-later
2 -- © 2020 Georgi Kirilov
4 require("vis")
5 local vis = vis
7 require("lpeg")
8 local l = lpeg
9 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
11 local progname = ...
13 local M = {config = {}}
15 -- Inverted and unrolled config table.
16 -- With this table format, make_shift() can use the same logic for both numeric and word increment/decrement.
17 local lookup = {}
19 -- Pattern that matches any one of the words listed in the config table.
20 local ordinal_words
22 local count
24 -- copied from vis.h
25 local VIS_MOVE_CHAR_NEXT = 17
27 local dec_digit = R"09"
28 local hex_digit = R("09", "af", "AF")
29 local zeros = P"0"^0 / function(c) return #c end
31 local function at_or_after(_, _, pos, start, finish, is_number)
32 if pos >= start and pos < finish or is_number and pos < start then
33 return true, start, finish
34 end
35 end
37 local function ordinal(win, pos)
38 local selection
39 for s in win:selections_iterator() do
40 if s.pos == pos then selection = s break end
41 end
42 local line = win.file.lines[selection.line]
43 local num = S"-+"^-1 * ((P"0x" + "0X") * hex_digit^1 + dec_digit^1)
44 local patt = Cp() * num * Cp() * Cc(true) + Cp() * ordinal_words * Cp()
45 local start, finish = P{Cmt(Cc(selection.col) * patt, at_or_after) + 1 * V(1)}:match(line)
46 if not (start and finish) then return end
47 local line_begin = selection.pos - selection.col + 1
48 return line_begin + start - 1, line_begin + finish - 1
49 end
51 local function toggle(func, motion)
52 return function(file, range, pos)
53 local word = file:content(range)
54 local toggled = func(word, count)
55 if toggled then
56 file:delete(range)
57 file:insert(range.start, toggled)
58 return motion and range.finish or range.start
59 end
60 return pos
61 end
62 end
64 local function make_shift(shift)
65 local upper
66 return function(word, delta)
67 local number = tonumber(word)
68 local iter = number and {number} or lookup[word]
69 if not iter then return end
70 local binary = iter[2] and #iter[2] == 2
71 local neighbor, rotate = shift(iter[1], iter[2], delta)
72 if number then
73 local num = Ct(S"-+"^-1 * (Cg(P"0x" + "0X", "base") * zeros * C(hex_digit^1)^-1 +
74 zeros * C(dec_digit^1)^-1))
75 local groups = num:match(word)
76 local digits = groups[2] and #groups[2] or 0
77 local sign = neighbor < 0 and "-" or ""
78 local has_letters = groups[2] and groups[2]:find"%a"
79 if has_letters then
80 upper = groups[2]:find"%u"
81 elseif groups.base == "0X" then
82 upper = true
83 end
84 local hexfmt = upper and "%X" or "%x"
85 local abs = string.format(groups.base and hexfmt or "%d", math.abs(neighbor))
86 local dzero = #tostring(abs) - digits
87 local base = groups.base or ""
88 return sign .. base .. string.rep("0", groups[1] > 0 and groups[1] - dzero or 0) .. abs
89 end
90 return iter[2][neighbor] or binary and iter[2][rotate]
91 end
92 end
94 local increment = make_shift(function(i, _, delta) return i + (delta or 1), 1 end)
95 local decrement = make_shift(function(i, options, delta) return i - (delta or 1), options and #options end)
97 local function case(str)
98 return str:gsub("%a", function(char)
99 local lower = char:lower()
100 return char == lower and char:upper() or lower
101 end)
104 local function h(msg)
105 return string.format("|@%s| %s", progname, msg)
108 local function operator_new(key, handler, object, motion, help, novisual)
109 local id = vis:operator_register(toggle(handler, motion))
110 if id < 0 then
111 return false
113 local function binding()
114 vis:operator(id)
115 if vis.mode == vis.modes.OPERATOR_PENDING then
116 if object then
117 count = vis.count
118 vis.count = nil
119 vis:textobject(object)
120 elseif motion then
121 vis:motion(motion)
125 vis:map(vis.modes.NORMAL, key, binding, h(help))
126 if not novisual then
127 vis:map(vis.modes.VISUAL, key, binding, h(help))
131 local function preprocess(tbl)
132 local cfg, ord = {}, P(false)
133 local longer_first = {}
134 for _, options in ipairs(tbl) do
135 for i, key in ipairs(options) do
136 cfg[key] = {i, options}
137 table.insert(longer_first, key)
140 table.sort(longer_first, function(f, s)
141 local flen, slen = #f, #s
142 return flen > slen or flen == slen and f < s
143 end)
144 for _, key in ipairs(longer_first) do
145 ord = ord + key
147 return cfg, ord
150 vis.events.subscribe(vis.events.INIT, function()
151 local ord_next = vis:textobject_register(ordinal)
152 operator_new("<C-a>", increment, ord_next, nil, "Toggle/increment word or number", true)
153 operator_new("<C-x>", decrement, ord_next, nil, "Toggle/decrement word or number", true)
154 operator_new("~", case, nil, VIS_MOVE_CHAR_NEXT, "Toggle case of character or selection")
155 operator_new("g~", case, nil, nil, "Toggle-case operator")
156 operator_new("gu", string.lower, nil, nil, "Lower-case operator")
157 operator_new("gU", string.upper, nil, nil, "Upper-case operator")
158 lookup, ordinal_words = preprocess(M.config)
159 vis:map(vis.modes.NORMAL, "g~~", "g~il")
160 vis:map(vis.modes.NORMAL, "guu", "guil")
161 vis:map(vis.modes.NORMAL, "gUU", "gUil")
162 vis:map(vis.modes.VISUAL, "u", "gu")
163 vis:map(vis.modes.VISUAL, "U", "gU")
164 end)
166 return M