]> granicus.if.org Git - neomutt/commitdiff
Extend the ~m pattern to allow relative ranges
authorIan Zimmerman <itz@primate.net>
Fri, 16 Dec 2016 18:42:36 +0000 (10:42 -0800)
committerRichard Russon <rich@flatcap.org>
Sat, 21 Jan 2017 01:06:22 +0000 (01:06 +0000)
The ~m pattern now accepts absolute ranges, relative ranges and has some
mnemonics (shortcuts).

There are shortcuts for some emails:
    ^   First email
    .   Current email
    $   Last email

These shortcuts can be used anywhere a number is used in the examples
below.

Relative ranges have a comma ',' in them:
    -3,.    Previous 3 emails plus the current one
    .,$     Current email and all the later ones
    -2,2    Previous 2, current and next 2 emails
    0,1     Current and next email

Absolute range:
    3-10    Emails 3-10
    -10     Emails 1-10
    10-     Emails 10-last
    7       Just email 7

Closes: #269
Closes: #283
curs_main.c
mutt.h
mutt_curses.h
pattern.c

index 70daba7f466c9cf93cc81edb63db22cd77e01045..77b94c35e8633fee4251afe5fb9db3c58abab5da 100644 (file)
@@ -880,6 +880,9 @@ int mutt_index_menu (void)
       unset_option (OPTREDRAWTREE);
     }
 
+    if (Context)
+      Context->menu = menu;
+
     if (Context && !attach_msg)
     {
       int check;
diff --git a/mutt.h b/mutt.h
index 6f4707b408abf7bad9fa6d5e4c449549ff462326..3c70f4a9c17c3a161a0938e81334334e67cb5d2f 100644 (file)
--- a/mutt.h
+++ b/mutt.h
@@ -124,12 +124,6 @@ typedef struct
   int destroy; /* destroy `data' when done? */
 } BUFFER;
 
-typedef struct
-{
-  int ch; /* raw key pressed */
-  int op; /* function op */
-} event_t;
-
 /* flags for _mutt_system() */
 #define MUTT_DETACH_PROCESS    1       /* detach subprocess from group */
 
@@ -1005,6 +999,8 @@ struct mx_ops
   int (*open_new_msg) (struct _message *, struct _context *, HEADER *);
 };
 
+#include "mutt_menu.h"
+
 typedef struct _context
 {
   char *path;
@@ -1033,6 +1029,8 @@ typedef struct _context
   int flagged;                 /* how many flagged messages */
   int msgnotreadyet;           /* which msg "new" in pager, -1 if none */
 
+  MUTTMENU *menu;               /* needed for pattern compilation */
+
   short magic;                 /* mailbox type */
 
   unsigned char rights[(RIGHTSMAX + 7)/8];     /* ACL bits */
index bb6aa00ca7f549f7ff2755754a84986a40d217eb..74dc753aead7099c8ceea31e2327ae35ca91d1f4 100644 (file)
@@ -76,6 +76,12 @@ void mutt_curs_set (int);
 #define CI_is_return(c) ((c) == '\r' || (c) == '\n')
 #endif
 
+typedef struct
+{
+  int ch; /* raw key pressed */
+  int op; /* function op */
+} event_t;
+
 event_t mutt_getch (void);
 
 void mutt_endwin (const char *);
@@ -146,8 +152,8 @@ typedef struct color_line
   regex_t rx;
   int match; /* which substringmap 0 for old behaviour */
   char *pattern;
-  pattern_t *color_pattern; /* compiled pattern to speed up index color
-                               calculation */
+  struct pattern_t *color_pattern; /* compiled pattern to speed up index color
+                                      calculation */
   short fg;
   short bg;
   int pair;
index 77ca6031e026c76dd1e89a4472c06a27ba8b2160..ac344fcb4c17136918e15e4643691626c382a54a 100644 (file)
--- a/pattern.c
+++ b/pattern.c
@@ -36,6 +36,7 @@
 #include "mutt_crypt.h"
 #include "mutt_curses.h"
 #include "group.h"
+#include "mutt_menu.h"
 
 #ifdef USE_IMAP
 #include "mx.h"
@@ -49,6 +50,7 @@
 static int eat_regexp (pattern_t *pat, BUFFER *, BUFFER *);
 static int eat_date (pattern_t *pat, BUFFER *, BUFFER *);
 static int eat_range (pattern_t *pat, BUFFER *, BUFFER *);
+static int eat_message_range (pattern_t *pat, BUFFER *s, BUFFER *err);
 static int patmatch (const pattern_t *pat, const char *buf);
 
 static const struct pattern_flags
@@ -79,7 +81,7 @@ Flags[] =
   { 'k', MUTT_PGP_KEY,         0,              NULL },
   { 'l', MUTT_LIST,            0,              NULL },
   { 'L', MUTT_ADDRESS,         0,              eat_regexp },
-  { 'm', MUTT_MESSAGE,         0,              eat_range },
+  { 'm', MUTT_MESSAGE,         0,              eat_message_range },
   { 'n', MUTT_SCORE,           0,              eat_range },
   { 'N', MUTT_NEW,                     0,              NULL },
   { 'O', MUTT_OLD,                     0,              NULL },
@@ -358,6 +360,289 @@ static int eat_regexp (pattern_t *pat, BUFFER *s, BUFFER *err)
   return 0;
 }
 
