Hi,
The ability to jump to a tag in less is such a powerful feature that
deserves a better interface. Especially in combination with mandoc which
emits tags. Here's a diff that adds tab completion for tags, similar to
the existing completion for file names.

Some notes about the diff:

- As I read the POSIX specification for ctags[1], the pattern part in
  the output file must be enclosed in either /<pattern>/ or ?<pattern>?.
  The current implementation accepts any delimiter and this behavior is
  kept as is but with the added requirement of a trailing delimiter to be
  present.

- The diff is quite large but could be split up into chunks, for
  instance: extracting the parsetagent function as a first step.

Comments and feedback are much appreciated.

[1] http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ctags.html

Index: cmd.h
===================================================================
RCS file: /cvs/src/usr.bin/less/cmd.h,v
retrieving revision 1.10
diff -u -p -r1.10 cmd.h
--- cmd.h       6 Nov 2015 15:58:01 -0000       1.10
+++ cmd.h       3 May 2017 14:24:37 -0000
@@ -79,6 +79,9 @@
 
 #define        A_EXTRA                 0200
 
+/* Command completion modes */
+#define        COMPL_FILE      1
+#define        COMPL_TAG       2
 
 /* Line editing characters */
 
Index: cmdbuf.c
===================================================================
RCS file: /cvs/src/usr.bin/less/cmdbuf.c,v
retrieving revision 1.16
diff -u -p -r1.16 cmdbuf.c
--- cmdbuf.c    17 Sep 2016 15:06:41 -0000      1.16
+++ cmdbuf.c    3 May 2017 14:24:38 -0000
@@ -80,6 +80,11 @@ struct mlist mlist_shell =
 void * const ml_shell = (void *) &mlist_shell;
 
 /*
+ * Current mca completion mode.
+ */
+static int curr_mca_comp;
+
+/*
  * History for the current command.
  */
 static struct mlist *curr_mlist = NULL;
