Overleaf Editor Custom VIM Keybindings

Configure a list of shortcuts for Vim-mode + :commands for toggling panes on Overleaf

// ==UserScript==
// @name         Overleaf Editor Custom VIM Keybindings
// @description  Configure a list of shortcuts for Vim-mode + :commands for toggling panes on Overleaf
// @namespace    http://tampermonkey.net/
// @version      0.3.7
// @match        https://www.overleaf.com/project/*
// @grant        none
// Source 1: https://groups.google.com/d/msg/ace-discuss/gwXMzUM17I4/9B_acHBSCQAJ
// Source 2: https://www.overleaf.com/learn/how-to/How_can_I_define_custom_Vim_macros_in_a_vimrc_file_on_Overleaf%3F
// @license MIT
// ==/UserScript==

(function() {
    'use strict';
    // poll until editor is loaded
      setTimeout(function(){
            var card;
            var editor = window._debug_editors[window._debug_editors.length -1]
            $(document).keyup(function(event){
                    switch(event.keyCode) {
                    case 27:
                    esc()
                    case 96:
                    esc()
                    }
                    });
            function esc(){
            card = $('.writefull-suggest-card');
            if(card.css("display")=='flex'){
                card.css("display",'none')
                // Restore the focus back to ACE editor
                // Ref: https://groups.google.com/g/ace-discuss/c/1_AXADucTEA/m/cc2KZQMtDgAJ?utm_medium=email&utm_source=footer
                editor.focus() // side-effect - a side box is drawn
            }
            };
            var DA = {48:-1,49:0,50:1,51:2};
            $(document).keypress(function(e){
                    if(e.which>47 && e.which < 52){
                    card = $('.writefull-suggest-card');
                    if(card.css("display")=='flex'){
                    console.log(DA[e.which])
                    card.find('li.replacement').eq(DA[e.which])[0].click();
                    // Ref: https://groups.google.com/g/ace-discuss/c/1_AXADucTEA/m/cc2KZQMtDgAJ?utm_medium=email&utm_source=footer
                    editor.focus() // side-effect - a side box is drawn
                    }
                    }
                    });

    },2000);
    // End of chunk
    // The following chunk is the main Vim emulator piece, defining a bunch of functions + shortcuts + remapping of keys.
    const retry = setInterval(() => {
        if (window._debug_editors === undefined) return
        clearInterval(retry)
        // get current editor instance
        // get current editor instance
        var editor = window._debug_editors[window._debug_editors.length -1]
        // Option for a vertical split. Source: https://stackoverflow.com/a/24417327/3925312
        editor.setOption("showPrintMargin", true)
        // session wrap at 80: thanks to help from Harutyun Amirjanyan,
        // Source: https://groups.google.com/g/ace-discuss/c/gjLwB6Bmj0c/m/cdkIvkXuAgAJ
        //editor.setOption("wrap", 80)
        // Enforcing soft-wrap while allowing for auto-completion (not killing it).
        ace.config.on("session", function(session) {
        //    session.setOption("wrap", 80) // This may break auto-completion.
        session.setWrapLimitRange(80, 80) //Caveat: no more wrap to the pane of size smaler than 80 col.
        })
        // Unbind Ctrl+L ==> for Overleaf, this is to go to certain line/location. Not necessary with Vim bindings
        editor.commands.bindKeys({"ctrl-l":null})
        // vim keyboard plugin
        var vimKeyboard = window.ace.require("ace/keyboard/vim")
        ////////////////////////////////////////////////////////////
        // 2021-10-20 - https://groups.google.com/g/ace-discuss/c/2hwMMzBbYH8/m/Q0l9vJAqBQAJ?utm_medium=email&utm_source=footer
        // g0, g$, and g^ as a bonus
        vimKeyboard.handler.defaultKeymap.push({
            keys: "g$", type: "motion", motion: "moveToLineEnd"
        }, {
            keys: "g^", type: "motion", motion: "moveToLineStart"
        }, {
            keys: "g0", type: "motion", motion: "moveToLineStart"
        })
        vimKeyboard.Vim.defineMotion("moveToLineEnd", function(cm) {
            cm.ace.selection.moveCursorLineEnd()
            cm.ace.selection.clearSelection()
            return cm.getCursor()
        })
        vimKeyboard.Vim.defineMotion("moveToLineStart", function(cm) {
            cm.ace.selection.moveCursorLineStart()
            cm.ace.selection.clearSelection()
            return cm.getCursor()
        })
        // j k within long lines
        vimKeyboard.Vim.map("j", "gj", "normal")
        vimKeyboard.Vim.map("k", "gk", "normal")
        // Override $ and 0
        vimKeyboard.Vim.map("0", "g0", "normal")
        vimKeyboard.Vim.map("$", "g$", "normal")
        // add custom keybindings - insert mode applies on insert
        vimKeyboard.Vim.map("jj", "<Esc>", "insert")
        // Equilivantly, from https://groups.google.com/d/msg/ace-discuss/gwXMzUM17I4/9B_acHBSCQAJ
        // window.ace.require("ace/keyboard/vim").Vim.map('jj', '<Esc>', 'insert')
        vimKeyboard.Vim.map("jk", "<Esc>", "insert")
        ////////////////////////////////////////////////////////////
        // Remapping one key to another is doable, in Normal mode
        // `nmap h j` can be defined as the following:
        // Though, why should this mapping be here anyways? It is just meant for demo purpose.
        // vimKeyboard.Vim.map("h", "j", "normal")
        // Define local commands ==> matching elements along with a identifier that matches.
        // Maintenance note: if one hotkey/cmd does not work, check here for the matching HTML element.
        editor.commands.addCommands(
        [
        { // Note, this is not working as the element will change to "Split screen"
          // At least, we need an "or-condition" for the querySelector.
            name: "Projects",
            exec: function() {
              document.querySelector('a[class*="toolbar-header-back-projects"]').click()
            }
        },
        { // Note, this is not working as the element will change to "Split screen"
          // At least, we need an "or-condition" for the querySelector.
            name: "FullScreen",
            exec: function() {
              document.querySelector('a[tooltip*="Full screen"]').click()
            }
        },
        {
            name: "ToggleHistory",
            exec: function() {
              document.querySelector('.fa-history').click()
            }
        },
        {
            name: "ToggleLog",
            exec: function() {
              document.querySelector('.log-btn').click()
            }
        },
        {
            name: "jumpToPdf",
            exec: function() {
                document.querySelector(".fa-arrow-right").click()
            }
        },
          {
            name: "VimtexTocToggle",
            exec: function() {
                document.querySelector('a[tooltip*="the file-tree"]').click()
            }
        }, {
            name: "CloseComment",
            exec: function() {
                document.querySelector('a[class*="review-panel-toggler"]').click()
            }
        }, {
            name: "TogglePDF",
            exec: function() {
                //document.querySelector('a[tooltip*="the PDF"]').click() // This stops working (2022-01-31)
                document.querySelector('a[class*="custom-toggler-east"]').click()
            }
        }, {
            name: "OmegaPress",
            exec: function() {
                // For the Toggle Symbol Palette, press it twice, to remove the gray-box should the Overleaf project is page is restored.
                document.querySelector('button[tooltip*="Toggle Symbol Palette"]').click()
                document.querySelector('button[tooltip*="Toggle Symbol Palette"]').click()
            }
        }, {
            name: "FindTypoNext",
            exec: function() {
                document.querySelector('mwc-icon-button[id="writefull-next"]').click()
            }
        }, {
            name: "FindTypoPrevious",
            exec: function() {
                document.querySelector('mwc-icon-button[id="writefull-previous"]').click()
            }
        }, {
            name: "TestFocus",
            exec: function() {
                editor.focus()
            }
        }])
        // add keybindings for jump to pdf
        vimKeyboard.Vim.mapCommand("\\v", "action", "aceCommand",
            { name: "VimtexTocToggle" }, { context: "normal" });
        vimKeyboard.Vim.mapCommand("\\lv", "action", "aceCommand",
            { name: "jumpToPdf" }, { context: "normal" });
        vimKeyboard.Vim.mapCommand("\\o", "action", "aceCommand",
            { name: "TogglePDF" }, { context: "normal" });

        // add keybindgs for jump to typos
        // Note, [s and ]s has unknown complications. Just avoid using it.
        vimKeyboard.Vim.mapCommand("\]", "action", "aceCommand",
            { name: "FindTypoNext" }, { context: "normal" });
        //vimKeyboard.Vim.mapCommand("N", "action", "aceCommand",
            //{ name: "FindTypoNext" }, { context: "normal" });
        vimKeyboard.Vim.mapCommand("\[", "action", "aceCommand",
            { name: "FindTypoPrevious" }, { context: "normal" });
        //vimKeyboard.Vim.mapCommand("P", "action", "aceCommand",
            //{ name: "FindTypoPrevious" }, { context: "normal" });

        // ESC to regain focus
        vimKeyboard.Vim.mapCommand("<ESC>", "action", "aceCommand",
            { name: "TestFocus" }, { context: "normal" });

        // bind to ;lv
        vimKeyboard.Vim.unmap(";")
        vimKeyboard.Vim.unmap(",")
        vimKeyboard.Vim.unmap("\[\[")
        vimKeyboard.Vim.unmap("\]\]")
        vimKeyboard.Vim.map(";lv", "\\lv", "normal")
        vimKeyboard.Vim.map(",v", "\\v", "normal")

        // Use ,o to activate two hotkeys: hide file-menu and hide PDF preview
        vimKeyboard.Vim.map(",o", "\\v\\o", "normal")
        //vimKeyboard.Vim.mapCommand(",o", "action", "aceCommand",
        //    { name: "FullScreen" }, { context: "normal" });

        //Use cmd statment to change theme: :light vs :dark
            //Ref: https://github.com/Seldaek/php-console/issues/36
        vimKeyboard.Vim.defineEx("light", "", ()=>editor.setTheme("ace/theme/dreamweaver"))
        vimKeyboard.Vim.defineEx("dark", "", ()=>editor.setTheme("ace/theme/gruvbox"))
        //vimKeyboard.Vim.defineEx("full", "", ()=>editor.commands.exec("FullScreen"))
        vimKeyboard.Vim.defineEx("home", "", ()=>editor.commands.exec("Projects"))
        vimKeyboard.Vim.defineEx("back", "", ()=>editor.commands.exec("Projects"))
        vimKeyboard.Vim.defineEx("history", "", ()=>editor.commands.exec("ToggleHistory"))
        vimKeyboard.Vim.defineEx("log", "", ()=>editor.commands.exec("ToggleLog"))
        vimKeyboard.Vim.defineEx("ShowPDF", "", ()=>editor.commands.exec("TogglePDF"))
        vimKeyboard.Vim.defineEx("ClosePDF", "", ()=>editor.commands.exec("TogglePDF"))
        vimKeyboard.Vim.defineEx("OpenPDF", "", ()=>editor.commands.exec("TogglePDF"))
        vimKeyboard.Vim.defineEx("PDF", "", ()=>editor.commands.exec("TogglePDF"))
        vimKeyboard.Vim.defineEx("pdf", "", ()=>editor.commands.exec("TogglePDF"))
        // Close comment: CC, cc, CloseComment
        vimKeyboard.Vim.defineEx("CloseComment", "", ()=>editor.commands.exec("CloseComment"))
        vimKeyboard.Vim.defineEx("CC", "", ()=>editor.commands.exec("CloseComment"))
        vimKeyboard.Vim.defineEx("cc", "", ()=>editor.commands.exec("CloseComment"))
        // Toggl the omega icon, for symbol palette
        vimKeyboard.Vim.defineEx("o", "", ()=>editor.commands.exec("OmegaPress"))
        // Test editor-focus
        vimKeyboard.Vim.defineEx("focus", "", ()=>editor.commands.exec("TestFocus"))
        // Test typo-find
        vimKeyboard.Vim.defineEx("tnext", "", ()=>editor.commands.exec("FindTypoNext"))
        // set the modified keyboard handler for editor
        editor.setKeyboardHandler(vimKeyboard.handler)
        console.log("Custom key bindings applied")
    }, 100)
})();