+#define KILO 1024
+#define MEGA 1048576
+#define HMSG(h) (((h)->msgno) + 1)
+#define CTX_MSGNO(c) (HMSG((c)->hdrs[(c)->v2r[(c)->menu->current]]))
+
+enum
+{
+  RANGE_K_REL,
+  RANGE_K_ABS,
+  RANGE_K_LT,
+  RANGE_K_GT,
+  RANGE_K_BARE,
+  /* add new ones HERE */
+  RANGE_K_INVALID
+};
+
+static int
+scan_range_num (BUFFER *s, regmatch_t pmatch[], int group, int kind)
+{
+  int num;
+  unsigned char c;
+
+  /* this cast looks dangerous, but is already all over this code
+   * (explicit or not) */
+  num = (int)strtol(&s->dptr[pmatch[group].rm_so], NULL, 0);
+  c = (unsigned char)(s->dptr[pmatch[group].rm_eo - 1]);
+  if (toupper(c) == 'K')
+    num *= KILO;
+  else if (toupper(c) == 'M')
+    num *= MEGA;
+  switch (kind)
+  {
+  case RANGE_K_REL:
+    return num + CTX_MSGNO(Context);
+  case RANGE_K_LT:
+    return num - 1;
+  case RANGE_K_GT:
+    return num + 1;
+  default:
+    return num;
+  }
+}
+
+#define RANGE_DOT '.'
+#define RANGE_CIRCUM '^'
+#define RANGE_DOLLAR '$'
+#define RANGE_LT '<'
+#define RANGE_GT '>'
+
+/* range sides: left or right */
+enum
+{
+  RANGE_S_LEFT,
+  RANGE_S_RIGHT
+};
+
+static int
+scan_range_slot (BUFFER *s, regmatch_t pmatch[], int grp,
+                 int side, int kind)
+{
+  unsigned char c;
+
+  /* This means the left or right subpattern was empty, e.g. ",." */
+  if ((pmatch[grp].rm_so == -1) || (pmatch[grp].rm_so == pmatch[grp].rm_eo))
+  {
+    if (side == RANGE_S_LEFT)
+      return 1;
+    else if (side == RANGE_S_RIGHT)
+      return Context->msgcount;
+  }
+  /* We have something, so determine what */
+  c = (unsigned char)(s->dptr[pmatch[grp].rm_so]);
+  switch (c)
+  {
+  case RANGE_CIRCUM:
+    return 1;
+  case RANGE_DOLLAR:
+    return Context->msgcount;
+  case RANGE_DOT:
+    return CTX_MSGNO(Context);
+  case RANGE_LT:
+  case RANGE_GT:
+    return scan_range_num(s, pmatch, grp+1, kind);
+  default:
+    /* Only other possibility: a number */
+    return scan_range_num(s, pmatch, grp, kind);
+  }
+}
+
+static void
+order_range (pattern_t *pat)
+{
+  int num;
+
+  if (pat->min <= pat->max)
+    return;
+  num = pat->min;
+  pat->min = pat->max;
+  pat->max = num;
+}
+
+/* Error codes for eat_range_by_regexp */
+enum
+{
+  RANGE_E_OK,
+  RANGE_E_SYNTAX,
+  RANGE_E_CTX,
+};
+
+static int
+report_regerror(int regerr, regex_t *preg, BUFFER *err)
+{
+  size_t ds = err->dsize;
+
+  if (regerror(regerr, preg, err->data, ds) > ds)
+    dprint(2, (debugfile, "warning: buffer too small for regerror\n"));
+  /* The return value is fixed, exists only to shorten code at callsite */
+  return RANGE_E_SYNTAX;
+}
+
+static int
+is_context_available(BUFFER *s, regmatch_t pmatch[], int kind, BUFFER *err)
+{
+  char *context_loc;
+  const char *context_req_chars[] =
+  {
+    [RANGE_K_REL] = ".0123456789",
+    [RANGE_K_ABS] = ".",
+    [RANGE_K_LT] = "",
+    [RANGE_K_GT] = "",
+    [RANGE_K_BARE] = ".",
+  };
+
+  /* First decide if we're going to need the context at all.
+   * Relative patterns need it iff they contain a dot or a number.
+   * Absolute patterns only need it if they contain a dot. */
+  context_loc = strpbrk(s->dptr+pmatch[0].rm_so, context_req_chars[kind]);
+  if ((context_loc == NULL) || (context_loc >= &s->dptr[pmatch[0].rm_eo]))
+    return 1;
+
+  /* We need a current message.  Do we actually have one? */
+  if (Context->menu)
+    return 1;
+
+  /* Nope. */
+  strfcpy(err->data, _("No current message"), err->dsize);
+  return 0;
+}
+
+#define RANGE_NUM_RX "([[:digit:]]+|0x[[:xdigit:]]+)[MmKk]?"
+
+#define RANGE_REL_SLOT_RX \
+    "[[:blank:]]*([.^$]|-?" RANGE_NUM_RX ")?[[:blank:]]*"
+
+#define RANGE_REL_RX "^" RANGE_REL_SLOT_RX "," RANGE_REL_SLOT_RX
+
+/* Almost the same, but no negative numbers allowed */
+#define RANGE_ABS_SLOT_RX \
+    "[[:blank:]]*([.^$]|" RANGE_NUM_RX ")?[[:blank:]]*"
+
+#define RANGE_ABS_RX "^" RANGE_ABS_SLOT_RX "-" RANGE_ABS_SLOT_RX
+
+/* Frist group is intentionally empty */
+#define RANGE_LT_RX "^()[[:blank:]]*(<[[:blank:]]*" RANGE_NUM_RX ")[[:blank:]]*"
+
+#define RANGE_GT_RX "^()[[:blank:]]*(>[[:blank:]]*" RANGE_NUM_RX ")[[:blank:]]*"
+
+/* Single group for min and max */
+#define RANGE_BARE_RX "^[[:blank:]]*([.^$]|" RANGE_NUM_RX ")[[:blank:]]*"
+
+#define RANGE_RX_GROUPS 5
+
+struct range_regexp
+{
+  const char* raw;              /* regexp as string */
+  int lgrp;                     /* paren group matching the left side */
+  int rgrp;                     /* paren group matching the right side */
+  int ready;                    /* compiled yet? */
+  regex_t cooked;               /* compiled form */
+};
+
+static struct range_regexp range_regexps[] =
+{
+  [RANGE_K_REL] = {.raw = RANGE_REL_RX, .lgrp = 1, .rgrp = 3, .ready = 0},
+  [RANGE_K_ABS] = {.raw = RANGE_ABS_RX, .lgrp = 1, .rgrp = 3, .ready = 0},
+  [RANGE_K_LT] = {.raw = RANGE_LT_RX, .lgrp = 1, .rgrp = 2, .ready = 0},
+  [RANGE_K_GT] = {.raw = RANGE_GT_RX, .lgrp = 2, .rgrp = 1, .ready = 0},
+  [RANGE_K_BARE] = {.raw = RANGE_BARE_RX, .lgrp = 1, .rgrp = 1, .ready = 0},
+};
+
+static int
+eat_range_by_regexp (pattern_t *pat, BUFFER *s, int kind, BUFFER *err)
+{
+  int regerr;
+  regmatch_t pmatch[RANGE_RX_GROUPS];
+  struct range_regexp *pspec = &range_regexps[kind];
+
+  /* First time through, compile the big regexp */
+  if (!pspec->ready)
+  {
+    regerr = regcomp(&pspec->cooked, pspec->raw, REG_EXTENDED);
+    if (regerr)
+      return report_regerror(regerr, &pspec->cooked, err);
+    pspec->ready = 1;
+  }
+
+  /* Match the pattern buffer against the compiled regexp.
+   * No match means syntax error. */
+  regerr = regexec(&pspec->cooked, s->dptr, RANGE_RX_GROUPS, pmatch, 0);
+  if (regerr)
+    return report_regerror(regerr, &pspec->cooked, err);
+
+  if (!is_context_available(s, pmatch, kind, err))
+    return RANGE_E_CTX;
+
+  /* Snarf the contents of the two sides of the range. */
+  pat->min = scan_range_slot(s, pmatch, pspec->lgrp, RANGE_S_LEFT, kind);
+  pat->max = scan_range_slot(s, pmatch, pspec->rgrp, RANGE_S_RIGHT, kind);
+  dprint(1, (debugfile, "pat->min=%d pat->max=%d\n", pat->min, pat->max));
+
+  /* Special case for a bare 0. */
+  if ((kind == RANGE_K_BARE) && (pat->min == 0) && (pat->max == 0))
+  {
+    if (!Context->menu)
+    {
+      strfcpy(err->data, _("No current message"), err->dsize);
+      return RANGE_E_CTX;
+    }
+    pat->min = pat->max = CTX_MSGNO(Context);
+  }
+  
+  /* Since we don't enforce order, we must swap bounds if they're backward */
+  order_range(pat);
+
+  /* Slide pointer past the entire match. */
+  s->dptr += pmatch[0].rm_eo;
+  return RANGE_E_OK;
+}
+
+static int
+eat_message_range (pattern_t *pat, BUFFER *s, BUFFER *err)
+{
+  int skip_quote = 0;
+  int i_kind;
+
+  /* We need a Context for pretty much anything. */
+  if (!Context)
+  {
+    strfcpy(err->data, _("No Context"), err->dsize);
+    return -1;
+  }
+
+  /*
+   * If simple_search is set to "~m %s", the range will have double quotes
+   * around it...
+   */
+  if (*s->dptr == '"')
+  {
+    s->dptr++;
+    skip_quote = 1;
+  }
+
+  for (i_kind = 0; i_kind != RANGE_K_INVALID; ++i_kind)
+  {
+    switch (eat_range_by_regexp(pat, s, i_kind, err))
+    {
+    case RANGE_E_CTX:
+      /* This means it matched syntactically but lacked context.
+       * No point in continuing. */
+      break;
+    case RANGE_E_SYNTAX:
+      /* Try another syntax, then */
+      continue;
+    case RANGE_E_OK:
+      if (skip_quote && (*s->dptr == '"'))
+        s->dptr++;
+      SKIPWS (s->dptr);
+      return 0;
+    }
+  }
+  return -1;
+}
+
 int eat_range (pattern_t *pat, BUFFER *s, BUFFER *err)
 {
   char *tmp;
@@ -1195,8 +1480,7 @@ mutt_pattern_exec (struct pattern_t *pat, pattern_exec_flag flags, CONTEXT *ctx,
     case MUTT_DELETED:
       return (pat->not ^ h->deleted);
     case MUTT_MESSAGE:
-      return (pat->not ^ (h->msgno >= pat->min - 1 && (pat->max == MUTT_MAXRANGE ||
-                                                  h->msgno <= pat->max - 1)));
+      return (pat->not ^ ((HMSG(h) >= pat->min) && (HMSG(h) <= pat->max)));
     case MUTT_DATE:
       return (pat->not ^ (h->date_sent >= pat->min && h->date_sent <= pat->max));
     case MUTT_DATE_RECEIVED: