-
Notifications
You must be signed in to change notification settings - Fork 3
/
RegExHotstring.ahk
263 lines (246 loc) · 6.89 KB
/
RegExHotstring.ahk
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#Requires AutoHotkey v2.0
RegHook := RegExHk("VI")
RegHook.NotifyNonText := true
RegHook.VisibleText := false
RegHook.KeyOpt("{Space}{Tab}{Enter}{NumpadEnter}{BackSpace}", "+SN")
RegHook.Start()
/**
* Create a RegEx Hotstring or replace already existing one
* @param {String} String [RegEx string](https://www.autohotkey.com/docs/v2/misc/RegEx-QuickRef.htm)
* @param {Func or String} CallBack calls function with [RegExMatchInfo](https://www.autohotkey.com/docs/v2/lib/RegExMatch.htm#MatchObject)
* and array of additional params or replace string like [RegExReplace](https://www.autohotkey.com/docs/v2/lib/RegExReplace.htm)
* @param {String} Options A string of zero or more of the following options (in any order, with optional spaces in between)
*
* Use the following options follow by a zero to turn them off:
*
* `*` (asterisk): An ending character (e.g. Space, Tab, or Enter) is not required to trigger the hotstring.
*
* `?` (question mark): The hotstring will be triggered even when it is inside another word;
* that is, when the character typed immediately before it is alphanumeric.
*
* `B0` (B followed by a zero): Automatic backspacing is not done to erase the abbreviation you type.
* Use a plain B to turn backspacing back on after it was previously turned off.
*
* `C`: Case sensitive: When you type an abbreviation, it must exactly match the case defined in the script.
*
* `O`: Omit the ending character of auto-replace hotstrings when the replacement is produced.
*
* `T`: Use SendText instead of SendInput to send the replacement string.
* Only works when CallBack is a string.
*
* @param {Params} Params additional params pass to CallBack, check [Variadic functions](https://www.autohotkey.com/docs/v2/Functions.htm#Variadic)
* and [Variadic function calls](https://www.autohotkey.com/docs/v2/Functions.htm#VariadicCall), only works when CallBack is a function.
*/
RegExHotstring(String, CallBack, Options := "", OnOffToggle := "On", Params*) {
RegHook.Add(String, CallBack, Options, OnOffToggle, Params*)
}
class RegExHk extends InputHook {
; stores with RegEx string as key and obj as value
; "*0" option
a0 := Map()
; "*" option
a := Map()
/**
* Object for storing all the information
*/
class obj {
__New(string, call, options, on, params*) {
this.call := call
this.str := string
this.params := params
this.opt := Map("*", false, "?", false, "B", true, "C", false, "O", false, "T", false)
loop parse (options) {
switch A_LoopField {
case "*", "?", "B", "C", "O", "T":
this.opt[A_LoopField] := true
case "0":
try
this.opt[temp] := false
catch
throw ValueError("Unknown Option: " A_LoopField)
case " ":
continue
default:
throw ValueError("Unknown Option: " A_LoopField)
}
temp := A_LoopField
}
this.str := this.opt["?"] ? this.str "$" : "^" this.str "$"
this.str := this.opt["C"] ? this.str : "i)" this.str
switch on {
case "On", 1, true:
this.on := true
case "Off", 0, false:
this.on := false
case "Toggle", -1:
; toggle depands on the previous state, it's unreachable here, so defaults to true.
this.on := true
default:
throw ValueError("Unknown OnOffToggle: " on)
}
}
}
Add(String, CallBack, Options, OnOffToggle, Params*) {
toggle := false
switch OnOffToggle {
case "Toggle", -1:
toggle := true
}
info := RegExHk.obj(String, CallBack, Options, OnOffToggle, Params*)
if (info.opt["*"]) {
try
this.a0.Delete(String)
; end key is always omitted
info.opt["O"] := true
if (toggle) {
try
info.on := !this.a[String].on
}
this.a[String] := info
} else {
try
this.a.Delete(String)
if (toggle) {
try
info.on := !this.a0[String].on
}
this.a0[String] := info
}
}
OnKeyDown := this.keyDown
keyDown(vk, sc) {
switch vk {
case 8:
Send("{Blind}{vk08 down}")
case 9, 13, 32:
; clear input if not match
if (!this.match(this.a0,
SubStr(this.Input, 1, StrLen(this.Input) - 1),
(*) => Send("{Blind}{vk" Format("{:02x}", vk) " down}")
)) {
this.Stop()
this.Start()
s.Restart()
}
case 160, 161:
; do nothing on shift key
default:
; clear input when press non-text key
this.Stop()
this.Start()
s.Restart()
}
}
OnKeyUp := this.keyUp
keyUp(vk, sc) {
switch vk {
case 8, 9, 13, 32:
Send("{Blind}{vk" Format("{:02x}", vk) " up}")
}
}
OnChar := this.char
char(c) {
blind := StrLen(c) > 1 ? "" : "{Blind}"
loop parse c {
c := A_LoopField
vk := GetKeyVK(GetKeyName(c))
switch vk {
case 9, 13, 32:
return
}
; if capslock is on, convert to lower case
GetKeyState("CapsLock", "T") ? c := StrLower(c) : 0
SendLevel(A_SendLevel) ; WTF is this needed?
this.match(this.a, , (*) => Send(blind "{" c " down}"), 1)
OnKeyUp(c, (*) => Send(blind "{" c " up}"))
}
}
/**
* Function for matching and executing
* @param map Map to search for RegEx string
* @param {String} input Input string
* @param {(*) => void} defer What to do if no match or `O` is `false`
* @param {Integer} a Backspace count offset `match.Len[0] - a`
* @returns {Boolean} If match found
*/
match(map, input := this.Input, defer := (*) => 0, a := 0) {
; debug use
; ToolTip(this.Input)
if (!map.Count) {
defer()
return false
}
; loop through each strings and find the first match
for , obj in map {
if (!obj.on)
continue
str := obj.str
call := obj.call
opt := obj.opt
params := obj.params
start := RegExMatch(input, str, &match)
; if match, send replace or call function
if (start) {
if (opt["B"])
Send("{BS " match.Len[0] - a "}")
else
defer()
if (call is String) {
this.Stop()
if (opt["T"]) {
SendText(RegExReplace(SubStr(input, start), str, call))
} else {
Send(RegExReplace(SubStr(input, start), str, call))
}
if (!opt["O"])
defer()
this.Start()
s.Restart()
} else if (call is Func) {
this.Stop()
call(match, params*)
this.Start()
s.Restart()
} else
throw TypeError('CallBack should be "Func" or "String"')
return true
}
}
defer()
return false
}
}
class Store extends Map {
Restart() {
for _, v in this {
v.Stop()
v.Start()
}
}
}
s := Store()
/**
* Call function when key is up, for the same `c` only first `Callback` will be called, not for general use.
*
* Fix: Restarting the RegHook will put it on the top of the input stack, preventing the keyup event from being triggered.
* @param c Character to listen
* @param Callback Callback function
*/
OnKeyUp(c, Callback) {
if s.Has(c) {
return
}
hook := s[c] := InputHook("VI")
hook.KeyOpt(c, "N")
hook.MinSendLevel := RegHook.MinSendLevel
hook.OnKeyUp := KeyUp
hook.Dispose := (*) => s.Delete(c)
hook.Start()
; ToolTip(s.Count)
KeyUp(ih, *) {
Callback()
ih.Stop()
ih.Dispose()
ih := ""
}
}