diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index 1af561488fa718d47ae520a9df7f59067781c5ef..6a01a941ea727701343e962e5ef4bfd6948a702c 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -453,6 +453,11 @@ select {
               <input type="checkbox" id="autocomplete_prefix" name="autocomplete_prefix">
               <span data-i18n="prefs.gui.autocomplete.autocomplete_prefix"></span>
             </label>
+            <br/>
+            <label data-i18n="[title]prefs.gui.autocomplete.autocomplete_relevance_tt">
+              <input type="checkbox" id="autocomplete_relevance" name="autocomplete_relevance">
+              <span data-i18n="prefs.gui.autocomplete.autocomplete_relevance"></span>
+            </label>
             <br/><br/>
             <span data-i18n="prefs.gui.browser.browser_title"></span>
             <br/>
@@ -806,6 +811,7 @@ function apply(save_prefs) {
         get_bool_elem("save_zoom"),
         get_bool_elem("autocomplete"),
         get_bool_elem("autocomplete_prefix"),
+        get_bool_elem("autocomplete_relevance"),
         get_bool_elem("browser_doc"),
         get_bool_elem("browser_path"),
         get_bool_elem("browser_init"),
@@ -817,7 +823,8 @@ function apply(save_prefs) {
         get_bool_elem("browser_doc"),
         get_bool_elem("browser_path"),
         get_bool_elem("autocomplete"),
-        get_bool_elem("autocomplete_prefix")
+        get_bool_elem("autocomplete_prefix"),
+        get_bool_elem("autocomplete_relevance")
     );
     // Update the grid on all open windows.
     pdgui.update_grid(get_bool_elem("show_grid"),
@@ -1071,7 +1078,7 @@ function autopatch_yoffset_toggle(checked) {
 }
 
 function gui_prefs_callback(name, show_grid, grid_size, save_zoom,
-    autocomplete, autocomplete_prefix,
+    autocomplete, autocomplete_prefix, autocomplete_relevance,
     browser_doc, browser_path, browser_init, autopatch_yoffset) {
     var s = document.getElementById("gui_preset");
 
@@ -1107,6 +1114,7 @@ function gui_prefs_callback(name, show_grid, grid_size, save_zoom,
     document.getElementById("save_zoom").checked = !!save_zoom;
     document.getElementById("autocomplete").checked = !!autocomplete;
     document.getElementById("autocomplete_prefix").checked = !!autocomplete_prefix;
+    document.getElementById("autocomplete_relevance").checked = !!autocomplete_relevance;
     document.getElementById("browser_doc").checked = !!browser_doc;
     document.getElementById("browser_path").checked = !!browser_path;
     document.getElementById("browser_init").checked = !!browser_init;
diff --git a/pd/nw/dialog_search.html b/pd/nw/dialog_search.html
index 74a7e95ef33e3008c762a833fe7c2e2a6f673fab..618804612a0b65c778a6e6f588fe4d389617a7ab 100644
--- a/pd/nw/dialog_search.html
+++ b/pd/nw/dialog_search.html
@@ -837,7 +837,8 @@ function display_doc(doc) {
     }
     if (doc.related_objects) {
         var p_rel_objs = document.createElement("p");
-        var ref_rel_objs = doc.ref_related_objects.split("\,");
+        var ref_rel_objs = doc.ref_related_objects == null
+            ? [] : doc.ref_related_objects.split("\,");
         p_rel_objs.innerHTML = "Related objects: ".bold();
         doc.related_objects.split(" ").forEach(function (rel_obj, i, a) {
             let link_rel_obj = rel_obj;
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index 5ff39dab72c248374969be60f7a4a3ba1c88e59b..8a5d495192d2aa3d9604e57c129b5764adb08e0c 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -444,7 +444,9 @@
         "autocomplete": "Autovervollständigung von Objekt-Namen und Argumenten (experimentell)",
         "autocomplete_tt": "Schlägt bei der Eingabe Vervollständigungen von Objekt-Namen und Argumenten vor",
         "autocomplete_prefix": "Vervollständigung per Objektnamens-Präfix",
-        "autocomplete_prefix_tt": "Vervollständigung nur per Objektnamens-Präfix (statt Übereinstimmung irgendwo im Objektnamen)"
+        "autocomplete_prefix_tt": "Vervollständigung nur per Objektnamens-Präfix (statt Übereinstimmung irgendwo im Objektnamen)",
+        "autocomplete_relevance": "Sortiere Vervollständigungen nach Relevanz",
+        "autocomplete_relevance_tt": "Zeigt die relevantesten Vervollständigungen zuerst (basierend auf der Nutzungshäufigkeit)"
       },
       "browser": {
         "browser_title": "Hilfe-Browser-Einstellungen (WARNUNG: Änderungen können Startup-Zeiten beeinflussen!)",
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index b18fd87d2dfd276122b6e312e76cf2132a734baf..d340835b204976d47a3c24a0b4d396540c971780 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -444,7 +444,9 @@
         "autocomplete": "auto-complete object names and arguments (experimental)",
         "autocomplete_tt": "Offers completions for object names and arguments as you type",
         "autocomplete_prefix": "match completions by object name prefix",
-        "autocomplete_prefix_tt": "Only list completions whose prefix matches (rather than matches anywhere in object names)"
+        "autocomplete_prefix_tt": "Only list completions whose prefix matches (rather than matches anywhere in object names)",
+        "autocomplete_relevance": "sort completions by relevance",
+        "autocomplete_relevance_tt": "List most relevant completions first (based on how frequently they are used)"
       },
       "browser": {
         "browser_title": "Help browser settings (WARNING: changing these may affect startup times!)",
diff --git a/pd/nw/locales/fr/translation.json b/pd/nw/locales/fr/translation.json
index f5dd974a93fe38c6b18b4a21d98d83cd9cb5920e..4721a9c13089a19b1fc91554409a9815f30da211 100644
--- a/pd/nw/locales/fr/translation.json
+++ b/pd/nw/locales/fr/translation.json
@@ -444,7 +444,9 @@
         "autocomplete": "autocompléter les noms et arguments des objets (expérimental)",
         "autocomplete_tt": "Offre complétion pour les noms et arguments des objets au fur et à mesure de la saisie",
         "autocomplete_prefix": "complétion par le préfixe du nom de l'objet",
-        "autocomplete_prefix_tt": "Complétion uniquement par le préfixe du nom de l'objet (ignorer les correspondances au milieu du nom des objets)"
+        "autocomplete_prefix_tt": "Complétion uniquement par le préfixe du nom de l'objet (ignorer les correspondances au milieu du nom des objets)",
+        "autocomplete_relevance": "Trier les complétions par pertinence",
+        "autocomplete_relevance_tt": "Affiche les complétions les plus pertinentes en premier (basé sur la fréquence d'utilisation)"
       },
       "browser": {
         "browser_title": "Paramètres du navigateur d'Aide (AVERTISSEMENT: les modifier peut affecter les temps de démarrage!)",
diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js
index 00cc64efa30dec713ec16fd5cbc8790688e72f44..3f438eacb50bdca0a6a574cb53f0571bbf2549a7 100644
--- a/pd/nw/pd_canvas.js
+++ b/pd/nw/pd_canvas.js
@@ -50,9 +50,40 @@ var canvas_events = (function() {
         last_dropdown_menu_y,
         last_search_term = "",
         svg_view = document.getElementById("patchsvg").viewBox.baseVal,
+        last_results = [], // last completion results (autocomplete)
+        last_completed = -1, // last Tab completion (autocomplete)
+        last_offset = -1, // offset of last Tab completion (autocomplete)
+        last_yanked = "", // last yanked completion (to confirm deletion)
         textbox = function () {
             return document.getElementById("new_object_textentry");
         },
+        caret_end = function () {
+            /* ag: Move the caret to the end of the texbox while editing. This
+               is needed for the autcompletion. We essentially fake pressing
+               the End key here; maybe there's an easier way to do this, but
+               the following seems to work alright, so... We first grab the
+               textbox content and determine its length, which is where we
+               want the caret to be. */
+            var t = textbox();
+            var x = t.innerText;
+            var p = x.length;
+            //console.log("move "+p+": "+x);
+            /* The DOM doesn't make this easy. We first have to define a
+               range, set its start to the desired caret position, and
+               collapse it to a single position (i.e., end = start). Next we
+               grab the current selection, remove all currently selected
+               ranges, and set our new range. Quite a hullaballoo for such a
+               simple task. */
+            var r = document.createRange();
+            var s = window.getSelection();
+            r.setStart(t.childNodes[0], p);
+            r.collapse(true);
+            s.removeAllRanges();
+            s.addRange(r);
+            // Defer this to the event loop to prevent losing the
+            // keyboard focus.
+            setTimeout(function () { t.focus() }, 0);
+        },
         current_events = {}, // keep track of our current listeners
         edit_events = function(elem, events, action, init) {
             // convenience routine for adding an object full of
@@ -294,6 +325,31 @@ var canvas_events = (function() {
                 }
             }
         },
+        ac_dropdown = function() {
+            return document.getElementById("autocomplete_dropdown")
+        },
+        // AG: Little helper function to do all the necessary steps to
+        // re-create the autocompletion dropdown after changes. We factored
+        // this out since it will be needed to deal with changes to both the
+        // edited object text and the autocompletion index.
+        ac_repopulate = function() {
+            // GB: Finding the class from obj: find obj through tag of
+            // textbox, get obj class and remove from the class the word
+            // "selected". This is necessary because in textbox obj and
+            // comment both have class 'obj' and it's import here to
+            // differentiate them.
+            let obj_class = document
+                .getElementById(textbox().getAttribute("tag")+"gobj")
+                .getAttribute("class").toString()
+                .split(" ").slice(0,1).toString();
+            if (obj_class === "obj") { // autocomplete only works for objects
+                pdgui.create_autocomplete_dd(document, ac_dropdown(), textbox());
+                if (ac_dropdown().getAttribute("searched_text") !== textbox().innerText) {
+                    last_results = pdgui.repopulate_autocomplete_dd(document, ac_dropdown, obj_class, textbox().innerText);
+                    last_offset = 0;
+                }
+            }
+        },
         events = {
             mousemove: function(evt) {
                 //pdgui.post("x: " + evt.pageX + " y: " + evt.pageY +
@@ -494,8 +550,17 @@ var canvas_events = (function() {
                 return false;
             },
             text_mousedown: function(evt) {
-                if (evt.target.parentNode === document.getElementById("autocomplete_dropdown")) {
-                    pdgui.select_result_autocomplete_dd(textbox(), document.getElementById("autocomplete_dropdown"));
+                if (evt.target.parentNode === ac_dropdown()) {
+                    pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown());
+                    last_yanked = "";
+                    // ag: Don't do the usual object instantiation thing if
+                    // we've clicked on the autocompletion dropdown. This
+                    // means that the user can just go on editing, entering
+                    // object arguments, etc.
+                    evt.stopPropagation();
+                    //evt.preventDefault();
+                    caret_end();
+                    return false;
                 }
                 if (textbox() !== evt.target && !target_is_scrollbar(evt)) {
                     utils.create_obj();
@@ -528,15 +593,14 @@ var canvas_events = (function() {
                 evt.stopPropagation();
 
                 // GB: Autocomplete feature
-                let ac_dropdown = function() {
-                    return document.getElementById("autocomplete_dropdown")
-                }
                 switch (evt.keyCode) {
                     case 40: // arrowdown
                         pdgui.update_autocomplete_dd_arrowdown(ac_dropdown())
+                        last_yanked = "";
                         break;
                     case 38: // arrowup
                         pdgui.update_autocomplete_dd_arrowup(ac_dropdown())
+                        last_yanked = "";
                         break;
                     case 13: // enter
                         // if there is no item selected on autocomplete dropdown, enter make the obj box bigger
@@ -544,29 +608,98 @@ var canvas_events = (function() {
                             grow_svg_for_element(textbox());
                         } else { // else, if there is a selected item on autocompletion tool, the selected item is written on the box
                             pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown());
-                            // TODO: Substitute the editing box by the object itself
-                            // utils.create_obj(); // not working, it's not that simple.
-                            // canvas_events.normal();
+                            caret_end();
+                            // No need to instantiate the object here,
+                            // presumably the user wants to go on editing.
                         }
+                        last_yanked = "";
                         break;
                     case 9: // tab
-                        // TODO: Substitute this function by one that autocomplete with the prefix in common in all results
-                        pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown());
+                        [last_completed, last_offset] = pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown(), last_completed, last_offset, last_results, evt.shiftKey?-1:1);
+                        last_yanked = "";
+                        caret_end();
                         break;
-                    default:
-                        if (textbox().innerText === "") {
-                            pdgui.delete_autocomplete_dd(ac_dropdown());
-                        } else {
-                            let obj_class = document.getElementById(textbox().getAttribute("tag")+"gobj")
-                                .getAttribute("class").toString().split(" ").slice(0,1).toString();
-                            if (obj_class === "obj") { // autocomplete only works for objects
-                                pdgui.create_autocomplete_dd(document, ac_dropdown(), textbox());
-                                if (ac_dropdown().getAttribute("searched_text") !== textbox().innerText) {
-                                    // finding the class from obj: find obj throwout tag of textbox, get obj class and remove from the class the word "selected".
-                                    //                             this has to be done because in textbox obj and comment have class: 'obj'
-                                    //                             and it's import here to differentiate them
-                                    pdgui.repopulate_autocomplete_dd(document, ac_dropdown, obj_class, textbox().innerText);
+                    case 36:
+                        if (evt.altKey) { // alt-home
+                            [last_completed, last_offset] = pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown(), 0, last_offset, last_results, 0);
+                            last_yanked = "";
+                            caret_end();
+                        }
+                        break;
+                    case 35:
+                        if (evt.altKey) { // alt-end
+                            [last_completed, last_offset] = pdgui.select_result_autocomplete_dd(textbox(), ac_dropdown(), last_results.length-1, last_offset, last_results, 0);
+                            last_yanked = "";
+                            caret_end();
+                        }
+                        break;
+                    case 27: // esc
+                        pdgui.delete_autocomplete_dd(ac_dropdown());
+                        last_completed = last_offset = -1;
+                        last_results = [];
+                        if (last_yanked != "") {
+                            pdgui.post("Operation aborted.")
+                        }
+                        last_yanked = "";
+                        break;
+		    case 89:
+                        if (evt.ctrlKey) { // ctrl-y
+                            // AG: Note that this key is usually bound to the
+                            // Tidy Up operation in the Edit menu, but this
+                            // presumably won't interfere with our use here,
+                            // which is to "yank" the current completion
+                            // (remove it from the completion index).
+                            last_completed = last_offset = -1;
+                            last_results = [];
+                            if (textbox().innerText === "") {
+                                pdgui.delete_autocomplete_dd(ac_dropdown());
+                                last_yanked = "";
+                            } else if (textbox().innerText === last_yanked) {
+                                // confirmed, really yank now
+                                if (pdgui.remove_completion(textbox().innerText, console.log)) {
+                                    pdgui.post("Removed completion: "+textbox().innerText);
+                                    ac_repopulate();
+                                    last_results = [];
+                                    last_completed = last_offset = -1;
                                 }
+                                last_yanked = "";
+                            } else if (pdgui.check_completion(textbox().innerText)) {
+                                // for some safety, ask the user to confirm with another ctrl+y
+                                pdgui.post("Really remove completion "+textbox().innerText+"?");
+                                pdgui.post("Press ctrl+y again to confirm (Esc to abort).");
+                                last_yanked = textbox().innerText;
+                            } else {
+                                pdgui.post("No completion "+textbox().innerText);
+                            }
+                        }
+                        break;
+                    default:
+                        // ag: Only update the state if a "valid key" is
+                        // pressed, not some modifier or other special key.
+                        function is_valid_key(e)
+                        {
+                            // See https://stackoverflow.com/questions/51296562
+                            // This is a quick hack which works because the
+                            // names of special keys are all multiple chars
+                            // long and only contain letters and numbers.
+                            return e.key.length == 1 // ASCII
+                                // non-ASCII:
+                                || (e.key.length > 1 && /[^a-zA-Z0-9]/.test(e.key))
+                                // Spacebar:
+                                || e.keyCode == 32
+                                // We also include Backspace and Del here
+                                // since they are used in editing. Note that
+                                // Return (key code 13) is handled above.
+                                || e.keyCode == 8 || e.keyCode == 46;
+                        }
+                        if (is_valid_key(evt)) {
+                            last_results = [];
+                            last_completed = last_offset = -1;
+                            last_yanked = "";
+                            if (textbox().innerText === "") {
+                                pdgui.delete_autocomplete_dd(ac_dropdown());
+                            } else {
+                                ac_repopulate();
                             }
                         }
                 }
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index 09dd35b1d80570c63ea8a0b02e6f5c17a93766af..60947ffef80cba9f85e026c60aaec2f197df2017 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -3,7 +3,7 @@
 var pwd;
 var lib_dir;
 var help_path, browser_doc, browser_path, browser_init;
-var autocomplete, autocomplete_prefix;
+var autocomplete, autocomplete_prefix, autocomplete_relevance;
 var pd_engine_id;
 
 exports.autocomplete_enabled = function() {
@@ -33,7 +33,7 @@ exports.set_pd_engine_id = function (id) {
 exports.defunkify_windows_path = defunkify_windows_path;
 
 function gui_set_browser_config(doc_flag, path_flag, init_flag,
-                                ac_flag, ac_prefix_flag,
+                                ac_flag, ac_prefix_flag, ac_relevance_flag,
                                 helppath) {
     // post("gui_set_browser_config: " + helppath.join(":"));
     browser_doc = doc_flag;
@@ -49,12 +49,20 @@ function gui_set_browser_config(doc_flag, path_flag, init_flag,
     // user decides to enable it later.
     autocomplete = ac_flag;
     autocomplete_prefix = ac_prefix_flag;
+    autocomplete_relevance = ac_relevance_flag;
     make_completion_index();
     // AG: Start building the keyword index for dialog_search.html. We do this
-    // here so that we can be sure that lib_dir and help_path are known already.
-    // (This may also be deferred until the browser is launched for the first
-    // time, depending on the value of browser_init.)
-    if (browser_init == 1) make_index();
+    // here so that we can be sure that lib_dir and help_path are known
+    // already. (This may also be deferred until the browser is launched for
+    // the first time, depending on the value of browser_init, unless
+    // autocomplete is enabled in which case we have to build it anyway.)
+    if (autocomplete == 1 && !fs.existsSync(expand_tilde(compl_name))) {
+        // if the completion.json file has gone missing, rebuild it
+        rebuild_index();
+    } else if (browser_init == 1 || autocomplete == 1) {
+        // otherwise we only generate the index as needed
+        make_index();
+    }
 }
 
 function gui_set_lib_dir(dir) {
@@ -151,6 +159,8 @@ function index_entry_esc(s) {
 // keywords, and description of help patches.
 function add_doc_details_to_index(filename, data) {
     var title = path.basename(filename, "-help.pd"),
+        // AG: This is confusing. I'm not sure why we just replace the first
+        // newline here. Maybe there's a reason to do so, but I don't get it.
         big_line = data.replace("\n", " "),
         keywords,
         desc,
@@ -225,10 +235,113 @@ function add_doc_details_to_index(filename, data) {
     keywords = keywords && keywords.length > 1 ? keywords[1].trim() : null;
     desc = desc && desc.length > 1 ? desc[1].trim() : null;
     // Remove the Pd escapes for commas
-    desc = desc ? desc.replace(" \\,", ",") : null;
+    // AG: We want to do a global replacement here.
+    desc = desc ? desc.replace(/ \\,/g, ",") : null;
     if (desc) {
-        // format Pd's "comma atoms" as normal commas
-        desc = desc.replace(" \\,", ",");
+        // AG: And we want to get rid of the newlines, there's no reason
+        // whatsover to preserve the original formatting.
+        desc = desc.replace(/\n/g, " ");
+    }
+
+    // AG: Deal with a bunch of special cases which have multiple objects
+    // documented in them, listing the object names (and arguments) in the
+    // NAME field of the META data.
+    var names = big_line
+        .match(/#X text \-?[0-9]+ \-?[0-9]+ NAME ([\s\S]*?);/);
+    // Some NAME fields span multiple lines, pretend that they're spaces.
+    names = names && names.length > 1
+        ? names[1].trim().replace(/\n/g, " ").split(" ")
+        : [];
+    if (names.length > 1) {
+        // special help file, not a single object, remove from completions
+        let obj_result = obj_exact_match(title);
+        if (obj_result.length !== 0) {
+            let obj_ref = obj_result[0].refIndex;
+            //post("scanning "+filename);
+            completion_index.removeAt(obj_ref);
+        }
+        // Some NAME entries (e.g., list) list different variations of the
+        // same object (list append, list trim etc.), we try to be clever
+        // about these.
+        let prefix = names[0];
+        let count = names.filter(name => name == prefix).length;
+        if (count > 3) {
+            // Chances are good that we're looking at variations of the same
+            // command, and not just some oddball NAME list with repeated
+            // entries. First collect the arguments.
+            let myargs = [];
+            for (let i = 0; i < names.length; i++) {
+                if (names[i] == prefix) {
+                    let a = [];
+                    while (i+1 < names.length && names[i+1] != prefix) {
+                        a.push(names[i+1]); i++;
+                    }
+                    if (a.length > 0) {
+                        myargs.push({"occurrences" : 0, "text" : a.join(' ')});
+                    }
+                }
+            }
+            // Next check for an existing object which can be updated.
+            obj_result = obj_exact_match(prefix);
+            if (obj_result.length !== 0) {
+                // Found an object, add new arg entries in-place.
+                let args = obj_result[0].item.args;
+                for (let i = 0; i < myargs.length; i++) {
+                    let arg_result = arg_exact_match(title, myargs[i].text);
+                    if (arg_result.length == 0) {
+                        // New arg completion. Note that the args array is
+                        // live data straight from the fuse, so we can just
+                        // add it in-place.
+                        args.push(myargs[i]);
+                    } else {
+                        // keep track of which args we actually added
+                        myargs[i] = null
+                    }
+                }
+            } else {
+                // the object doesn't exist yet in the index, add it
+                completion_index.add({
+                    "occurrences" : 0, "title" : prefix, "args" : myargs
+                });
+            }
+            //post("add completion "+prefix+" "+myargs.filter(a => a != null).map(a => a.text).join(', '));
+        } else {
+            // just a list of object names here, add them all
+            for (let i = 0; i < names.length; i++) {
+                let title = names[i];
+                // check for existing entries, skip those
+                obj_result = obj_exact_match(title);
+                if (obj_result.length === 0) {
+                    //post("add completion "+title);
+                    completion_index.add({
+                        "occurrences" : 0,
+                        "title" : title,
+                        "args" : []
+                    });
+                }
+            }
+        }
+    }
+
+    /* AG: Some help patches have library information in them, we might want
+       to use that information, so we record it in the completion entries if
+       it is available. NOTE: The lib field will only be added to entries for
+       which library information is available, so you *must* check for its
+       existence before trying to access that data.*/
+    var libs = big_line
+        .match(/#X text \-?[0-9]+ \-?[0-9]+ LIBRARY ([\s\S]*?);/);
+    libs = libs && libs.length > 1
+        ? libs[1].trim().replace(/\n/g, " ").split(" ")
+        : [];
+    // Filter out some unwanted noise.
+    libs = libs.filter(x => x!="external" && x!="addons");
+    if (libs.length > 0) {
+        //post(title+" lib: "+libs.join(' '));
+        let obj_result = obj_exact_match(title);
+        if (obj_result.length !== 0) {
+            // The first LIBRARY entry seems to be most useful.
+            obj_result[0].item.lib = libs[0];
+        }
     }
 
     index_cache[index_cache.length] = [filename, title, keywords, desc, rel_objs, ref_rel_objs]
@@ -469,11 +582,10 @@ function build_index(cb) {
 
 exports.build_index = build_index;
 
-// this doesn't actually rebuild the index, it just clears it, so that it
-// will be rebuilt the next time the help browser is opened
+// normally, this doesn't actually rebuild the index, it just clears it, so
+// that it will be rebuilt the next time the help browser is opened
 function rebuild_index()
 {
-    post("clearing help index (reopen browser to rebuild!)");
     index = init_elasticlunr();
     index_started = index_done = false;
     try {
@@ -482,14 +594,22 @@ function rebuild_index()
     } catch (err) {
         //console.log(err);
     }
+    if (browser_init == 1 || autocomplete == 1) {
+        // if autocomplete is enabled, we *have* to rebuild the index now
+        make_index();
+    } else {
+        // we can defer rebuilding of the index until the browser is reopened
+        post("clearing help index (reopen the browser to rebuild!)");
+    }
 }
 
 // this is called from the gui tab of the prefs dialog
-function update_browser(doc_flag, path_flag, ac_flag, ac_prefix_flag)
+function update_browser(doc_flag, path_flag, ac_flag, ac_prefix_flag, ac_relevance_flag)
 {
-    var changed = false;
+    var changed = ac_flag == 1 && autocomplete == 0;
     autocomplete = ac_flag;
     autocomplete_prefix = ac_prefix_flag;
+    autocomplete_relevance = ac_relevance_flag;
     doc_flag = doc_flag?1:0;
     path_flag = path_flag?1:0;
     if (browser_doc !== doc_flag) {
@@ -558,7 +678,25 @@ function search_arg(title, arg) {
     if (!autocomplete) return [];
     // for the arguments, we are only interested on the obj that match exactly the 'title', so we return only the args from this obj
     let results = completion_index.search({$and: [{"title": "=\"" + title + "\""}, {"args.text": "^\"" + arg + "\""}]});
-    return (results.length > 0) ? results[0].matches : [];
+    if (results.length > 0) {
+        let args = results[0].item.args;
+        // AG: Matched args are in matches.slice(1,), extract them.
+        // This code originally just returned matches itself, which has the
+        // text of all matched arguments, but not the occurrence data, and we
+        // need the latter to sort based on relevance.
+        results = results[0].matches.slice(1,).map(a => args[a.refIndex]);
+    }
+    return results;
+}
+
+function search_args(title) {
+    // like above, but look up *all* arg completions for a given object
+    if (!autocomplete) return [];
+    // for the arguments, we are only interested on the obj that match exactly the 'title', so we return only the args from this obj
+    let results = obj_exact_match(title);
+    // item.args is live data from the fuse, make sure to take a shallow copy
+    // with slice() which can be safely sorted in-place later
+    return (results.length > 0) ? results[0].item.args.slice() : [];
 }
 
 function index_obj_completion(obj_or_msg, obj_or_msg_text) {
@@ -631,34 +769,142 @@ function update_autocomplete_dd_arrowup(ac_dropdown) {
     }
 }
 
-function select_result_autocomplete_dd(textbox, ac_dropdown) {
+function select_result_autocomplete_dd(textbox, ac_dropdown, last, offs, res, dir) {
     if (ac_dropdown !== null) {
         let sel = ac_dropdown.getAttribute("selected_item");
         if (sel > -1) {
             textbox.innerText = ac_dropdown.children.item(sel).innerText;
             delete_autocomplete_dd(ac_dropdown);
-        } else { // it only passes here when the user presses 'tab' and there is no option selected
-            textbox.innerText = ac_dropdown.children.item(0).innerText;
+            return [sel+offs, offs];
+        } else {
+	    // We only come here if the user presses 'tab' and there is no
+	    // option selected.
+            var n = res.length;
+            if (n == 0) {
+                // It seems that while pondering the mysteries of the
+                // universe, your computer has lost our completion list. This
+                // shouldn't happen, but a little bit of defensive programming
+                // can't hurt.
+                return [-1,-1];
+            }
+            var next =
+		(dir==0 ? last : dir>0 ? last+1 : last<=0 ? n-1 : last-1) % n;
+	    // If the new index is outside the current scope of the popup,
+	    // repopulate the popup by shifting the entries accordingly (poor
+	    // man's scroll).
+	    if (next < offs) {
+		offs = next;
+		let c = ac_dropdown.childNodes;
+		c.forEach((r, i) => r.textContent = res[offs+i]);
+	    } else if (next > offs+7) {
+		offs = next-7;
+		let c = ac_dropdown.childNodes;
+		c.forEach((r, i) => r.textContent = res[offs+i]);
+	    }
+            textbox.innerText = res[next];
+            return [next, offs];
         }
+    } else {
+        return [-1,-1];
     }
 }
 
 // GB: update autocomplete dropdown with new results
 function repopulate_autocomplete_dd(doc, ac_dropdown, obj_class, text) {
     ac_dropdown().setAttribute("searched_text", text);
-    let title, arg;
+    let title, arg, have_arg;
     if (obj_class === "obj") {
         let text_array = text.split(" ");
         title = text_array[0].toString();
         arg = text_array.slice(1, text_array.length);
+        // check whether *anything* follows the obj name, even an empty arg
+        have_arg = arg.length !== 0;
         arg = (arg.length !== 0) ? arg.toString().replace(/\,/g, " ") : "";
     } else { // the autocomplete feature doesn't work with messages and comments
         return;
     }
 
-    // If there are arg, we are autocompleting the arg field, and if there isn't we are autocompleting the obj_title field
-    let results = (arg.length > 0) ? (search_arg(title, arg).slice(1,)) : (search_obj(title));
+    /* AG: We're dealing with three different cases here which must be handled
+       separately.
+
+       (1) We're completing an object name; in this case have_arg is false and
+       arg is empty as well.
+
+       (2) We're about to start argument completion; here we have that
+       have_arg is true even though arg is still empty (which happens as soon
+       as you enter a blank after the object name).
+
+       (3) We're in the middle of argument completion (started typing some
+       arguments) in which case have_arg is true and arg is non-empty as
+       well. */
+    let results = (arg.length > 0) ? (search_arg(title, arg)) : have_arg ? (search_args(title)) : (search_obj(title));
+
+    /* AG: Here we massage the result list from what Fuse delivers, which is
+       based on scoring similarity and can look pretty random at times. What
+       we actually want here is an order which also takes into account
+       relevance (as determined by the occurences and library information that
+       we have), and the alphabetic order of the available completions. The
+       latter tends to make the list tidier and easier to navigate.
+
+       The final step then is to condense the result list to a simple string
+       list, since we don't use the other data anymore beyond this point.
+
+       NB: The use of library information was an idea proposed by JW, and we
+       record that now, but unfortunately the information available in the
+       help patches is incomplete and inconsistent. Notable exceptions are
+       'internal' and 'cyclone', for which there are plenty of entries. At
+       present we only use 'internal' to identify built-ins, so that they will
+       be preferred in relevance ordering. This has the advantage that it
+       works from the get-go when there's not much live usage data yet, as
+       these objects will tend to be used most frequently by most Pd
+       users. But only time and user feedback will tell whether it actually
+       makes sense to put this criterion above the live usage data that we
+       have in the occurrences field. */
+    if (arg.length > 0 || have_arg) {
+        // argument completions, these don't have scores, order them by
+        // just relevance and text
+        results.sort((a, b) =>
+            a.occurrences == b.occurrences || !autocomplete_relevance
+                ? (a.text == b.text ? 0 : a.text < b.text ? -1 : 1)
+                : b.occurrences - a.occurrences);
+        results = results.map(a => title + " " + a.text);
+    } else {
+        // object completions, order by score, relevance and item.title
+        results.sort(function (a, b) {
+            /* XXXREVIEW: We might have to revisit this. We currently use a
+             * numeric relevance score to make this easy to adjust. The boost
+             * factor determines the relative weight of built-ins over live
+             * usage data. We currently have this at 50, so that you need 50
+             * uses of an external to win against a built-in. I hope that this
+             * will work well in practice, but currently noone knows. Also,
+             * should score take precedence over relevance? Currently we
+             * demote it to a secondary criterion, but score can be pretty
+             * important if the auto-completion prefix option is disabled
+             * (which it is by default). */
+            const boost = 50; /* NOTE: Increasing the boost to a very large
+                               * value >> 1 makes built-ins effectively take
+                               * priority over live usage data, decreasing it
+                               * to a very small value << 1 makes live usage
+                               * data the most important. */
+            let aflag = a.item.hasOwnProperty("lib") && a.item.lib == "internal" ? 1 : 0;
+            let bflag = b.item.hasOwnProperty("lib") && b.item.lib == "internal" ? 1 : 0;
+            let afreq = a.item.occurrences, bfreq = b.item.occurrences;
+            let relevance = (bflag-aflag)*boost + (bfreq-afreq);
+            if (autocomplete_relevance && relevance !== 0) {
+                return relevance;
+            } else if (a.score == b.score) {
+                return a.item.title == b.item.title ? 0
+                    : a.item.title < b.item.title ? -1 : 1;
+            } else {
+                let d = a.score - b.score;
+                return d == 0 ? 0 : d < 0 ? -1 : 1;
+            }
+        });
+        results = results.map(a => a.item.title);
+    }
 
+    // record the complete results, we need them for tab completion
+    let all_results = results;
     // GB TODO: ideally we should be able to show all the results in a limited window with a scroll bar
     let n = 8; // Maximum number of suggestions
     if (results.length > n) results = results.slice(0,n);
@@ -666,8 +912,8 @@ function repopulate_autocomplete_dd(doc, ac_dropdown, obj_class, text) {
     ac_dropdown().innerHTML = ""; // clear all old results
     if (results.length > 0) {
         // for each result, make a paragraph child of autocomplete_dropdown
+        let h = ac_dropdown().getAttribute("font_height");
         results.forEach(function (f,i,a) {
-            let h = ac_dropdown().getAttribute("font_height");
             let y = h*(i+1);
             let r = doc.createElement("p");
             r.setAttribute("width", "150");
@@ -675,17 +921,14 @@ function repopulate_autocomplete_dd(doc, ac_dropdown, obj_class, text) {
             r.setAttribute("y", y);
             r.setAttribute("class", "border");
             r.setAttribute("idx", i);
-            if (arg.length < 1) { // autocomplete object
-                r.textContent = f.item.title;
-            } else { // autocomplete argument, message or comment
-                r.textContent = ((obj_class==="obj")?(title+" "):"") + f.value;
-            }
+            r.textContent = f;
             ac_dropdown().appendChild(r);
         })
         ac_dropdown().setAttribute("selected_item", "-1");
     } else { // if there is no suggestion candidate, the autocompletion dropdown should disappear
         delete_autocomplete_dd (ac_dropdown());
     }
+    return all_results;
 }
 
 // GB: create autocomplete dropdown based on the properties of the textbox for new_obj_element
@@ -729,6 +972,81 @@ function delete_autocomplete_dd (ac_dropdown) {
     }
 }
 
+function check_completion(text)
+{
+    if (!autocomplete) return false;
+    // checks whether an exact match (title+args) exists for the given text
+    let text_array = text.split(" ");
+    let title = text_array[0].toString();
+    let arg = text_array.slice(1, text_array.length);
+    if (arg.length > 0) {
+        arg = arg.toString().replace(/\,/g, " ");
+    } else {
+        arg = "";
+    }
+    let obj_result = obj_exact_match(title);
+    if (obj_result.length !== 0) {
+        let args = obj_result[0].item.args;
+        if (arg !== "") {
+            let arg_result = arg_exact_match(title, arg);
+            if (arg_result.length !== 0) {
+                // found exact title+args match
+                return true;
+            }
+        } else {
+            // no args, found exact title match
+            return true;
+        }
+    }
+    // no matches found
+    return false;
+}
+
+function remove_completion(text, log)
+{
+    if (!autocomplete) return false;
+    // Check to see whether we're removing an object or just its arguments
+    // from the index.
+    let text_array = text.split(" ");
+    let title = text_array[0].toString();
+    let arg = text_array.slice(1, text_array.length);
+    if (arg.length > 0) {
+        arg = arg.toString().replace(/\,/g, " ");
+        //log("title: "+title+", args: "+arg);
+    } else {
+        arg = "";
+        //log("title: "+title);
+    }
+    let obj_result = obj_exact_match(title);
+    if (obj_result.length !== 0) {
+        let obj_ref = obj_result[0].refIndex;
+        let obj_freq = obj_result[0].item.occurrences;
+        let args = obj_result[0].item.args;
+        if (arg !== "") {
+            let arg_result = arg_exact_match(title, arg);
+            if (arg_result.length !== 0) {
+                // found exact title+args match
+                let arg_ref = arg_result[0].matches[1].refIndex;
+                //log("removing %s %s at index %d %d", title, arg, obj_ref, arg_ref);
+                // remove from args
+                args.splice(arg_ref, 1);
+                // update the object
+                let obj = {"occurrences" : obj_freq, "title" : title, "args" : args};
+                //log("updating %d with %o", obj_ref, obj);
+                completion_index.update(obj, obj_ref);
+                return true;
+            }
+        } else {
+            // no args, found exact title match, remove it
+            //log("removing %s at index %d", title, obj_ref);
+            completion_index.removeAt(obj_ref);
+            return true;
+        }
+    }
+    // no matches found
+    return false;
+}
+
 exports.index_obj_completion = index_obj_completion;
 exports.write_completion_index = write_completion_index;
 exports.update_autocomplete_selected = update_autocomplete_selected;
@@ -738,6 +1056,8 @@ exports.select_result_autocomplete_dd = select_result_autocomplete_dd;
 exports.repopulate_autocomplete_dd = repopulate_autocomplete_dd;
 exports.create_autocomplete_dd = create_autocomplete_dd;
 exports.delete_autocomplete_dd = delete_autocomplete_dd;
+exports.check_completion = check_completion;
+exports.remove_completion = remove_completion;
 
 // Modules
 
@@ -6656,12 +6976,12 @@ function gui_midi_properties(gfxstub, sys_indevs, sys_outdevs,
 }
 
 function gui_gui_properties(dummy, name, show_grid, grid_size, save_zoom,
-                            autocomplete, autocomplete_prefix,
+                            autocomplete, autocomplete_prefix, autocomplete_relevance,
                             browser_doc, browser_path, browser_init,
                             autopatch_yoffset) {
     if (dialogwin["prefs"] !== null) {
         dialogwin["prefs"].window.gui_prefs_callback(name, show_grid, grid_size,
-            save_zoom, autocomplete, autocomplete_prefix,
+            save_zoom, autocomplete, autocomplete_prefix, autocomplete_relevance,
             browser_doc, browser_path, browser_init, autopatch_yoffset);
     }
 }
diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c
index d302ea9748dd717b061d0a03352db71c1da4e606..e4f9f85c92288149410c972668e9de98fc8ba83f 100644
--- a/pd/src/m_glob.c
+++ b/pd/src/m_glob.c
@@ -81,7 +81,7 @@ static void glob_perf(t_pd *dummy, float f)
 }
 
 extern int sys_snaptogrid, sys_gridsize, sys_zoom,
-    sys_autocomplete, sys_autocomplete_prefix,
+    sys_autocomplete, sys_autocomplete_prefix, sys_autocomplete_relevance,
     sys_browser_doc, sys_browser_path, sys_browser_init,
     sys_autopatch_yoffset;
 extern t_symbol *sys_gui_preset;
@@ -93,6 +93,7 @@ static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
     sys_zoom = !!atom_getintarg(0, argc--, argv++);
     sys_autocomplete = !!atom_getintarg(0, argc--, argv++);
     sys_autocomplete_prefix = !!atom_getintarg(0, argc--, argv++);
+    sys_autocomplete_relevance = !!atom_getintarg(0, argc--, argv++);
     sys_browser_doc = !!atom_getintarg(0, argc--, argv++);
     sys_browser_path = !!atom_getintarg(0, argc--, argv++);
     sys_browser_init = !!atom_getintarg(0, argc--, argv++);
@@ -102,7 +103,7 @@ static void glob_gui_prefs(t_pd *dummy, t_symbol *s, int argc, t_atom *argv)
 /* just the gui-preset, the save-zoom toggle and various help browser options for now */
 static void glob_gui_properties(t_pd *dummy)
 {
-    gui_vmess("gui_gui_properties", "xsiiiiiiiii",
+    gui_vmess("gui_gui_properties", "xsiiiiiiiiii",
         dummy,
         sys_gui_preset->s_name,
         sys_snaptogrid,
@@ -110,6 +111,7 @@ static void glob_gui_properties(t_pd *dummy)
         sys_zoom,
         sys_autocomplete,
         sys_autocomplete_prefix,
+        sys_autocomplete_relevance,
         sys_browser_doc,
         sys_browser_path,
         sys_browser_init,
diff --git a/pd/src/s_file.c b/pd/src/s_file.c
index f6c00e169dbd649707c22a3821feee5631a498de..5f8f570e65fc88fdf7379053292c8a112f4c0b8c 100644
--- a/pd/src/s_file.c
+++ b/pd/src/s_file.c
@@ -43,7 +43,8 @@
 #endif
 
 int sys_defeatrt, sys_autopatch_yoffset, sys_snaptogrid = 1, sys_gridsize = 10,
-    sys_zoom, sys_autocomplete, sys_autocomplete_prefix,
+    sys_zoom, sys_autocomplete = 1, sys_autocomplete_prefix,
+    sys_autocomplete_relevance = 1,
     sys_browser_doc = 1, sys_browser_path, sys_browser_init;
 t_symbol *sys_flags = &s_;
 void sys_doflags( void);
@@ -681,6 +682,8 @@ void sys_loadpreferences( void)
         sscanf(prefbuf, "%d", &sys_autocomplete);
     if (sys_getpreference("autocomplete_prefix", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_autocomplete_prefix);
+    if (sys_getpreference("autocomplete_relevance", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_autocomplete_relevance);
     if (sys_getpreference("browser_doc", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_browser_doc);
     if (sys_getpreference("browser_path", prefbuf, MAXPDSTRING))
@@ -836,6 +839,8 @@ void glob_savepreferences(t_pd *dummy)
     sys_putpreference("autocomplete", buf1);
     sprintf(buf1, "%d", sys_autocomplete_prefix);
     sys_putpreference("autocomplete_prefix", buf1);
+    sprintf(buf1, "%d", sys_autocomplete_relevance);
+    sys_putpreference("autocomplete_relevance", buf1);
     sprintf(buf1, "%d", sys_browser_doc);
     sys_putpreference("browser_doc", buf1);
     sprintf(buf1, "%d", sys_browser_path);
diff --git a/pd/src/s_main.c b/pd/src/s_main.c
index e002b2c3f8945d9e44c34c61faf77197466c74be..0fb8cc06556a53053ba314b961296393bee3221e 100644
--- a/pd/src/s_main.c
+++ b/pd/src/s_main.c
@@ -331,7 +331,8 @@ void glob_forward_files_from_secondary_instance(void)
 
 extern void glob_recent_files(t_pd *dummy);
 extern int sys_browser_doc, sys_browser_path, sys_browser_init;
-extern int sys_autocomplete, sys_autocomplete_prefix;
+extern int sys_autocomplete, sys_autocomplete_prefix,
+  sys_autocomplete_relevance;
 
 /* this is called from main() in s_entry.c */
 int sys_main(int argc, char **argv)
@@ -412,9 +413,10 @@ int sys_main(int argc, char **argv)
     glob_recent_files(0);
         /* AG: send the browser config; this must come *after* gui_set_lib_dir
            so that the lib_dir is available when help indexing starts */
-    gui_start_vmess("gui_set_browser_config", "iiiii",
+    gui_start_vmess("gui_set_browser_config", "iiiiii",
                     sys_browser_doc, sys_browser_path, sys_browser_init,
-                    sys_autocomplete, sys_autocomplete_prefix);
+                    sys_autocomplete, sys_autocomplete_prefix,
+                    sys_autocomplete_relevance);
     gui_start_array();
     for (nl = sys_helppath; nl; nl = nl->nl_next)
     {