Having issue modifying super-global object inside of a nested function Topic is solved

Get help with using AutoHotkey (v2 or newer) and its commands and hotkeys
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Having issue modifying super-global object inside of a nested function

28 Jun 2018, 08:12

I apologize for the size and complexity of the code below. I haven't been able to create a simple example that illustrates my problem. I am creating an app that consumes the GitLab Snippets API so users can quickly insert snippets and are easily shared between users.

I probably need to restructure my initializeApp() function into a class as I am using many nested functions. But I am having a problem with the populateContents() function. It calls store.getSnippetContent() and passes an id and a callback. The callback passes data to the nested function testFunc(). This nested function is supposed to set a key:value on the super-global variable localSnippets. I am confirming via debugging and MsgBox's that both the params in the nested function are correct. The problem is that it doesn't appear that localSnippets is actually getting updated. Also, my MsgBox inside of testFunc() doesn't show where it is currently. If I move it up a line, then it works and shows 0.

I am probably just misunderstanding nested functions and closures limitations in AHK.

EDIT: I attached the two includes. This should work for anyone that has a gitlab account on gitlab.com if you insert the proper root URL and access token.

Code: Select all

#Persistent
#SingleInstance Force
#Include <JSON>
#Include <Class_Toolbar>

OnExit("cleanup")
global localSnippets := loadLocalSnippets() || {}
global store := new GitLab("<token>")
store.getAllSnippets(Func("initializeApp"))
global snippetSelectionGui := new localSnippetsGui()

HotKey("#H", () => snippetSelectionGui.toggleGui())
Return

