hotstring icon indicating copy to clipboard operation
hotstring copied to clipboard

A few bugs

Open mviens opened this issue 9 years ago • 4 comments

For the following script, I pasted your code to the bottom and ran it. Many of the tests I created are working, but a few do not.

#NoEnv
#SingleInstance force
#Warn

; these are working
; -----------------
Hotstring("btw", "by the way")                      ; simple text replacement
Hotstring("ahk", "autohotkey")                      ; case-insensitive
Hotstring("trigger1", "replace")                    ; case-insensitive
Hotstring("(\d+)\/(\d+)%", "percent", 3)            ; regex mode  -->  (2/2% -> 100)  or  (70/100% -> 70%)
Hotstring("i)((d|w)on)(t)", "$1'$3",3)              ; DONT --> DON'T,  dont --> don't,  dOnt --> dOn't,  WONT --> WON'T
Hotstring("toLabel", "label")                       ; call a label
Hotstring("i)regex_(trigger)", "replace2", 3)       ; regex with capturing
Hotstring("only1", "onlyOnce")                      ; only works one time!

; these are NOT working
; -----------------
Hotstring("Abc", "Alphabet", 2)                     ; case-sensitive - (BUG: does not erase the triggering text)
Hotstring("Trigger2", "replace", 2)                 ; case-sensitive - (BUG: the value of $ is empty)
Hotstring("i)colou?rs","$0 $0 everywhere!", 3)      ; Regex, Case insensitive - (BUG: the $0 is not replaced, it uses the exact string)
Hotstring("(B|b)i(\d+)(\r\n|\n|\r)", "hsBackInX")   ; back in X minutes (does not trigger at all - but is valid regex for "bi10{cr}" or "Bi5{crlf}" or "bi20{lf}")

return

label:
    ; $ == "toLabel"
    MsgBox % "In the 'label' code... - triggered by [" . $ . "]"
    return

percent:
    ; now $ is a match object.
    sendInput, % Round(($.Value(1)/$.Value(2))*100) . "%"
    return

replace($) {
    ; $ == 'trigger'
    MsgBox % "'" . $ . "' was entered!"
}

replace2($) {
    ;$ is a match Object
    Msgbox % $.Value(0) . " was entered."
    Msgbox % $.Value(1) . " == 'trigger'"
}

onlyOnce($) {
    MsgBox % "This will only be shown once..."
    Hotstring("only1", "")
}

hsBackInX($) {
;    global $
;    addHotString()
    ;sendText($.1 . "ack in " . $.2 . " minutes...")
    msgBox % "Back in " . $.Value(2) . "minutes..."
}

mviens avatar Jun 21 '15 01:06 mviens

