xref: /dragonfly/contrib/less/tags.c (revision e0f238eda64c20d98364903e0c003825fd0b66dd)
1 /*
2  * Copyright (C) 1984-2024  Mark Nudelman
3  *
4  * You may distribute under the terms of either the GNU General Public
5  * License or the Less License, as specified in the README file.
6  *
7  * For more information, see the README file.
8  */
9 
10 
11 #include "less.h"
12 
13 #define WHITESP(c)      ((c)==' ' || (c)=='\t')
14 
15 #if TAGS
16 
17 public constant char ztags[] = "tags";
18 public constant char *tags = ztags;
19 
20 static int total;
21 static int curseq;
22 
23 extern int linenums;
24 extern int sigs;
25 extern int ctldisp;
26 
27 enum tag_result {
28           TAG_FOUND,
29           TAG_NOFILE,
30           TAG_NOTAG,
31           TAG_NOTYPE,
32           TAG_INTR
33 };
34 
35 /*
36  * Tag type
37  */
38 enum {
39           T_CTAGS,        /* 'tags': standard and extended format (ctags) */
40           T_CTAGS_X,      /* stdin: cross reference format (ctags) */
41           T_GTAGS,        /* 'GTAGS': function definition (global) */
42           T_GRTAGS,       /* 'GRTAGS': function reference (global) */
43           T_GSYMS,        /* 'GSYMS': other symbols (global) */
44           T_GPATH         /* 'GPATH': path name (global) */
45 };
46 
47 static enum tag_result findctag(constant char *tag);
48 static enum tag_result findgtag(constant char *tag, int type);
49 static constant char *nextgtag(void);
50 static constant char *prevgtag(void);
51 static POSITION ctagsearch(void);
52 static POSITION gtagsearch(void);
53 static int getentry(char *buf, constant char **tag, constant char **file, constant char **line);
54 
55 /*
56  * The list of tags generated by the last findgtag() call.
57  *
58  * Use either pattern or line number.
59  * findgtag() always uses line number, so pattern is always NULL.
60  * findctag() uses either pattern (in which case line number is 0),
61  * or line number (in which case pattern is NULL).
62  */
63 struct taglist {
64           struct tag *tl_first;
65           struct tag *tl_last;
66 };
67 struct tag {
68           struct tag *next, *prev; /* List links */
69           char *tag_file;         /* Source file containing the tag */
70           LINENUM tag_linenum;    /* Appropriate line number in source file */
71           char *tag_pattern;      /* Pattern used to find the tag */
72           lbool tag_endline;      /* True if the pattern includes '$' */
73 };
74 #define TAG_END  ((struct tag *) &taglist)
75 static struct taglist taglist = { TAG_END, TAG_END };
76 static struct tag *curtag;
77 
78 #define TAG_INS(tp) \
79           (tp)->next = TAG_END; \
80           (tp)->prev = taglist.tl_last; \
81           taglist.tl_last->next = (tp); \
82           taglist.tl_last = (tp);
83 
84 #define TAG_RM(tp) \
85           (tp)->next->prev = (tp)->prev; \
86           (tp)->prev->next = (tp)->next;
87 
88 /*
89  * Delete tag structures.
90  */
cleantags(void)91 public void cleantags(void)
92 {
93           struct tag *tp;
94 
95           /*
96            * Delete any existing tag list.
97            * {{ Ideally, we wouldn't do this until after we know that we
98            *    can load some other tag information. }}
99            */
100           while ((tp = taglist.tl_first) != TAG_END)
101           {
102                     TAG_RM(tp);
103                     free(tp->tag_file);
104                     free(tp->tag_pattern);
105                     free(tp);
106           }
107           curtag = NULL;
108           total = curseq = 0;
109 }
110 
111 /*
112  * Create a new tag entry.
113  */
maketagent(constant char * file,LINENUM linenum,constant char * pattern,lbool endline)114 static struct tag * maketagent(constant char *file, LINENUM linenum, constant char *pattern, lbool endline)
115 {
116           struct tag *tp;
117 
118           tp = (struct tag *) ecalloc(sizeof(struct tag), 1);
119           tp->tag_file = (char *) ecalloc(strlen(file) + 1, sizeof(char));
120           strcpy(tp->tag_file, file);
121           tp->tag_linenum = linenum;
122           tp->tag_endline = endline;
123           if (pattern == NULL)
124                     tp->tag_pattern = NULL;
125           else
126           {
127                     tp->tag_pattern = (char *) ecalloc(strlen(pattern) + 1, sizeof(char));
128                     strcpy(tp->tag_pattern, pattern);
129           }
130           return (tp);
131 }
132 
133 /*
134  * Get tag mode.
135  */
gettagtype(void)136 public int gettagtype(void)
137 {
138           int f;
139 
140           if (strcmp(tags, "GTAGS") == 0)
141                     return T_GTAGS;
142           if (strcmp(tags, "GRTAGS") == 0)
143                     return T_GRTAGS;
144           if (strcmp(tags, "GSYMS") == 0)
145                     return T_GSYMS;
146           if (strcmp(tags, "GPATH") == 0)
147                     return T_GPATH;
148           if (strcmp(tags, "-") == 0)
149                     return T_CTAGS_X;
150           f = open(tags, OPEN_READ);
151           if (f >= 0)
152           {
153                     close(f);
154                     return T_CTAGS;
155           }
156           return T_GTAGS;
157 }
158 
159 /*
160  * Find tags in tag file.
161  * Find a tag in the "tags" file.
162  * Sets "tag_file" to the name of the file containing the tag,
163  * and "tagpattern" to the search pattern which should be used
164  * to find the tag.
165  */
findtag(constant char * tag)166 public void findtag(constant char *tag)
167 {
168           int type = gettagtype();
169           enum tag_result result;
170 
171           if (type == T_CTAGS)
172                     result = findctag(tag);
173           else
174                     result = findgtag(tag, type);
175           switch (result)
176           {
177           case TAG_FOUND:
178           case TAG_INTR:
179                     break;
180           case TAG_NOFILE:
181                     error("No tags file", NULL_PARG);
182                     break;
183           case TAG_NOTAG:
184                     error("No such tag in tags file", NULL_PARG);
185                     break;
186           case TAG_NOTYPE:
187                     error("unknown tag type", NULL_PARG);
188                     break;
189           }
190 }
191 
192 /*
193  * Search for a tag.
194  */
tagsearch(void)195 public POSITION tagsearch(void)
196 {
197           if (curtag == NULL)
198                     return (NULL_POSITION);  /* No gtags loaded! */
199           if (curtag->tag_linenum != 0)
200                     return gtagsearch();
201           else
202                     return ctagsearch();
203 }
204 
205 /*
206  * Go to the next tag.
207  */
nexttag(int n)208 public constant char * nexttag(int n)
209 {
210           constant char *tagfile = (char *) NULL;
211 
212           while (n-- > 0)
213                     tagfile = nextgtag();
214           return tagfile;
215 }
216 
217 /*
218  * Go to the previous tag.
219  */
prevtag(int n)220 public constant char * prevtag(int n)
221 {
222           constant char *tagfile = (char *) NULL;
223 
224           while (n-- > 0)
225                     tagfile = prevgtag();
226           return tagfile;
227 }
228 
229 /*
230  * Return the total number of tags.
231  */
ntags(void)232 public int ntags(void)
233 {
234           return total;
235 }
236 
237 /*
238  * Return the sequence number of current tag.
239  */
curr_tag(void)240 public int curr_tag(void)
241 {
242           return curseq;
243 }
244 
245 /*****************************************************************************
246  * ctags
247  */
248 
249 /*
250  * Find tags in the "tags" file.
251  * Sets curtag to the first tag entry.
252  */
findctag(constant char * tag)253 static enum tag_result findctag(constant char *tag)
254 {
255           char *p;
256           char *q;
257           FILE *f;
258           size_t taglen;
259           LINENUM taglinenum;
260           char *tagfile;
261           char *tagpattern;
262           lbool tagendline;
263           int search_char;
264           lbool err;
265           char tline[TAGLINE_SIZE];
266           struct tag *tp;
267 
268           p = shell_unquote(tags);
269           f = fopen(p, "r");
270           free(p);
271           if (f == NULL)
272                     return TAG_NOFILE;
273 
274           cleantags();
275           total = 0;
276           taglen = strlen(tag);
277 
278           /*
279            * Search the tags file for the desired tag.
280            */
281           while (fgets(tline, sizeof(tline), f) != NULL)
282           {
283                     if (tline[0] == '!')
284                               /* Skip header of extended format. */
285                               continue;
286                     if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen]))
287                               continue;
288 
289                     /*
290                      * Found it.
291                      * The line contains the tag, the filename and the
292                      * location in the file, separated by white space.
293                      * The location is either a decimal line number,
294                      * or a search pattern surrounded by a pair of delimiters.
295                      * Parse the line and extract these parts.
296                      */
297                     tagpattern = NULL;
298 
299                     /*
300                      * Skip over the whitespace after the tag name.
301                      */
302                     p = skipsp(tline+taglen);
303                     if (*p == '\0')
304                               /* File name is missing! */
305                               continue;
306 
307                     /*
308                      * Save the file name.
309                      * Skip over the whitespace after the file name.
310                      */
311                     tagfile = p;
312                     while (!WHITESP(*p) && *p != '\0')
313                               p++;
314                     *p++ = '\0';
315                     p = skipsp(p);
316                     if (*p == '\0')
317                               /* Pattern is missing! */
318                               continue;
319 
320                     /*
321                      * First see if it is a line number.
322                      */
323                     tagendline = FALSE;
324                     taglinenum = getnum(&p, 0, &err);
325                     if (err)
326                     {
327                               /*
328                                * No, it must be a pattern.
329                                * Delete the initial "^" (if present) and
330                                * the final "$" from the pattern.
331                                * Delete any backslash in the pattern.
332                                */
333                               taglinenum = 0;
334                               search_char = *p++;
335                               if (*p == '^')
336                                         p++;
337                               tagpattern = q = p;
338                               while (*p != search_char && *p != '\0')
339                               {
340                                         if (*p == '\\')
341                                                   p++;
342                                         if (q != p)
343                                         {
344                                                   *q++ = *p++;
345                                         } else
346                                         {
347                                                   q++;
348                                                   p++;
349                                         }
350                               }
351                               tagendline = (q[-1] == '$');
352                               if (tagendline)
353                                         q--;
354                               *q = '\0';
355                     }
356                     tp = maketagent(tagfile, taglinenum, tagpattern, tagendline);
357                     TAG_INS(tp);
358                     total++;
359           }
360           fclose(f);
361           if (total == 0)
362                     return TAG_NOTAG;
363           curtag = taglist.tl_first;
364           curseq = 1;
365           return TAG_FOUND;
366 }
367 
368 /*
369  * Edit current tagged file.
370  */
edit_tagfile(void)371 public int edit_tagfile(void)
372 {
373           if (curtag == NULL)
374                     return (1);
375           return (edit(curtag->tag_file));
376 }
377 
curtag_match(char constant * line,POSITION linepos)378 static int curtag_match(char constant *line, POSITION linepos)
379 {
380           /*
381            * Test the line to see if we have a match.
382            * Use strncmp because the pattern may be
383            * truncated (in the tags file) if it is too long.
384            * If tagendline is set, make sure we match all
385            * the way to end of line (no extra chars after the match).
386            */
387           size_t len = strlen(curtag->tag_pattern);
388           if (strncmp(curtag->tag_pattern, line, len) == 0 &&
389               (!curtag->tag_endline || line[len] == '\0' || line[len] == '\r'))
390           {
391                     curtag->tag_linenum = find_linenum(linepos);
392                     return 1;
393           }
394           return 0;
395 }
396 
397 /*
398  * Search for a tag.
399  * This is a stripped-down version of search().
400  * We don't use search() for several reasons:
401  *   -  We don't want to blow away any search string we may have saved.
402  *   -  The various regular-expression functions (from different systems:
403  *      regcmp vs. re_comp) behave differently in the presence of
404  *      parentheses (which are almost always found in a tag).
405  */
ctagsearch(void)406 static POSITION ctagsearch(void)
407 {
408           POSITION pos, linepos;
409           LINENUM linenum;
410           size_t line_len;
411           constant char *line;
412           int found;
413 
414           pos = ch_zero();
415           linenum = find_linenum(pos);
416 
417           for (found = 0; !found;)
418           {
419                     /*
420                      * Get lines until we find a matching one or
421                      * until we hit end-of-file.
422                      */
423                     if (ABORT_SIGS())
424                               return (NULL_POSITION);
425 
426                     /*
427                      * Read the next line, and save the
428                      * starting position of that line in linepos.
429                      */
430                     linepos = pos;
431                     pos = forw_raw_line(pos, &line, &line_len);
432                     if (linenum != 0)
433                               linenum++;
434 
435                     if (pos == NULL_POSITION)
436                     {
437                               /*
438                                * We hit EOF without a match.
439                                */
440                               error("Tag not found", NULL_PARG);
441                               return (NULL_POSITION);
442                     }
443 
444                     /*
445                      * If we're using line numbers, we might as well
446                      * remember the information we have now (the position
447                      * and line number of the current line).
448                      */
449                     if (linenums)
450                               add_lnum(linenum, pos);
451 
452                     if (ctldisp != OPT_ONPLUS)
453                     {
454                               if (curtag_match(line, linepos))
455                                         found = 1;
456                     } else
457                     {
458                               int cvt_ops = CVT_ANSI;
459                               size_t cvt_len = cvt_length(line_len, cvt_ops);
460                               int *chpos = cvt_alloc_chpos(cvt_len);
461                               char *cline = (char *) ecalloc(1, cvt_len);
462                               cvt_text(cline, line, chpos, &line_len, cvt_ops);
463                               if (curtag_match(cline, linepos))
464                                         found = 1;
465                               free(chpos);
466                               free(cline);
467                     }
468           }
469 
470           return (linepos);
471 }
472 
473 /*******************************************************************************
474  * gtags
475  */
476 
477 /*
478  * Find tags in the GLOBAL's tag file.
479  * The findgtag() will try and load information about the requested tag.
480  * It does this by calling "global -x tag" and storing the parsed output
481  * for future use by gtagsearch().
482  * Sets curtag to the first tag entry.
483  */
findgtag(constant char * tag,int type)484 static enum tag_result findgtag(constant char *tag, int type)
485 {
486           char buf[1024];
487           FILE *fp;
488           struct tag *tp;
489 
490           if (type != T_CTAGS_X && tag == NULL)
491                     return TAG_NOFILE;
492 
493           cleantags();
494           total = 0;
495 
496           /*
497            * If type == T_CTAGS_X then read ctags's -x format from stdin
498            * else execute global(1) and read from it.
499            */
500           if (type == T_CTAGS_X)
501           {
502                     fp = stdin;
503                     /* Set tag default because we cannot read stdin again. */
504                     tags = ztags;
505           } else
506           {
507 #if !HAVE_POPEN
508                     return TAG_NOFILE;
509 #else
510                     char *command;
511                     char *flag;
512                     char *qtag;
513                     constant char *cmd = lgetenv("LESSGLOBALTAGS");
514 
515                     if (isnullenv(cmd))
516                               return TAG_NOFILE;
517                     /* Get suitable flag value for global(1). */
518                     switch (type)
519                     {
520                     case T_GTAGS:
521                               flag = "" ;
522                               break;
523                     case T_GRTAGS:
524                               flag = "r";
525                               break;
526                     case T_GSYMS:
527                               flag = "s";
528                               break;
529                     case T_GPATH:
530                               flag = "P";
531                               break;
532                     default:
533                               return TAG_NOTYPE;
534                     }
535 
536                     /* Get our data from global(1). */
537                     qtag = shell_quote(tag);
538                     if (qtag == NULL)
539                               qtag = save(tag);
540                     command = (char *) ecalloc(strlen(cmd) + strlen(flag) +
541                                         strlen(qtag) + 5, sizeof(char));
542                     sprintf(command, "%s -x%s %s", cmd, flag, qtag);
543                     free(qtag);
544                     fp = popen(command, "r");
545                     free(command);
546 #endif
547           }
548           if (fp != NULL)
549           {
550                     while (fgets(buf, sizeof(buf), fp))
551                     {
552                               constant char *name;
553                               constant char *file;
554                               constant char *line;
555                               size_t len;
556 
557                               if (sigs)
558                               {
559 #if HAVE_POPEN
560                                         if (fp != stdin)
561                                                   pclose(fp);
562 #endif
563                                         return TAG_INTR;
564                               }
565                               len = strlen(buf);
566                               if (len > 0 && buf[len-1] == '\n')
567                                         buf[len-1] = '\0';
568                               else
569                               {
570                                         int c;
571                                         do {
572                                                   c = fgetc(fp);
573                                         } while (c != '\n' && c != EOF);
574                               }
575 
576                               if (getentry(buf, &name, &file, &line))
577                               {
578                                         /*
579                                          * Couldn't parse this line for some reason.
580                                          * We'll just pretend it never happened.
581                                          */
582                                         break;
583                               }
584 
585                               /* Make new entry and add to list. */
586                               tp = maketagent(file, (LINENUM) atoi(line), NULL, FALSE);
587                               TAG_INS(tp);
588                               total++;
589                     }
590                     if (fp != stdin)
591                     {
592                               if (pclose(fp))
593                               {
594                                         curtag = NULL;
595                                         total = curseq = 0;
596                                         return TAG_NOFILE;
597                               }
598                     }
599           }
600 
601           /* Check to see if we found anything. */
602           tp = taglist.tl_first;
603           if (tp == TAG_END)
604                     return TAG_NOTAG;
605           curtag = tp;
606           curseq = 1;
607           return TAG_FOUND;
608 }
609 
610 static int circular = 0;        /* 1: circular tag structure */
611 
612 /*
613  * Return the filename required for the next gtag in the queue that was setup
614  * by findgtag().  The next call to gtagsearch() will try to position at the
615  * appropriate tag.
616  */
nextgtag(void)617 static constant char * nextgtag(void)
618 {
619           struct tag *tp;
620 
621           if (curtag == NULL)
622                     /* No tag loaded */
623                     return NULL;
624 
625           tp = curtag->next;
626           if (tp == TAG_END)
627           {
628                     if (!circular)
629                               return NULL;
630                     /* Wrapped around to the head of the queue */
631                     curtag = taglist.tl_first;
632                     curseq = 1;
633           } else
634           {
635                     curtag = tp;
636                     curseq++;
637           }
638           return (curtag->tag_file);
639 }
640 
641 /*
642  * Return the filename required for the previous gtag in the queue that was
643  * setup by findgtat().  The next call to gtagsearch() will try to position
644  * at the appropriate tag.
645  */
prevgtag(void)646 static constant char * prevgtag(void)
647 {
648           struct tag *tp;
649 
650           if (curtag == NULL)
651                     /* No tag loaded */
652                     return NULL;
653 
654           tp = curtag->prev;
655           if (tp == TAG_END)
656           {
657                     if (!circular)
658                               return NULL;
659                     /* Wrapped around to the tail of the queue */
660                     curtag = taglist.tl_last;
661                     curseq = total;
662           } else
663           {
664                     curtag = tp;
665                     curseq--;
666           }
667           return (curtag->tag_file);
668 }
669 
670 /*
671  * Position the current file at at what is hopefully the tag that was chosen
672  * using either findtag() or one of nextgtag() and prevgtag().  Returns -1
673  * if it was unable to position at the tag, 0 if successful.
674  */
gtagsearch(void)675 static POSITION gtagsearch(void)
676 {
677           if (curtag == NULL)
678                     return (NULL_POSITION);  /* No gtags loaded! */
679           return (find_pos(curtag->tag_linenum));
680 }
681 
682 /*
683  * The getentry() parses both standard and extended ctags -x format.
684  *
685  * [standard format]
686  * <tag>   <lineno>  <file>         <image>
687  * +------------------------------------------------
688  * |main     30      main.c         main(argc, argv)
689  * |func     21      subr.c         func(arg)
690  *
691  * The following commands write this format.
692  *      o Traditional Ctags with -x option
693  *      o Global with -x option
694  *              See <http://www.gnu.org/software/global/global.html>
695  *
696  * [extended format]
697  * <tag>   <type>  <lineno>   <file>        <image>
698  * +----------------------------------------------------------
699  * |main     function 30      main.c         main(argc, argv)
700  * |func     function 21      subr.c         func(arg)
701  *
702  * The following commands write this format.
703  *      o Exuberant Ctags with -x option
704  *              See <http://ctags.sourceforge.net>
705  *
706  * Returns 0 on success, -1 on error.
707  * The tag, file, and line will each be NUL-terminated pointers
708  * into buf.
709  */
getentry(char * buf,constant char ** tag,constant char ** file,constant char ** line)710 static int getentry(char *buf, constant char **tag, constant char **file, constant char **line)
711 {
712           char *p = buf;
713 
714           for (*tag = p;  *p && !IS_SPACE(*p);  p++)      /* tag name */
715                     ;
716           if (*p == 0)
717                     return (-1);
718           *p++ = 0;
719           for ( ;  *p && IS_SPACE(*p);  p++)              /* (skip blanks) */
720                     ;
721           if (*p == 0)
722                     return (-1);
723           /*
724            * If the second part begin with other than digit,
725            * it is assumed tag type. Skip it.
726            */
727           if (!IS_DIGIT(*p))
728           {
729                     for ( ;  *p && !IS_SPACE(*p);  p++)     /* (skip tag type) */
730                               ;
731                     for (;  *p && IS_SPACE(*p);  p++)       /* (skip blanks) */
732                               ;
733           }
734           if (!IS_DIGIT(*p))
735                     return (-1);
736           *line = p;                                      /* line number */
737           for (*line = p;  *p && !IS_SPACE(*p);  p++)
738                     ;
739           if (*p == 0)
740                     return (-1);
741           *p++ = 0;
742           for ( ; *p && IS_SPACE(*p);  p++)               /* (skip blanks) */
743                     ;
744           if (*p == 0)
745                     return (-1);
746           *file = p;                                      /* file name */
747           for (*file = p;  *p && !IS_SPACE(*p);  p++)
748                     ;
749           if (*p == 0)
750                     return (-1);
751           *p = 0;
752 
753           /* value check */
754           if (strlen(*tag) && strlen(*line) && strlen(*file) && atoi(*line) > 0)
755                     return (0);
756           return (-1);
757 }
758 
759 #endif
760