@@ -729,6 +734,7 @@ static int
 cmd_edit(int c)
 {
        int action;
+       int comp;
        int flags;
 
 #define        not_in_completion()     in_completion = 0
@@ -748,6 +754,15 @@ cmd_edit(int c)
                 */
                flags |= EC_NOCOMPLETE;
 
+       /*
+        * Disable in_completion on completion mode change.
+        * This will ensure that any previous completion becomes unavailable.
+        */
+       comp = mca_comp();
+       if (curr_mca_comp != comp)
+               not_in_completion();
+       curr_mca_comp = comp;
+
        action = editchar(c, flags);
 
        switch (action) {
@@ -908,6 +923,19 @@ delimit_word(void)
        return (word);
 }
 
+static char *
+complete(char *s)
+{
+       switch (curr_mca_comp) {
+       case COMPL_FILE:
+               return (fcomplete(s));
+       case COMPL_TAG:
+               return (tcomplete(s));
+       default:
+               return (NULL);
+       }
+}
+
 /*
  * Set things up to enter completion mode.
  * Expand the word under the cursor into a list of filenames
@@ -946,13 +974,13 @@ init_compl(void)
        c = *cp;
        *cp = '\0';
        if (*word != openquote) {
-               tk_text = fcomplete(word);
+               tk_text = complete(word);
        } else {
                char *qword = shell_quote(word+1);
                if (qword == NULL)
-                       tk_text = fcomplete(word+1);
+                       tk_text = complete(word+1);
                else
-                       tk_text = fcomplete(qword);
+                       tk_text = complete(qword);
                free(qword);
        }
        *cp = c;
Index: command.c
===================================================================
RCS file: /cvs/src/usr.bin/less/command.c,v
retrieving revision 1.31
diff -u -p -r1.31 command.c
--- command.c   12 Jan 2017 20:32:01 -0000      1.31
+++ command.c   3 May 2017 14:24:39 -0000
@@ -104,6 +104,18 @@ in_mca(void)
 }
 
 /*
+ * Returns the completion mode for the current mca.
+ */
+int
+mca_comp(void)
+{
+       if (curropt != NULL && curropt->oletter == 't')
+               return (COMPL_TAG);
+
+       return (COMPL_FILE);
+}
+
+/*
  * Set up the display to start a new search command.
  */
 static void
Index: funcs.h
===================================================================
RCS file: /cvs/src/usr.bin/less/funcs.h,v
retrieving revision 1.18
diff -u -p -r1.18 funcs.h
--- funcs.h     19 Jan 2016 06:14:54 -0000      1.18
+++ funcs.h     3 May 2017 14:24:39 -0000
@@ -83,6 +83,7 @@ char *cmd_lastpattern(void);
 void init_cmdhist(void);
 void save_cmdhist(void);
 int in_mca(void);
+int mca_comp(void);
 void dispversion(void);
 int getcc(void);
 void ungetcc(int);
@@ -275,6 +276,7 @@ void psignals(void);
 void cleantags(void);
 void findtag(char *);
 off_t tagsearch(void);
+char *tcomplete(const char *);
 char *nexttag(int);
 char *prevtag(int);
 int ntags(void);
Index: tags.c
===================================================================
RCS file: /cvs/src/usr.bin/less/tags.c,v
retrieving revision 1.19
diff -u -p -r1.19 tags.c
--- tags.c      3 May 2017 11:59:25 -0000       1.19
+++ tags.c      3 May 2017 14:24:39 -0000
@@ -29,11 +29,6 @@ enum tag_result {
        TAG_INTR
 };
 
-static enum tag_result findctag(char *);
-static char *nextctag(void);
-static char *prevctag(void);
-static off_t ctagsearch(void);
-
 /*
  * The list of tags generated by the last findctag() call.
  */
@@ -45,6 +40,7 @@ struct taglist {
 static struct taglist taglist = { TAG_END, TAG_END };
 struct tag {
        struct tag *next, *prev; /* List links */
+       char *tag_ident;        /* Tag identifier */
        char *tag_file;         /* Source file containing the tag */
        off_t tag_linenum;      /* Appropriate line number in source file */
        char *tag_pattern;      /* Pattern used to find the tag */
@@ -52,6 +48,13 @@ struct tag {
 };
 static struct tag *curtag;
 
+static enum tag_result findctag(char *);
+static char *nextctag(void);
+static char *prevctag(void);
+static off_t ctagsearch(void);
+static char *nexttoken(char *);
+static int   parsetagent(char *, struct tag *);
+
 #define        TAG_INS(tp) \
        (tp)->next = TAG_END; \
        (tp)->prev = taglist.tl_last; \
@@ -77,6 +80,7 @@ cleantags(void)
         */
        while ((tp = taglist.tl_first) != TAG_END) {
                TAG_RM(tp);
+               free(tp->tag_ident);
                free(tp->tag_file);
                free(tp->tag_pattern);
                free(tp);
@@ -86,22 +90,24 @@ cleantags(void)
 }
 
 /*
- * Create a new tag entry.
+ * Copy an existing tag entry.
  */
 static struct tag *
-maketagent(char *file, off_t linenum, char *pattern, int endline)
+copytagent(const struct tag *src)
 {
-       struct tag *tp;
+       struct tag *dst;
 
-       tp = ecalloc(sizeof (struct tag), 1);
-       tp->tag_file = estrdup(file);
-       tp->tag_linenum = linenum;
-       tp->tag_endline = endline;
-       if (pattern == NULL)
-               tp->tag_pattern = NULL;
+       dst = ecalloc(sizeof (struct tag), 1);
+       dst->tag_ident = estrdup(src->tag_ident);
+       dst->tag_file = estrdup(src->tag_file);
+       dst->tag_linenum = src->tag_linenum;
+       dst->tag_endline = src->tag_endline;
+       if (src->tag_pattern != NULL)
+               dst->tag_pattern = estrdup(src->tag_pattern);
        else
-               tp->tag_pattern = estrdup(pattern);
-       return (tp);
+               dst->tag_pattern = NULL;
+
+       return (dst);
 }
 
 /*
@@ -143,6 +149,67 @@ tagsearch(void)
 }
 
 /*
+ * Returns all tag entries present in the current tags file that start with
+ * prefix.
+ * As opposed of the findtag function which only finds exact matches.
+ * The returned string includes the identifier of all found tag entries,
+ * separated by a single space in order to make it complaint with the fcomplete
+ * function which dictates how completion is implemented.
+ */
+char *
+tcomplete(const char *prefix)
+{
+       char line[TAGLINE_SIZE];
+       struct tag tag;
+       FILE *fh;
+       char *matches = NULL;
+       char *path = NULL;
+       size_t ilen, plen;
+       size_t len = 0;
+       size_t size = 0;
+       int nmatches = 0;
+
+       path = shell_unquote(tags);
+       fh = fopen(path, "r");
+       if (fh == NULL)
+               goto ret;       /* TAG_NOFILE */
+
+       plen = strlen(prefix);
+       while (fgets(line, sizeof (line), fh) != NULL) {
+               if (*line == '!')
+                       continue;       /* skip header */
+               if (strncmp(line, prefix, plen) != 0)
+                       continue;
+
+               if (parsetagent(line, &tag) == 0)
+                       continue;       /* invalid tag entry */
+               ilen = strlen(tag.tag_ident);
+               if (matches == NULL)
+                       matches = malloc(ilen + 1);
+               else
+                       matches = reallocarray(matches, 1, size + ilen + 1);
+               if (matches == NULL)
+                       goto ret;       /* ENOMEM */
+               size += ilen + 1;
+
+               if (nmatches > 0)
+                       matches[len++] = ' ';
+               memcpy(&matches[len], tag.tag_ident, ilen);
+               len += ilen;
+               nmatches++;
+       }
+       if (nmatches > 0)
+               matches[len] = '\0';
+
+ret:
+       if (fh != NULL)
+               fclose(fh);
+       free(path);
+
+       return (matches);
+}
+
+/*
  * Go to the next tag.
  */
 char *
@@ -193,17 +260,12 @@ curr_tag(void)
 static enum tag_result
 findctag(char *tag)
 {
-       char *p;
-       FILE *f;
-       int taglen;
-       off_t taglinenum;
-       char *tagfile;
-       char *tagpattern;
-       int tagendline;
-       int search_char;
-       int err;
        char tline[TAGLINE_SIZE];
+       struct tag tmp;
+       FILE *f;
+       char *p;
        struct tag *tp;
+       int taglen;
 
        p = shell_unquote(tags);
        f = fopen(p, "r");
@@ -225,65 +287,11 @@ findctag(char *tag)
                if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen]))
                        continue;
 
-               /*
-                * Found it.
-                * The line contains the tag, the filename and the
-                * location in the file, separated by white space.
-                * The location is either a decimal line number,
-                * or a search pattern surrounded by a pair of delimiters.
-                * Parse the line and extract these parts.
-                */
-               tagpattern = NULL;
-
-               /*
-                * Skip over the whitespace after the tag name.
-                */
-               p = skipsp(tline+taglen);
-               if (*p == '\0')
-                       /* File name is missing! */
-                       continue;
-
-               /*
-                * Save the file name.
-                * Skip over the whitespace after the file name.
-                */
-               tagfile = p;
-               while (!WHITESP(*p) && *p != '\0')
-                       p++;
-               *p++ = '\0';
-               p = skipsp(p);
-               if (*p == '\0')
-                       /* Pattern is missing! */
+               if (parsetagent(tline, &tmp) == 0)
+                       /* Invalid tag entry. */
                        continue;
 
-               /*
-                * First see if it is a line number.
-                */
-               tagendline = 0;
-               taglinenum = getnum(&p, 0, &err);
-               if (err) {
-                       /*
-                        * No, it must be a pattern.
-                        * Delete the initial "^" (if present) and
-                        * the final "$" from the pattern.
-                        * Delete any backslash in the pattern.
-                        */
-                       taglinenum = 0;
-                       search_char = *p++;
-                       if (*p == '^')
-                               p++;
-                       tagpattern = p;
-                       while (*p != search_char && *p != '\0') {
-                               if (*p == '\\')
-                                       p++;
-                               p++;
-                       }
-                       tagendline = (p[-1] == '$');
-                       if (tagendline)
-                               p--;
-                       *p = '\0';
-               }
-               tp = maketagent(tagfile, taglinenum, tagpattern, tagendline);
+               tp = copytagent(&tmp);
                TAG_INS(tp);
                total++;
        }
