From b21b6c54269c692b7dd8e9198eaead1cb939e0c6 Mon Sep 17 00:00:00 2001 From: Peter Tirsek Date: Sun, 14 Aug 2022 00:52:56 -0500 Subject: Add support for tab-completion when selecting by rule When selecting messages based on a previously defined rule, this patch allows for completing the name of the rule if the enable-tab-completion feature is set. --- alpine/mailcmd.c | 147 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 136 insertions(+), 11 deletions(-) diff --git a/alpine/mailcmd.c b/alpine/mailcmd.c index ed93348d..05c7de26 100644 --- a/alpine/mailcmd.c +++ b/alpine/mailcmd.c @@ -107,7 +107,8 @@ SEARCHSET *visible_searchset(MAILSTREAM *, MSGNO_S *); int select_by_status(MAILSTREAM *, SEARCHSET **); int select_by_rule(MAILSTREAM *, SEARCHSET **); int select_by_thread(MAILSTREAM *, MSGNO_S *, SEARCHSET **); -char *choose_a_rule(int); +char *choose_a_rule(int, char *); +int rulenick_complete(int, char *, int); int select_by_keyword(MAILSTREAM *, SEARCHSET **); char *choose_a_keyword(void); int select_sort(struct pine *, int, SortOrder *, int *); @@ -328,6 +329,14 @@ static ESCKEY_S sel_size_opt[] = { {-1, 0, NULL, NULL} }; +static ESCKEY_S sel_rule_opt[] = { + {0, 0, NULL, NULL}, + {ctrl('T'), 14, "^T", N_("To List")}, + {0, 0, NULL, NULL}, /* Reserved for TAB completion */ + {'!', '!', "!", N_("Not")}, + {-1, 0, NULL, NULL} +}; + static ESCKEY_S sel_key_opt[] = { {0, 0, NULL, NULL}, {ctrl('T'), 14, "^T", N_("To List")}, @@ -8856,7 +8865,7 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) { char rulenick[1000], *nick; PATGRP_S *patgrp; - int r, not = 0, we_cancel = 0, rflags = ROLE_DO_SRCH + int r, last_r = 0, not = 0, we_cancel = 0, rflags = ROLE_DO_SRCH | ROLE_DO_INCOLS | ROLE_DO_ROLES | ROLE_DO_SCORES @@ -8866,6 +8875,16 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) rulenick[0] = '\0'; ps_global->mangled_footer = 1; + if(F_ON(F_ENABLE_TAB_COMPLETE, ps_global)){ + sel_rule_opt[2].ch = TAB; + sel_rule_opt[2].rval = 15; + sel_rule_opt[2].name = "TAB"; + sel_rule_opt[2].label = N_("Complete"); + } + else{ + memset(&sel_rule_opt[2], 0, sizeof(sel_rule_opt[2])); + } + do{ int oe_flags; @@ -8874,11 +8893,11 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) sizeof(rulenick), not ? _("Rule to NOT match: ") : _("Rule to match: "), - sel_key_opt, NO_HELP, &oe_flags); + sel_rule_opt, NO_HELP, &oe_flags); if(r == 14){ /* select rulenick from a list */ - if((nick=choose_a_rule(rflags)) != NULL){ + if((nick=choose_a_rule(rflags, NULL)) != NULL){ strncpy(rulenick, nick, sizeof(rulenick)-1); rulenick[sizeof(rulenick)-1] = '\0'; fs_give((void **) &nick); @@ -8886,6 +8905,23 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) else r = 4; } + else if(r == 15){ + int n = rulenick_complete(rflags, rulenick, sizeof(rulenick)); + + if(n > 1 && last_r == 15 && !(oe_flags & OE_USER_MODIFIED)){ + /* double tab with multiple completions: select from list */ + if((nick=choose_a_rule(rflags, rulenick)) != NULL){ + strncpy(rulenick, nick, sizeof(rulenick)-1); + rulenick[sizeof(rulenick)-1] = '\0'; + fs_give((void **) &nick); + r = 0; + } + else + r = 4; + } + else if(n != 1) + Writechar(BELL, 0); + } else if(r == '!') not = !not; @@ -8899,8 +8935,9 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) } removing_leading_and_trailing_white_space(rulenick); + last_r = r; - }while(r == 3 || r == 4 || r == '!'); + }while(r == 3 || r == 4 || r == 15 || r == '!'); /* @@ -8942,14 +8979,18 @@ select_by_rule(MAILSTREAM *stream, SEARCHSET **limitsrch) /* * Allow user to choose a rule from their list of rules. * + * Args rflags -- Pattern types to choose from + * prefix -- Only list rules matching the given prefix + * * Returns an allocated rule nickname on success, NULL otherwise. */ char * -choose_a_rule(int rflags) +choose_a_rule(int rflags, char *prefix) { char *choice = NULL; char **rule_list, **lp; int cnt = 0; + int prefix_length = 0; PAT_S *pat; PAT_STATE pstate; void (*redraw)(void) = ps_global->redrawer; @@ -8964,8 +9005,15 @@ choose_a_rule(int rflags) * Build a list of rules to choose from. */ - for(pat = first_pattern(&pstate); pat; pat = next_pattern(&pstate)) - cnt++; + if(prefix) + prefix_length = strlen(prefix); + + for(pat = first_pattern(&pstate); pat; pat = next_pattern(&pstate)) { + if(!pat->patgrp || !pat->patgrp->nick) + continue; + if(!prefix || strncmp(pat->patgrp->nick, prefix, prefix_length) == 0) + cnt++; + } if(cnt <= 0){ q_status_message(SM_ORDER, 3, 4, _("No rules defined, use Setup/Rules")); @@ -8975,9 +9023,12 @@ choose_a_rule(int rflags) lp = rule_list = (char **) fs_get((cnt + 1) * sizeof(*rule_list)); memset(rule_list, 0, (cnt+1) * sizeof(*rule_list)); - for(pat = first_pattern(&pstate); pat; pat = next_pattern(&pstate)) - *lp++ = cpystr((pat->patgrp && pat->patgrp->nick) - ? pat->patgrp->nick : "?"); + for(pat = first_pattern(&pstate); pat; pat = next_pattern(&pstate)) { + if(!pat->patgrp || !pat->patgrp->nick) + continue; + if(!prefix || strncmp(pat->patgrp->nick, prefix, prefix_length) == 0) + *lp++ = cpystr(pat->patgrp->nick); + } /* TRANSLATORS: SELECT A RULE is a screen title TRANSLATORS: Print something1 using something2. @@ -8997,6 +9048,80 @@ choose_a_rule(int rflags) } +/* + * Complete a partial rule name against the user's list of rules. + * + * Args rflags -- Pattern types to choose from + * nick -- Current rule nickname to complete + * nick_size -- Maximum length of the nick array to store completion in + * + * Returns the number of valid completions, i.e.: + * + * 0 if there are no valid completions. The value of the nick argument has not + * been changed. + * 1 if there is only a single valid completion. The value of the nick argument + * has been replaced with the full text of that completion. + * >1 if there is more than one valid completion. The value of the nick argument + * has been replaced with the longest substring common to all the appropriate + * completions. + */ +int +rulenick_complete(int rflags, char *nick, int nick_size) +{ + char *candidate = NULL; + int cnt = 0; + int common_prefix_length = 0; + int nick_length; + PAT_S *pat; + PAT_STATE pstate; + + if(!(nonempty_patterns(rflags, &pstate) && first_pattern(&pstate))) + return 0; + + nick_length = strlen(nick); + + for(pat = first_pattern(&pstate); pat; pat = next_pattern(&pstate)){ + if(!pat->patgrp || !pat->patgrp->nick) + continue; + if(strncmp(pat->patgrp->nick, nick, nick_length) == 0){ + /* This is a candidate for completion. */ + cnt++; + if(!candidate){ + /* This is the first candidate. Keep it as a future reference to + * compare against to find the longest common prefix length of + * all the matches. */ + candidate = pat->patgrp->nick; + common_prefix_length = strlen(pat->patgrp->nick); + } + else{ + /* Find the common prefix length between the first candidate and + * this one. */ + int i; + for(i = 0; i < common_prefix_length; i++){ + if(pat->patgrp->nick[i] != candidate[i]){ + /* In the event that we ended up in the middle of a + * UTF-8 code point, backtrack to the byte before the + * start of this code point. */ + while(i > 0 && (candidate[i] & 0xC0) == 0x80) + i--; + common_prefix_length = i; + break; + } + } + } + } + } + + if(cnt > 0){ + int length = MIN(nick_size, common_prefix_length); + strncpy(nick + nick_length, candidate + nick_length, length - nick_length); + nick[length] = '\0'; + } + + return cnt; +} + + /* * Select by current thread. * Sets searched bits in mail_elts for this entire thread -- cgit v1.2.3-70-g09d2