From a47553b11de2d4894534baf6df880c7548dc20fd Mon Sep 17 00:00:00 2001 From: Albert Graef <aggraef@gmail.com> Date: Tue, 16 Aug 2022 00:57:28 +0200 Subject: [PATCH] Add an option to sort completions by relevance. If enabled, this orders object and argument completions first by relevance, then alphabetically by object name, with "relevance" currently being defined as frequency of use (as given by the occurrence field in the completion data). As usual, the option is stored in the user preferences, its checkbox can be found on the GUI Preferences tab. --- pd/nw/dialog_prefs.html | 12 ++++- pd/nw/locales/de/translation.json | 4 +- pd/nw/locales/en/translation.json | 4 +- pd/nw/locales/fr/translation.json | 4 +- pd/nw/pdgui.js | 86 ++++++++++++++++++++----------- pd/src/m_glob.c | 6 ++- pd/src/s_file.c | 7 ++- pd/src/s_main.c | 8 +-- 8 files changed, 91 insertions(+), 40 deletions(-) diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html index 1af561488..6a01a941e 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/locales/de/translation.json b/pd/nw/locales/de/translation.json index 5ff39dab7..8a5d49519 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 b18fd87d2..d340835b2 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 f5dd974a9..4721a9c13 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/pdgui.js b/pd/nw/pdgui.js index 8c9441eba..05ee50684 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,6 +49,7 @@ 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 @@ -570,11 +571,12 @@ function rebuild_index() } // 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 = 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) { @@ -643,7 +645,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) { @@ -727,6 +747,13 @@ function select_result_autocomplete_dd(textbox, ac_dropdown, last, offs, res, di // 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, @@ -777,36 +804,37 @@ function repopulate_autocomplete_dd(doc, ac_dropdown, obj_class, text) { (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).slice(1,)) : have_arg ? (obj_exact_match(title)) : (search_obj(title)); - if (arg.length < 1 && have_arg && results.length > 0) { - results = results[0].item.args; - } + let results = (arg.length > 0) ? (search_arg(title, arg)) : have_arg ? (search_args(title)) : (search_obj(title)); /* AG: Massage the result list from what Fuse delivers, which is based on - scoring similarity and can appear pretty random at times. For now, we - just do a lexicographic sort on score first, then the completion text - (in particular, the latter makes sure that for each score the shortest - matches come first, which you'd expect but isn't always guaranteed with - Fuse). In the future we may incorporate the occurrence counts which are - already in GB's implementation, but AFAICT aren't currently used - anywhere. Finally, we condense the result list to a simple string list, - since we don't use all the other data any more beyond this point. */ - if (arg.length < 1 && have_arg) { - // all argument completions, order them lexicographically + scoring similarity and can appear pretty random at times. What we + actually want here is an order based on scores, which also takes into + account relevance (as determined by the occurences field), and, last + but not least, an alphabetic ordering of the available completions (in + particular, the latter makes sure that for each score and relevance the + shortest matches come first, which you'd expect but isn't always + guaranteed with Fuse). Finally, we condense the result list to a simple + string list, since we don't use all the other data anymore beyond this + point. */ + 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.text == b.text ? 0 : a.text < b.text ? -1 : 1); + 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 if (arg.length > 0) { - // matching arguments, order them lexicographically - results.sort((a, b) => - a.value == b.value ? 0 : a.value < b.value ? -1 : 1); - results = results.map(a => title + " " + a.value); } else { - // object completions, sort by score and item.title + // object completions, order by score, relevance and item.title results.sort(function (a, b) { if (a.score == b.score) { - return a.item.title == b.item.title ? 0 - : a.item.title < b.item.title ? -1 : 1; + if (a.item.occurrences == b.item.occurrences || + !autocomplete_relevance) { + return a.item.title == b.item.title ? 0 + : a.item.title < b.item.title ? -1 : 1; + } else { + return b.item.occurrences - a.item.occurrences; + } } else { let d = a.score - b.score; return d == 0 ? 0 : d < 0 ? -1 : 1; @@ -6888,12 +6916,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 d302ea974..e4f9f85c9 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 02100c621..a290215ee 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 = 1, sys_autocomplete_prefix, + sys_zoom, + sys_autocomplete = 1, sys_autocomplete_prefix, sys_autocomplete_relevance, 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 e002b2c3f..0fb8cc065 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) { -- GitLab