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