initializeApp(data) {
    global localSnippets
    initialHTML := "about:<!DOCTYPE html><meta http-equiv='X-UA-Compatible' content='IE=EDGE'>"
    IL := IL_Create(1, 1)
    IL_Add(IL, "shell32.dll", 239)
    IL_Add(IL, "imageres.dll", 233)
    snippetsGui := GuiCreate("", "Public Snippets")
    snippetsGui.MarginX := snippetsGui.MarginY := 10
    ;snippetsGui.OnEvent("Close", () => snippetsGui.Destroy())
    snippetsTB := snippetsGui.Add("Custom", "w800 h15 ClassToolbarWindow32 0x0800 0x0100 0x0040")
    snippetsLV := snippetsGui.Add("ListView", "xs w" 400 - snippetsGui.MarginX // 2 " h200 Section vSnippet +Checked", "Title|Author|Last Updated")
    snippetsLV.OnEvent("ItemSelect", "updateSnippetContents")
    snippetsLV.OnEvent("ItemCheck", "populateContents")
    for i, snippet in data {
        tempDate := StrSplit(RegExReplace(snippet.updated_at, "-|:"), "T")
        displayDateTime := FormatTime(tempDate[1] . SubStr(tempDate[2], 1, 6), "MM/dd/yyyy HH:mm")
        snippetsLV.Add(localSnippets[snippet.id] && "Check", snippet.title, snippet.author.name, displayDateTime)
        LV_SetItemParam(snippetsLV.hwnd, i, snippet.id)
    }
    snippetsLV.ModifyCol()
    
    ;snippetsGui.Add("Edit", "ys w" 400 - snippetsGui.MarginX // 2 " h200 vDescription")
    wbDescription := snippetsGui.Add("ActiveX", "ys w" 400 - snippetsGui.MarginX // 2 " h200 +VScroll +Border", initialHTML "<script src='https://cdn.rawgit.com/showdownjs/showdown/1.8.6/dist/showdown.min.js'></script><script>showdown.setFlavor('github');</script><style>body {font-family: 'Segoe UI';font-size: 14px; margin: 0; padding: 5px;} body > * {margin: 0; padding: 0;}</style>").value
    ;snippetsGui.Add("Edit", "w800 h400 vContents")
    wbContent := snippetsGui.Add("ActiveX", "xs w800 h400 +VScroll +Border", initialHTML "<style>body {margin: 0 !important;} .gitlab-embed-snippets {margin: 0 !important;}</style>").value
    snippetsGui.Show("Hide")
    snippetsTBWrapper := new ToolBar(snippetsTB.hwnd)
    snippetsTBWrapper.SetImageList(IL)
    snippetsTBWrapper.SetPadding(15, 15)
    snippetsTBWrapper.SetMaxTextRows(0)
    btns := []
    btns.push({ id: 1, label: "Reconfigure Snippets", image: 1, callback: () => refreshLocalSnippets(snippetsLV)})
    btns.push("")
    btns.push({ id: 2, label: "Exit program", image: 2, callback: () => ExitApp()})
    ret := snippetsTBWrapper.Add("Enabled", btns*)
    ;ret := snippetsTBWrapper.Add()
    HotKey("#F12", () => WinExist("ahk_id" snippetsGui.hwnd) ? snippetsGui.Hide() : snippetsGui.Show())
    
    ; Set a function to monitor the Toolbar's messages.
    WM_COMMAND := 0x111
    OnMessage(WM_COMMAND, "TB_Messages")
    
    ; Set a function to monitor notifications.
    WM_NOTIFY := 0x4E
    OnMessage(WM_NOTIFY, "TB_Notify")
    
    TB_Messages(w, l) {
        if (l = snippetsTB.hwnd) {
            snippetsTBWrapper.OnMessage(w)
        }
    }
    
    TB_Notify(w, l) {
        code := snippetsTBWrapper.OnNotify(l, MX, MY, label, id)
    }
    
    refreshLocalSnippets(ctrl) {
        rowNum := 0, newLocalSnippets := {}
        while(rowNum := ctrl.GetNext(rowNum, "Checked")) {
            id := LV_GetItemParam(ctrl.hwnd, rowNum)
            for key, snippet in data {
                if (snippet.id = id) {
                    newLocalSnippets[id] := { title: snippet.title }
                }
            }
        }
        localSnippets := newLocalSnippets
        snippetSelectionGui.updateSnippets()
    }
    
    updateSnippetContents(ctrl, row, selected) {
        if (selected) {
            id := LV_GetItemParam(ctrl.hwnd, row)
            store.getSnippetContents(id, (data) => showSnippet(snippetsGui, data))
        }
    }
    
    populateContents(ctrl, row, checked) {
        id := LV_GetItemParam(ctrl.hwnd, row)
        if (checked) {
            store.getSnippetContents(id, (snippetData) => testFunc(id, snippetData.content)) ; localSnippets[index].content := data.content)
        }
        ;else {
            ;localSnippets.Delete(id)
        ;}
    }
    
    testFunc(id, content) {
        localSnippets[id].content := content
        MsgBox(localSnippets.Count()) ;  localSnippets[5].content "'")
    }
    
    showSnippet(gui, data) {
        ;gui.control["description"].value := data.description
        wbDescription.document.parentWindow.execScript("document.body.innerHTML = new showdown.Converter().makeHtml('" StrReplace(data.description, "`r`n", "\r\n") "');")
        wbContent.document.head.innerHTML := wbContent.document.body.innerHTML := ""
        wbContent.document.write("<script src='" data.web_url ".js'></script>")
    }
}

class localSnippetsGui {
    __New() {
        this.gui := GuiCreate("+ToolWindow -Caption +AlwaysOnTop", "Pick a Snippet to insert")
        this.gui.MarginX := this.gui.MarginY := 0
        this.gui.OnEvent("Escape", () => this.toggleGui())
        
        newOpts := []
        for id, snippet in localSnippets {
            newOpts.push(snippet.title)
        }
        ddl := this.gui.Add("DropDownList", "w200 vSnippet", newOpts)
        this.guiSize := { w: ddl.pos.w, h: ddl.pos.h }
        submitButton := this.gui.Add("Button", "Default", "Submit")
        submitButton.OnEvent("click", () => this.submitGui())
    }
    
    toggleGui() {
        if (WinExist("ahk_id " this.gui.hwnd)) {
            this.gui.hide()
        }
        else {
            this.gui.show("w" this.guiSize.w " h" this.guiSize.h)
        }
    }
    
    submitGui() {
        data := this.gui.Submit()
        
        for id, snippet in localSnippets {
            if (snippet.title = data.Snippet) {
                oldClip := ClipboardAll()
                Clipboard := snippet.content
                SendInput("{Ctrl Down}v{Ctrl Up}")
                Sleep(50)
                Clipboard := oldClip, oldClip := ""
                break
            }
        }
    }
    
    updateSnippets() {
        newOpts := []
        for id, snippet in localSnippets {
            newOpts.push(snippet.title)
        }
        this.gui.control["Snippet"].Delete()
        this.gui.control["Snippet"].Add(newOpts)
    }
}

class GitLab {
    __New(token) {
        this.rootURL := "<url>"
        this.token := token
        this.xhr := ComObjCreate("Msxml2.XMLHTTP")
    }
    
    __Delete() {
        this.xhr := ""
    }
    
    handleStatusChange(callback) {
        if (this.xhr.readyState == 4 && this.xhr.status == 200) {
            callback.call(JSON.load(this.xhr.responseText))
        }
    }
    
    getAllSnippets(callback) {
        this.xhr.open("GET", this.rootURL "/snippets/public")
        this.xhr.SetRequestHeader("PRIVATE-TOKEN", this.token)
        this.xhr.onreadystatechange := () => this.handleStatusChange(callback)
        this.xhr.send()
    }
    
    getSnippetContents(id, callback) {
        this.xhr.open("GET", this.rootURL "/snippets/" id)
        this.xhr.SetRequestHeader("PRIVATE-TOKEN", this.token)
        this.xhr.onreadystatechange := () => this.handleStatusChange((data) => this.populateSnippetContent(data, callback))
        this.xhr.send()
    }
    
    populateSnippetContent(data, callback) {
        this.xhr.open("GET", data.raw_url, false)
        this.xhr.SetRequestHeader("PRIVATE-TOKEN", this.token)
        ;this.xhr.onreadystatechange := () => this.handleStatusChange(callback)
        this.xhr.send()
        data.content := this.xhr.responseText
        callback.call(data)
    }
}

; ======================================================================================================================
; LV_GetItemParam - Retrieves the value of the item's lParam field.
; ======================================================================================================================
LV_GetItemParam(HLV, Row) {
   ; LVM_GETITEM -> http://msdn.microsoft.com/en-us/library/bb774953(v=vs.85).aspx
   Static LVM_GETITEM := 0x104B ; LVM_GETITEMW
   Static OffParam := 24 + (A_PtrSize * 2)
   LV_LVITEM(LVITEM, 0x00000004, Row) ; LVIF_PARAM
   SendMessage(LVM_GETITEM, 0, &LVITEM, "", "ahk_id " . HLV)
   Return NumGet(LVITEM, OffParam, "UPtr")
}

; ======================================================================================================================
; LV_SetItemParam - Sets the lParam field of the item to the specified value.
; ======================================================================================================================
LV_SetItemParam(HLV, Row, Value) {
    ; LVM_SETITEMA -> http://msdn.microsoft.com/en-us/library/bb761186(v=vs.85).aspx
    Static LVM_SETITEM := 0x104C
    Static OffParam := 24 + (A_PtrSize * 2)
    LV_LVITEM(LVITEM, 0x00000004, Row) ; LVIF_PARAM
    NumPut(Value, LVITEM, OffParam, "UPtr")
    Return SendMessage(0x1006, 0, &LVITEM, "", "ahk_id " . HLV)
    ;Return ErrorLevel
}

LV_LVITEM(ByRef LVITEM, Mask := 0, Row := 1, Col := 1) {
    Static LVITEMSize := 48 + (A_PtrSize * 3)
    VarSetCapacity(LVITEM, LVITEMSize, 0)
    NumPut(Mask, LVITEM, 0, "UInt"), NumPut(Row - 1, LVITEM, 4, "Int"), NumPut(Col - 1, LVITEM, 8, "Int")
}

cleanup() {
    FileDelete("G-snips.json")
    FileAppend(JSON.Dump(localSnippets), "G-snips.json")
}

loadLocalSnippets() {
    return JSON.Load(FileRead("G-snips.json"))
}
Attachments
includes.zip
(14.59 KiB) Downloaded 81 times
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: Having issue modifying super-global object inside of a nested function

01 Jul 2018, 06:07

ListVars can be used to verify that the variable has been resolved as a global.

Code: Select all

global sg
f() {
    l := ""
    f2() {
        ListVars  ; Shows l as local, but not sg
        MsgBox sg l
    }
    f2
}
f
ExitApp
In this case, the l inside f2 and the l inside f are both aliases for a "free" variable, which can exist after the function returns. sg is resolved directly. (There is a bit more involved if sg is declared below the function, but that isn't relevant to your script.)

In your case, the first instance of global localSnippets should ensure that every subsequent reference to localSnippets resolves to that global variable unless overridden by a local declaration, parameter or force-local. The second instance of global localSnippets takes precedence, but should ultimately have no effect since global declarations are inherited by nested functions.

If localSnippets has not been resolved to the correct variable, it would likely be empty, therefore either localSnippets.Count() or localSnippets[id] should throw an exception. As this exception would not be handled, it should cause an error dialog to be shown. However, you have not mentioned any error dialogs.

If the MsgBox shows 0 when placed on the first line of testFunc, that must mean the variable contains an object. I see only two places that a variable named localSnippets is assigned an object; one is definitely assigned to a global variable, and I assume the other is not executed (i.e. refreshLocalSnippets is not called).

If id is numeric, a count of 0 should mean that localSnippets[id].content := content will throw an exception, since localSnippets[id] will not return an object. But again, you have not mentioned any error dialogs.

If the MsgBox is shown when placed above localSnippets[id].content := content but not when placed below it, that would seem to indicate that the assignment line is either stalling or exiting the thread. There doesn't seem to be any way that could happen (in the absence of meta-functions or a Property named "content"), so if it does happen, perhaps there is a bug in the script or AutoHotkey causing undefined/indeterminate behaviour. (For instance, an incorrect address passed to NumPut or an incorrect VarSetCapacity call could directly or indirectly cause memory corruption.)

Have you tried stepping through the code with a debugger (e.g. SciTE4AutoHotkey)? The DebugVars version can also show the type (string vs integer) of id and the contents of the object, if localSnippets contains one.
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Having issue modifying super-global object inside of a nested function

02 Jul 2018, 08:04

Hey Lexikos, thanks for your reply.

To start, I will mention that I have gone a different direction with this project and have converted the main Gui into a class and am passing the localSnippets object in. So far, no problems.

However, I think that hashing my initial issue out would be helpful for both myself and others. I had been using AutoGui to debug the script but at times, I don't feel confident of what it reports. I went back and ran the script in Scite4AHK. Threw a breakpoint on the first line of testFunc(). Below is what is reported:
ahk-v2-scope-issue.png
ahk-v2-scope-issue.png (16.87 KiB) Viewed 2420 times
After I let it continue passed the breakpoint and re-inspect the variables, localSnippets does not contain a key of 3. Again, no MsgBox is displayed (since it is after the assignment in testFunc()) and no error is displayed at all. I haven't seen errors related to any of the situations/scenarios that you mentioned above.
lexikos
Posts: 9553
Joined: 30 Sep 2013, 04:07
Contact:

Re: Having issue modifying super-global object inside of a nested function

07 Jul 2018, 00:28

So in the context of your script, this last line executes (when id and localSnippets are in the state shown in your screenshot) but does not raise an error?

Code: Select all

id := 3
localSnippets := {4: {}, 5: {}}
content := "doesn't matter"
localSnippets[id].content := content ; Error: No object to invoke.
I would like to know how that is possible.

If you put a line below that line, does it execute? If you step onto and then over that line, what happens? If you then resume the script and execute ListLines (via the tray/window menu), what does it show?
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Having issue modifying super-global object inside of a nested function  Topic is solved

09 Jul 2018, 09:26

Your simple test script revealed the issue to me!

Let me try to explain. It is pretty confusing because of the passing of callbacks in the GitLab class.

When a ListView item is checked, the populateContents function is called. This invokes store.getSnippetContents which is a method on the GitLab class. I pass it the id of the snippet and a callback function. The method call does a few things. First, it calls the GitLab API to get some information about the snippet. This returned JSON document does not contain the actual content of the snippet though. Therefore, I assigned a fat arrow expression which then calls Gitlab.populateSnippetContent and it is passed the AHK object representing the snippet's info and the original callback passed to store.getSnippetContent. This particular API call is synchronous. So once it returns, I add a content key to the data object with the value of the responseText. Then I am simply calling the original callback with the ENTIRE snippet's information.

So in the end, the main problem was that since localSnippets[id] wasn't defined as an object, I couldn't set a key called content on it. Plus, the content was already a part of the object passed to my callback. Thus, the two functions in question now look like this:

Code: Select all

    populateContents(ctrl, row, checked) {
        id := LV_GetItemParam(ctrl.hwnd, row)
        if (checked) {
            store.getSnippetContents(id, (snippetData) => testFunc(snippetData)) ; localSnippets[index].content := data.content)
        }
        else {
            localSnippets.Delete(id)
        }
    }
    
    testFunc(data) {
        localSnippets[data.id] := data
        MsgBox(localSnippets.Count()) ;  localSnippets[5].content "'")
    }
Can't explain why I was not receiving an error like you showed in your short example though.
User avatar
kczx3
Posts: 1640
Joined: 06 Oct 2015, 21:39

Re: Having issue modifying super-global object inside of a nested function

09 Jul 2018, 09:35

And actually, I can just set the new key in localSnippets directly in the fat arrow expression and remove the need for testFunc completely.

P.S. Can't forget to say thank you!

Return to “Ask for Help (v2)”

Who is online

Users browsing this forum: a_bolog, Lorence, WarlordAkamu67 and 42 guests