aboutsummaryrefslogtreecommitdiffstats
path: root/init.lua
blob: 4bac6b01a93f6c68ff89302ebf16ec7a1b2b9f29 (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
-- 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 Cc, Cmt, Cp, P, R, V = l.Cc, l.Cmt, l.Cp, l.P, l.R, l.V

local M = {}

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

local char_next = 17

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 dec_num = P"-"^-1 * R"09"^1
	local patt = Cp() * dec_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)
	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)
		return number and neighbor or 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 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, help)
	if not novisual then
		vis:map(vis.modes.VISUAL, key, binding, 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,        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)
	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 function(config)
	local ext_config = require(config)
	M = ext_config or M
	return M
end