I would like to add that this library is truly great. I needed the ability to create hotstrings from a file, and this gives me the ability to simulate some of the features of defining a hotstring directly within AHK. It also let me workaround the limitations of the (very old and buggy) Regex Dynamic HotString (http://www.autohotkey.com/board/topic/114764-regex-dynamic-hotstrings/).

So, a HUGE thank you for creating and sharing this.

mviens avatar Jun 21 '15 02:06 mviens

I have fixed the bugs, and did a little code cleanup. I hope someone finds this useful.

#NoEnv
#SingleInstance force
#Warn

;simple text
Hotstring("btw1", "by the way (case-insensitive)", 1)                                   ; simple text replacement (case-insensitive)                                type:  BTw1
Hotstring("Btw2", "by the way (case-sensitive)", 2)                                     ; simple text replacement (case-sensitive)                                  type:  Btw2
Hotstring("bTw3", "by the way (case-sensitive regex)", 3)                               ; simple text replacement (regex case-sensitive)                            type:  bTw3
Hotstring("i)btw4", "by the way (case-insensitive regex)", 3)                           ; simple text replacement (regex case-insensitive)                          type:  btW4
Hotstring("btw5", "You typed: '$0'", 3)                                                 ; simple text replacement (regex case-insensitive with back-references)     type:  btw5
Hotstring("btw6", " is a TLA for 'by the way' (no clearing, case-insensitive)", 1, 0)   ; no replacement (case-insensitive)                                         type:  BTW6
Hotstring("btW7", " is a TLA for 'by the way' (no clearing, case-sensitive)", 2, 0)     ; no replacement (case-insensitive)                                         type:  btW7
Hotstring("bTw8", " for... (no clearing, case-sensitive regex)", 3, 0)                  ; no replacement (regex case-sensitive)                                     type:  bTw8
Hotstring("i)btw9", " by the way (case-insensitive regex)", 3, 0)                       ; no replacement (regex case-insensitive)                                   type:  btW9
Hotstring("btwA", ", here is your back-ref: '$0' (case-sensitive regex)", 3, 0)         ; no replacement (regex case-sensitive with back-references)                type:  btwA
Hotstring("i)btwB", ", here is your back-ref: '$0' (case-insensitive regex)", 3, 0)     ; no replacement (regex case-insensitive with back-references)              type:  BtWb
Hotstring("i)colou?rs","$0 $0 everywhere!", 3)      ; Regex, Case insensitive
Hotstring("Abc", "Alphabet", 2)                     ; case-sensitive
Hotstring("i)((d|w)on)(t)", "$1'$3", 3)             ; regex, text, replace, back-references

;label
Hotstring("toLabel", "label", 1)                    ; call a label


;label with back references
Hotstring("(\d+)\/(\d+)%", "percent", 3)            ; regex mode  -->  (5/20% -> 25%)  or  (70/100% -> 70%)


;function
Hotstring("trigger1", "replace")                    ; case-insensitive
Hotstring("Trigger2", "replace", 2)                 ; case-sensitive
Hotstring("i)trigger3", "replace", 3)               ; case-insensitive regex


;function with back references
Hotstring("i)regex_(trigger)", "replace2", 3)       ; regex with capturing


; custom regex
Hotstring("(B|b)i(\d+)(\r\n|\n|\r)", "hsBackInX", 3) ; back in X minutes - valid "bi10{cr}" or "Bi5{crlf}" or "bi20{lf}"


;only run once
Hotstring("only1", "onlyOnce")                      ; only works one time!

;all done, wait for input
return


label:
    ; $ == "toLabel"
    MsgBox % "In the 'label' code... - triggered by [" . $ . "]"
    return

percent:
    ; now $ is a match object.
    sendInput, % Round(($.Value(1)/$.Value(2))*100) . "%"
    return

replace($) {
    ; $ == 'trigger'
    MsgBox % "'" . $ . "' was entered!"
}

replace2($) {
    ;$ is a match Object
    Msgbox % $.Value(0) . " was entered."
    Msgbox % $.Value(1) . " == 'trigger'"
}

onlyOnce($) {
    MsgBox % "This will only be shown once..."
    Hotstring("only1", "")
}

hsBackInX($) {
    MsgBox % "Back in " . $.Value(2) . " minutes..."
}

Hotstring(trigger, label, mode:=1, clearTrigger:=1, cond:= "") {
    global $
    static keysBound := false
    static hotkeyPrefix := "~$"
    static hotstrings := {}
    static typed := ""
    static keys := {
        (LTrim Join
            symbols: "!""#$%&'()*+,-./:;<=>?@[\]^_``{|}~",
            num: "0123456789",
            alpha: "abcdefghijklmnopqrstuvwxyz",
            other: "BS,Return,Tab,Space",
            breakKeys: "Left,Right,Up,Down,Home,End,RButton,LButton,LControl,RControl,LAlt,RAlt,AppsKey,Lwin,Rwin,WheelDown,WheelUp,f1,f2,f3,f4,f5,f6,f7,f8,f9,f6,f7,f9,f10,f11,f12",
            numpad: "Numpad0,Numpad1,Numpad2,Numpad3,Numpad4,Numpad5,Numpad6,Numpad7,Numpad8,Numpad9,NumpadDot,NumpadDiv,NumpadMult,NumpadAdd,NumpadSub,NumpadEnter"
        )}
    static effect := {
        (LTrim Join
            Return: "`n",
            Tab: A_Tab,
            Space: A_Space,
            Enter: "`n",
            Dot: ".",
            Div: "/",
            Mult: "*",
            Add: "+",
            Sub: "-"
        )}

    if (!keysBound) {
        ;Binds the keys to watch for triggers
        for k, v in ["symbols", "num", "alpha"]
        {
            ;alphanumeric/symbols
            v := keys[v]
            Loop, Parse, v
            {
                Hotkey, %hotkeyPrefix%%A_LoopField%, __hotstring
            }
        }

        v := keys.alpha
        Loop, Parse, v
        {
            Hotkey, %hotkeyPrefix%+%A_Loopfield%, __hotstring
        }
        for k, v in ["other", "breakKeys", "numpad"]
        {
            ;comma separated values
            v := keys[v]
            Loop, Parse, v, `,
            {
                Hotkey, %hotkeyPrefix%%A_LoopField%, __hotstring
            }
        }
        ;keysBound is a static variable, so now the keys won't be bound twice
        keysBound := true
    }
    if (mode == "CALLBACK") {
        ;Callback for the hotkeys
        Hotkey := SubStr(A_ThisHotkey, 3)
        if (StrLen(Hotkey) == 2 && Substr(Hotkey, 1, 1) == "+" && Instr(keys.alpha, Substr(Hotkey, 2,1))) {
            Hotkey := Substr(Hotkey, 2)
            if (!GetKeyState("Capslock", "T")) {
                StringUpper, Hotkey, Hotkey
            }
        }
        shiftState := GetKeyState("Shift", "P")
        uppercase := GetKeyState("Capslock", "T") ? !shiftState : shiftState
        ;if capslock is down, shift's function is reversed.(ie pressing shift and a key while capslock is on will provide the lowercase key)
        if (uppercase && Instr(keys.alpha, Hotkey)) {
            StringUpper, Hotkey, Hotkey
        }
        if (Instr("," . keys.breakKeys . ",", "," . Hotkey . ",")) {
            typed := ""
            return
        }
        else if Hotkey in Return,Tab,Space
        {
            typed .= effect[Hotkey]
        }
        else if (Hotkey == "BS") {
            ;trim typed var if Backspace was pressed
            StringTrimRight, typed, typed, 1
            return
        }
        else if (RegExMatch(Hotkey, "Numpad(.+?)", numKey)) {
            if (numkey1 ~= "\d") {
                typed .= numkey1
            }
            else {
                typed .= effect[numKey1]
            }
        }
        else {
            typed .= Hotkey
        }
        matched := false
        for k, v in hotstrings
        {
            matchRegex := (v.mode == 1 ? "Oi)" : "") . (v.mode == 3 ? RegExReplace(v.trigger, "\$$", "") : "\Q" . v.trigger . "\E") . "$"
            if (v.mode == 3) {
                if (matchRegex ~= "^[^\s\)\(\\]+?\)") {
                    matchRegex := "O" . matchRegex
                }
                else {
                    matchRegex := "O)" . matchRegex
                }
            }
            if (RegExMatch(typed, matchRegex, local$)) {
                matched := true
                if (v.cond != "" && IsFunc(v.cond)) {
                    ;if hotstring has a condition function
                    A_LoopCond := Func(v.cond)
                    if (A_LoopCond.MinParams >= 1) {
                        ;if the function has atleast 1 parameters
                        A_LoopRetVal := A_LoopCond.(v.mode == 3 ? local$ : local$.Value(0))
                    }
                    else {
                        A_LoopRetVal := A_LoopCond.()
                    }
                    if (!A_LoopRetVal) {
                        ;if the function returns a non-true value
                        matched := false
                        continue
                    }
                }
                if (v.mode == 2) {
                    returnValue := (local$ == "" ? local$.Value(0) : local$)
                }
                else {
                    returnValue := (local$.Count > 0 ? local$ : local$.Value(0))
                }
                if (v.clearTrigger) {
                    ;delete the trigger
                    triggerStr := (v.mode == 3 && local$.Count > 0 ? local$.Value(0) : returnValue)
                    SendInput % "{BS " . StrLen(triggerStr) . "}"
                }
                if (IsLabel(v.label)) {
                    $ := returnValue
                    gosub, % v.label
                }
                else if (IsFunc(v.label)) {
                    callbackFunc := Func(v.label)
                    if (callbackFunc.MinParams >= 1) {
                        callbackFunc.(returnValue)
                    }
                    else {
                        callbackFunc.()
                    }
                }
                else {
                    toSend := v.label
                    ;working out the back-references
                    if (local$.Count() == 0) {
                        StringReplace, toSend, toSend, % "$0", % local$.Value(0), All
                    }
                    Loop, % local$.Count()
                    {
                        StringReplace, toSend,toSend,% "$" . A_Index,% local$.Value(A_index),All
                    }
                    toSend := RegExReplace(toSend, "([!#\+\^\{\}])", "{$1}") ;Escape modifiers
                    SendInput, %toSend%
                }
            }
        }
        if (matched) {
            typed := ""
        }
        else if (StrLen(typed) > 350) {
            StringTrimLeft, typed, typed, 200
        }
    }
    else {
        if (hotstrings.HasKey(trigger) && label == "") {
            ;removing a hotstring
            hotstrings.remove(trigger)
        }
        else {
            ;add to hotstrings object
            hotstrings[trigger] := {
                (LTrim Join
                    trigger: trigger,
                    label: label,
                    mode: mode,
                    clearTrigger: clearTrigger,
                    cond: cond
                )}
        }
    }
    return

    __hotstring:
        ;this label is triggered every time a key is pressed
        Hotstring("", "", "CALLBACK")
        return
}

mviens avatar Jun 21 '15 06:06 mviens

You know . . . you could just do a pull request and I'll accept it. Or if you're up to it, I can always transfer the ownership of this repo. I'm not maintaining it anymore as I have switched over to ubuntu and dont really use autohotkey.

menixator avatar Jun 22 '15 19:06 menixator

@mviens , you tried to fix Hotstring("Trigger2", "replace", 2) by doing this:

if (v.mode == 2) {
   returnValue := (local$ == "" ? local$.Value(0) : local$)
}
else {
   returnValue := (local$.Count > 0 ? local$ : local$.Value(0))
}

This is incorrect. It changes the spec that is laid out in the readme doc. The readme says that if mode=3 for regex mode, then the return value is a match object. You've changed it so that it only returns the match object if there is no subpatterns.

Suppose I want to get the length of the match. Your solution fails for this example, because in this regex I'm not capturing the subpattern:

Hotstring("(?:reg|regex)label", "label2", 3)        ; type either "reglabel" or "regexlabel"

label2:
    ; $ == "toLabel"
    MsgBox % "trigger=" $.Value(0) "`nLength of trigger=" $.Len()
return

I guess the reason you changed it like you did, was so that you could get this example to work:

Hotstring("i)trigger3", "replace", 3)

replace($) {
    ; $ == 'trigger'
    MsgBox % "'" . $ . "' was entered!"
}

However, this is not supposed to work according to that spec. With mode=3, the spec says you get a match object in $, so you should be using $.value(0) within the func

Now if you don't like the spec, and think yours is better and more convenient for the end user, thats fine, but thats another discussion. But it would still fail for the example I showed above where a user would want to use the .Len() method from the match object

I believe the proper fix is what I proposed in PR #9

mmikeww avatar Jan 05 '18 17:01 mmikeww