Overleaf fileswitcher (no longer maintained)

Custom hotkeys for switching between files on overleaf.com

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Overleaf fileswitcher (no longer maintained)
// @namespace    http://tampermonkey.net/
// @version      1.22
// @license      apache2
// @description  Custom hotkeys for switching between files on overleaf.com
// @author       Aditya Sriram
// @match        https://www.overleaf.com/project/*
// @grant        none
// @require      http://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @require      http://cdnjs.cloudflare.com/ajax/libs/mousetrap/1.6.1/mousetrap.min.js
// @require      http://cdnjs.cloudflare.com/ajax/libs/mousetrap/1.6.1/plugins/global-bind/mousetrap-global-bind.min.js
// ==/UserScript==

(function($, undefined) {
    // list of allowed extensions (skip files that don't match)
    var filetypes = ['tex', 'bib', 'txt', 'pdf', 'cls', 'sty', 'png/jpg'];
    var allow_ext = ['tex', 'bib', 'txt']; // in lowercase
    var prev_file = undefined;
    var cur_file = undefined;
    var allow_all_types = false;

    var shortcuts = {};

    function keybindg(key, desc, func) {
        shortcuts[key] = desc;
        Mousetrap.bindGlobal(key, function() {
            console.log("triggered " + key);
            func();
        });
    }

    function getFileName(i,e) {
        if (typeof(e) === "undefined") e = i;
        return $(e).find('span.ng-binding').eq(0).text().trim();
    }

    function getFilePath(file) {
        var parents = file.parents('file-entity');
        return parents.map(getFileName).get().reverse();
    }

    function matchExtension(i, f) {
        var fname = getFileName(f);
        if (allow_all_types) return true;
        for (var exts of allow_ext) {
            for (var ext of exts.split("/")) {
                if (fname.toLowerCase().endsWith(ext))
                    return true;
            }
        }
        return false;
    }

    function getFileList() {
        return $('file-entity:not(:has(div[ng-controller]))');
    }

    function getCurrentFile() {
        var files = getFileList();
        var current = files.filter(':has(.selected)');
        if (current.length == 0) {
            return undefined;
        } else {
            return current.eq(0);
        }
    }

    function resizePanes() {
        $('a.custom-toggler-west')[0].click();
        $('a.custom-toggler-west')[0].click();
    }

    function dispFile(file) {
        var filename = getFileName(file);
        var filepath = getFilePath(file);
        $('#monkey-filename').text("/" + filepath.concat(filename).join("/"));
    }

    function focusFile(file) {
        prev_file = cur_file;
        file.find('li:not(.ng-scope)').eq(0).click(); // click li to focus file
        cur_file = file;
        dispFile(file);
        resizePanes();
    }

    function switchFile(n) {
        var files = getFileList();
        files = files.filter(matchExtension);

        var curidx = files.index(getCurrentFile());
        if (curidx < 0) {
            curidx = 0;
            console.log('no filtered file selected, falling back to first file');
        }

        var newidx = curidx+n;
        if (newidx >= files.length) newidx = 0;
        if (newidx < 0) newidx = files.length-1;

        var newfile = files.eq(newidx);
        focusFile(newfile);
    }

    function encloser(cmd) {
        var fn = function(editor) {
            var selection = editor.getSelection();
            if (selection.isEmpty()) {
                editor.insert("\\" + cmd + "{}");
                return editor.navigateLeft(1);
            }
            var text = editor.getCopyText();
            return editor.insert("\\" + cmd + "{" + text + "}");
        }
        return fn;
    }

    function fileTypeSelector() {
        var cursetting = filetypes.map((s,i) => i+1+". "+((allow_ext.includes(s) || allow_all_types) ? s + '*' : s));
        var ans = prompt("Select the file types to allow or type 'all'\n" + cursetting.join("\n"));
        if (ans && ans.trim().length > 0) {
            allow_all_types = (ans.trim() == "all");
            allow_ext = [];
            for (var c of ans) {
                if ('0123456789'.includes(c) !== -1 && parseInt(c) <= filetypes.length)
                    allow_ext.push(filetypes[parseInt(c)-1]);
            }
        }
    }

    function init() {
        console.log("activating custom overleaf hotkeys...");
        var editor = ace.edit($('.ace-editor-body')[0]);
        //console.log(editor);
        // replace italics hotkey function with custom emphasize function
        editor.commands.byName['italics'].exec = encloser('emph');
        // ctrl-m to insert '\text'
        editor.commands.addCommand({
            name: "mathmode",
            bindKey: {
                win: "Ctrl-M",
                mac: "Command-M"
            },
            exec: encloser('text'),
            readOnly: !1
        });

        // add span to display file path
        $('span.name.ng-binding').parent().after('<span id="monkey-filename" style="color:white;"></span>');
        // on click filepath, show filetype selector
        $('#monkey-filename').css("cursor", "pointer").click(fileTypeSelector);

        prev_file = cur_file = getCurrentFile();
        dispFile(getCurrentFile());

        // add eventlistener to detect file change due to manual user click
        $('aside.file-tree').on('click', 'file-entity li.ng-scope', function() {
            if (getFileName(getCurrentFile()) != getFileName(cur_file)) {
                console.log("manual file change detected");
                prev_file = cur_file;
                cur_file = getCurrentFile();
                dispFile(getCurrentFile());
            }
        });

        keybindg('ctrl+shift+pageup', 'Previous File', function() {switchFile(-1);});
        keybindg('ctrl+shift+pagedown', 'Next File', function() {switchFile(+1);});
        keybindg('ctrl+shift+,', 'Previous File', function() {switchFile(-1);});
        keybindg('ctrl+shift+.', 'Next File', function() {switchFile(1);});
        keybindg('ctrl+shift+/', 'Past File', function() {focusFile(prev_file);});
        console.log('hotkeys:', JSON.parse(JSON.stringify(shortcuts)));
    }

    function wait_to_load(callback) {
        if ($('.loading-screen-brand').length > 0) {
            console.log("still loading");
            window.setTimeout(wait_to_load, 500, callback);
        } else {
            console.log("detected loading completion");
            window.setTimeout(callback, 200);
        }
    }

    $(document).ready(function() {
        wait_to_load(init);
    });

})(window.jQuery.noConflict(true));