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); }