@@ -434,4 +442,84 @@ prevctag(void)
                curseq--;
        }
        return (curtag->tag_file);
+}
+
+/*
+ * Parse a line from a tags file conforming to the following grammar:
+ *
+ *   <identifier> [ \t]+ <filename> [ \t]+ <pattern>
+ */
+static int
+parsetagent(char *s, struct tag *tag)
+{
+       int err, delim, lnum;
+
+       /* Parse identifier. */
+       tag->tag_ident = s;
+
+       /* Parse file name. */
+       if ((s = nexttoken(s)) == NULL)
+               /* File name missing. */
+               return (0);
+       tag->tag_file = s;
+
+       /* Parse line number or pattern. */
+       if ((s = nexttoken(s)) == NULL)
+               /* Line number or pattern missing. */
+               return (0);
+       lnum = getnum(&s, 0, &err);
+       if (err == 0) {
+               /* Line number found. */
+               tag->tag_linenum = lnum;
+               tag->tag_pattern = NULL;
+               tag->tag_endline = 0;
+       } else {
+               /*
+                * Pattern found.
+                * The pattern must be wrapped by any delimiter.
+                * The same delimiter can be present in the pattern if it's
+                * escaped with a blackslash.
+                * Any anchor present in the pattern will be removed.
+                */
+               delim = *s++;
+               if (*s == '^')
+                       s++;
+               tag->tag_pattern = s;
+               for (; *s != delim; s++)
+                       if (*s == '\0')
+                               /* Trailing delimiter missing. */
+                               return (0);
+                       else if (*s == '\\')
+                               s++;
+
+               tag->tag_linenum = 0;
+               tag->tag_endline = 0;
+               if (s[-1] == '$') {
+                       tag->tag_endline = 1;
+                       s--;
+               }
+               *s = '\0';
+       }
+
+       return (1);
+}
+
+static char *
+nexttoken(char *s)
+{
+       for (; *s != '\0'; s++)
+               if (WHITESP(*s))
+                       break;
+       if (*s == '\0')
+               return (NULL);
+       *s++ = '\0';
+
+       for (; *s != '\0'; s++)
+               if (!WHITESP(*s))
+                       break;
+
+       if (*s == '\0')
+               return (NULL);
+
+       return (s);
 }

Reply via email to