tig(1): Do not differentiate between git (show|log|diff) options
[tig] / tig.c
diff --git a/tig.c b/tig.c
index 1ad1994..f9a9a1a 100644 (file)
--- a/tig.c
+++ b/tig.c
@@ -30,6 +30,9 @@
 #include <unistd.h>
 #include <time.h>
 
+#include <sys/types.h>
+#include <regex.h>
+
 #include <locale.h>
 #include <langinfo.h>
 #include <iconv.h>
@@ -54,8 +57,8 @@ static size_t utf8_length(const char *string, size_t max_width, int *coloffset,
 #define ARRAY_SIZE(x)  (sizeof(x) / sizeof(x[0]))
 #define STRING_SIZE(x) (sizeof(x) - 1)
 
+#define SIZEOF_STR     1024    /* Default string size. */
 #define SIZEOF_REF     256     /* Size of symbolic or SHA1 ID. */
-#define SIZEOF_CMD     1024    /* Size of command buffer. */
 #define SIZEOF_REVGRAPH        19      /* Size of revision ancestry graphics. */
 
 /* This color name can be used to refer to the default term colors. */
@@ -224,11 +227,11 @@ string_enum_compare(const char *str1, const char *str2, int len)
  */
 
 static size_t
-sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
+sq_quote(char buf[SIZEOF_STR], size_t bufsize, const char *src)
 {
        char c;
 
-#define BUFPUT(x) do { if (bufsize < SIZEOF_CMD) buf[bufsize++] = (x); } while (0)
+#define BUFPUT(x) do { if (bufsize < SIZEOF_STR) buf[bufsize++] = (x); } while (0)
 
        BUFPUT('\'');
        while ((c = *src++)) {
@@ -282,9 +285,15 @@ sq_quote(char buf[SIZEOF_CMD], size_t bufsize, const char *src)
        REQ_(SCROLL_PAGE_UP,    "Scroll one page up"), \
        REQ_(SCROLL_PAGE_DOWN,  "Scroll one page down"), \
        \
+       REQ_GROUP("Searching") \
+       REQ_(SEARCH,            "Search the view"), \
+       REQ_(SEARCH_BACK,       "Search backwards in the view"), \
+       REQ_(FIND_NEXT,         "Find next search match"), \
+       REQ_(FIND_PREV,         "Find previous search match"), \
+       \
        REQ_GROUP("Misc") \
+       REQ_(NONE,              "Do nothing"), \
        REQ_(PROMPT,            "Bring up the prompt"), \
-       REQ_(SCREEN_UPDATE,     "Update the screen"), \
        REQ_(SCREEN_REDRAW,     "Redraw the screen"), \
        REQ_(SCREEN_RESIZE,     "Resize the screen"), \
        REQ_(SHOW_VERSION,      "Show version information"), \
@@ -361,17 +370,18 @@ VERSION " (" __DATE__ ")\n"
 "  -h, --help                  Show help message and exit\n";
 
 /* Option and state variables. */
-static bool opt_line_number    = FALSE;
-static bool opt_rev_graph      = TRUE;
-static int opt_num_interval    = NUMBER_INTERVAL;
-static int opt_tab_size                = TABSIZE;
-static enum request opt_request = REQ_VIEW_MAIN;
-static char opt_cmd[SIZEOF_CMD]        = "";
-static FILE *opt_pipe          = NULL;
-static char opt_encoding[20]   = "UTF-8";
-static bool opt_utf8           = TRUE;
-static char opt_codeset[20]    = "UTF-8";
-static iconv_t opt_iconv       = ICONV_NONE;
+static bool opt_line_number            = FALSE;
+static bool opt_rev_graph              = TRUE;
+static int opt_num_interval            = NUMBER_INTERVAL;
+static int opt_tab_size                        = TABSIZE;
+static enum request opt_request                = REQ_VIEW_MAIN;
+static char opt_cmd[SIZEOF_STR]                = "";
+static FILE *opt_pipe                  = NULL;
+static char opt_encoding[20]           = "UTF-8";
+static bool opt_utf8                   = TRUE;
+static char opt_codeset[20]            = "UTF-8";
+static iconv_t opt_iconv               = ICONV_NONE;
+static char opt_search[SIZEOF_STR]     = "";
 
 enum option_type {
        OPT_NONE,
@@ -655,7 +665,6 @@ static struct keybinding default_keybindings[] = {
        { 'l',          REQ_VIEW_LOG },
        { 'p',          REQ_VIEW_PAGER },
        { 'h',          REQ_VIEW_HELP },
-       { '?',          REQ_VIEW_HELP },
 
        /* View manipulation */
        { 'q',          REQ_VIEW_CLOSE },
@@ -681,6 +690,12 @@ static struct keybinding default_keybindings[] = {
        { 'w',          REQ_SCROLL_PAGE_UP },
        { 's',          REQ_SCROLL_PAGE_DOWN },
 
+       /* Searching */
+       { '/',          REQ_SEARCH },
+       { '?',          REQ_SEARCH_BACK },
+       { 'n',          REQ_FIND_NEXT },
+       { 'N',          REQ_FIND_PREV },
+
        /* Misc */
        { 'Q',          REQ_QUIT },
        { 'z',          REQ_STOP_LOADING },
@@ -691,9 +706,9 @@ static struct keybinding default_keybindings[] = {
        { ':',          REQ_PROMPT },
 
        /* wgetch() with nodelay() enabled returns ERR when there's no input. */
-       { ERR,          REQ_SCREEN_UPDATE },
+       { ERR,          REQ_NONE },
 
-       /* Use the ncurses SIGWINCH handler. */
+       /* Using the ncurses SIGWINCH handler. */
        { KEY_RESIZE,   REQ_SCREEN_RESIZE },
 };
 
@@ -1089,7 +1104,7 @@ static int
 load_options(void)
 {
        char *home = getenv("HOME");
-       char buf[1024];
+       char buf[SIZEOF_STR];
        FILE *file;
 
        config_lineno = 0;
@@ -1141,7 +1156,7 @@ struct view {
 
        enum keymap keymap;     /* What keymap does this view have */
 
-       char cmd[SIZEOF_CMD];   /* Command buffer */
+       char cmd[SIZEOF_STR];   /* Command buffer */
        char ref[SIZEOF_REF];   /* Hovered commit reference */
        char vid[SIZEOF_REF];   /* View ID. Set to id member when updating. */
 
@@ -1153,6 +1168,10 @@ struct view {
        unsigned long offset;   /* Offset of the window top */
        unsigned long lineno;   /* Current line number */
 
+       /* Searching */
+       char grep[SIZEOF_STR];  /* Search string */
+       regex_t regex;          /* Pre-compiled regex */
+
        /* If non-NULL, points to the view that opened this view. If this view
         * is closed tig will switch back to the parent view. */
        struct view *parent;
@@ -1177,6 +1196,8 @@ struct view_ops {
        bool (*read)(struct view *view, char *data);
        /* Depending on view, change display based on current line. */
        bool (*enter)(struct view *view, struct line *line);
+       /* Search for regex in a line. */
+       bool (*grep)(struct view *view, struct line *line);
 };
 
 static struct view_ops pager_ops;
@@ -1537,6 +1558,109 @@ move_view(struct view *view, enum request request, bool redraw)
 
 
 /*
+ * Searching
+ */
+
+static void search_view(struct view *view, enum request request, const char *search);
+
+static bool
+find_next_line(struct view *view, unsigned long lineno, struct line *line)
+{
+       if (!view->ops->grep(view, line))
+               return FALSE;
+
+       if (lineno - view->offset >= view->height) {
+               view->offset = lineno;
+               view->lineno = lineno;
+               redraw_view(view);
+
+       } else {
+               unsigned long old_lineno = view->lineno - view->offset;
+
+               view->lineno = lineno;
+
+               wmove(view->win, old_lineno, 0);
+               wclrtoeol(view->win);
+               draw_view_line(view, old_lineno);
+
+               draw_view_line(view, view->lineno - view->offset);
+               redrawwin(view->win);
+               wrefresh(view->win);
+       }
+
+       report("Line %ld matches '%s'", lineno + 1, view->grep);
+       return TRUE;
+}
+
+static void
+find_next(struct view *view, enum request request)
+{
+       unsigned long lineno = view->lineno;
+       int direction;
+
+       if (!*view->grep) {
+               if (!*opt_search)
+                       report("No previous search");
+               else
+                       search_view(view, request, opt_search);
+               return;
+       }
+
+       switch (request) {
+       case REQ_SEARCH:
+       case REQ_FIND_NEXT:
+               direction = 1;
+               break;
+
+       case REQ_SEARCH_BACK:
+       case REQ_FIND_PREV:
+               direction = -1;
+               break;
+
+       default:
+               return;
+       }
+
+       if (request == REQ_FIND_NEXT || request == REQ_FIND_PREV)
+               lineno += direction;
+
+       /* Note, lineno is unsigned long so will wrap around in which case it
+        * will become bigger than view->lines. */
+       for (; lineno < view->lines; lineno += direction) {
+               struct line *line = &view->line[lineno];
+
+               if (find_next_line(view, lineno, line))
+                       return;
+       }
+
+       report("No match found for '%s'", view->grep);
+}
+
+static void
+search_view(struct view *view, enum request request, const char *search)
+{
+       int regex_err;
+
+       if (*view->grep) {
+               regfree(&view->regex);
+               *view->grep = 0;
+       }
+
+       regex_err = regcomp(&view->regex, search, REG_EXTENDED);
+       if (regex_err != 0) {
+               char buf[SIZEOF_STR] = "unknown error";
+
+               regerror(regex_err, &view->regex, buf, sizeof(buf));
+               report("Search failed: %s", buf);;
+               return;
+       }
+
+       string_copy(view->grep, search);
+
+       find_next(view, request);
+}
+
+/*
  * Incremental updating
  */
 
@@ -1936,6 +2060,16 @@ view_driver(struct view *view, enum request request)
                open_view(view, opt_request, OPEN_RELOAD);
                break;
 
+       case REQ_SEARCH:
+       case REQ_SEARCH_BACK:
+               search_view(view, request, opt_search);
+               break;
+
+       case REQ_FIND_NEXT:
+       case REQ_FIND_PREV:
+               find_next(view, request);
+               break;
+
        case REQ_STOP_LOADING:
                for (i = 0; i < ARRAY_SIZE(views); i++) {
                        view = &views[i];
@@ -1956,7 +2090,7 @@ view_driver(struct view *view, enum request request)
                redraw_display();
                break;
 
-       case REQ_SCREEN_UPDATE:
+       case REQ_NONE:
                doupdate();
                return TRUE;
 
@@ -2067,20 +2201,52 @@ pager_draw(struct view *view, struct line *line, unsigned int lineno)
        return TRUE;
 }
 
+static bool
+add_describe_ref(char *buf, int *bufpos, char *commit_id, const char *sep)
+{
+       char refbuf[SIZEOF_STR];
+       char *ref = NULL;
+       FILE *pipe;
+
+       if (!string_format(refbuf, "git describe %s", commit_id))
+               return TRUE;
+
+       pipe = popen(refbuf, "r");
+       if (!pipe)
+               return TRUE;
+
+       if ((ref = fgets(refbuf, sizeof(refbuf), pipe)))
+               ref = chomp_string(ref);
+       pclose(pipe);
+
+       if (!ref || !*ref)
+               return TRUE;
+
+       /* This is the only fatal call, since it can "corrupt" the buffer. */
+       if (!string_nformat(buf, SIZEOF_STR, bufpos, "%s%s", sep, ref))
+               return FALSE;
+
+       return TRUE;
+}
+
 static void
 add_pager_refs(struct view *view, struct line *line)
 {
-       char buf[1024];
-       char *data = line->data;
+       char buf[SIZEOF_STR];
+       char *commit_id = line->data + STRING_SIZE("commit ");
        struct ref **refs;
        int bufpos = 0, refpos = 0;
        const char *sep = "Refs: ";
+       bool is_tag = FALSE;
 
        assert(line->type == LINE_COMMIT);
 
-       refs = get_refs(data + STRING_SIZE("commit "));
-       if (!refs)
+       refs = get_refs(commit_id);
+       if (!refs) {
+               if (view == VIEW(REQ_VIEW_DIFF))
+                       goto try_add_describe_ref;
                return;
+       }
 
        do {
                struct ref *ref = refs[refpos];
@@ -2089,8 +2255,16 @@ add_pager_refs(struct view *view, struct line *line)
                if (!string_format_from(buf, &bufpos, fmt, sep, ref->name))
                        return;
                sep = ", ";
+               if (ref->tag)
+                       is_tag = TRUE;
        } while (refs[refpos++]->next);
 
+       if (!is_tag && view == VIEW(REQ_VIEW_DIFF)) {
+try_add_describe_ref:
+               if (!add_describe_ref(buf, &bufpos, commit_id, sep))
+                       return;
+       }
+
        if (!realloc_lines(view, view->line_size + 1))
                return;
 
@@ -2149,11 +2323,27 @@ pager_enter(struct view *view, struct line *line)
        return TRUE;
 }
 
+static bool
+pager_grep(struct view *view, struct line *line)
+{
+       regmatch_t pmatch;
+       char *text = line->data;
+
+       if (!*text)
+               return FALSE;
+
+       if (regexec(&view->regex, text, 1, &pmatch, 0) == REG_NOMATCH)
+               return FALSE;
+
+       return TRUE;
+}
+
 static struct view_ops pager_ops = {
        "line",
        pager_draw,
        pager_read,
        pager_enter,
+       pager_grep,
 };
 
 
@@ -2390,11 +2580,43 @@ main_enter(struct view *view, struct line *line)
        return TRUE;
 }
 
+static bool
+main_grep(struct view *view, struct line *line)
+{
+       struct commit *commit = line->data;
+       enum { S_TITLE, S_AUTHOR, S_DATE, S_END } state;
+       char buf[DATE_COLS + 1];
+       regmatch_t pmatch;
+
+       for (state = S_TITLE; state < S_END; state++) {
+               char *text;
+
+               switch (state) {
+               case S_TITLE:   text = commit->title;   break;
+               case S_AUTHOR:  text = commit->author;  break;
+               case S_DATE:
+                       if (!strftime(buf, sizeof(buf), DATE_FORMAT, &commit->time))
+                               continue;
+                       text = buf;
+                       break;
+
+               default:
+                       return FALSE;
+               }
+
+               if (regexec(&view->regex, text, 1, &pmatch, 0) != REG_NOMATCH)
+                       return TRUE;
+       }
+
+       return FALSE;
+}
+
 static struct view_ops main_ops = {
        "commit",
        main_draw,
        main_read,
        main_enter,
+       main_grep,
 };
 
 
@@ -2646,11 +2868,11 @@ init_display(void)
        wbkgdset(status_win, get_line_attr(LINE_STATUS));
 }
 
-static int
-read_prompt(void)
+static char *
+read_prompt(const char *prompt)
 {
        enum { READING, STOP, CANCEL } status = READING;
-       char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
+       static char buf[sizeof(opt_cmd) - STRING_SIZE("git \0")];
        int pos = 0;
 
        while (status == READING) {
@@ -2660,7 +2882,7 @@ read_prompt(void)
                foreach_view (view, i)
                        update_view(view);
 
-               report(":%.*s", pos, buf);
+               report("%s%.*s", prompt, pos, buf);
                /* Refresh, accept single keystroke of input */
                key = wgetch(status_win);
                switch (key) {
@@ -2687,7 +2909,7 @@ read_prompt(void)
                default:
                        if (pos >= sizeof(buf)) {
                                report("Input string too long");
-                               return ERR;
+                               return NULL;
                        }
 
                        if (isprint(key))
@@ -2698,18 +2920,12 @@ read_prompt(void)
        if (status == CANCEL) {
                /* Clear the status window */
                report("");
-               return ERR;
+               return NULL;
        }
 
        buf[pos++] = 0;
-       if (!string_format(opt_cmd, "git %s", buf))
-               return ERR;
-       if (strncmp(buf, "show", 4) && isspace(buf[4]))
-               opt_request = REQ_VIEW_DIFF;
-       else
-               opt_request = REQ_VIEW_PAGER;
 
-       return OK;
+       return buf;
 }
 
 /*
@@ -2970,10 +3186,34 @@ main(int argc, char *argv[])
                 * status_win restricted. */
                switch (request) {
                case REQ_PROMPT:
-                       if (read_prompt() == ERR)
-                               request = REQ_SCREEN_UPDATE;
+               {
+                       char *cmd = read_prompt(":");
+
+                       if (cmd && string_format(opt_cmd, "git %s", cmd)) {
+                               if (strncmp(cmd, "show", 4) && isspace(cmd[4])) {
+                                       opt_request = REQ_VIEW_DIFF;
+                               } else {
+                                       opt_request = REQ_VIEW_PAGER;
+                               }
+                               break;
+                       }
+
+                       request = REQ_NONE;
                        break;
+               }
+               case REQ_SEARCH:
+               case REQ_SEARCH_BACK:
+               {
+                       const char *prompt = request == REQ_SEARCH
+                                          ? "/" : "?";
+                       char *search = read_prompt(prompt);
 
+                       if (search)
+                               string_copy(opt_search, search);
+                       else
+                               request = REQ_NONE;
+                       break;
+               }
                case REQ_SCREEN_RESIZE:
                {
                        int height, width;