1/*
2 * Copyright (C) 1984-2012  Mark Nudelman
3 * Modified for use with illumos by Garrett D'Amore.
4 * Copyright 2014 Garrett D'Amore <garrett@damore.org>
5 *
6 * You may distribute under the terms of either the GNU General Public
7 * License or the Less License, as specified in the README file.
8 *
9 * For more information, see the README file.
10 */
11
12#include "less.h"
13
14#define	WHITESP(c)	((c) == ' ' || (c) == '\t')
15
16char *tags = "tags";
17
18static int total;
19static int curseq;
20
21extern int linenums;
22
23enum tag_result {
24	TAG_FOUND,
25	TAG_NOFILE,
26	TAG_NOTAG,
27	TAG_NOTYPE,
28	TAG_INTR
29};
30
31static enum tag_result findctag(char *);
32static char *nextctag(void);
33static char *prevctag(void);
34static off_t ctagsearch(void);
35
36/*
37 * The list of tags generated by the last findctag() call.
38 */
39struct taglist {
40	struct tag *tl_first;
41	struct tag *tl_last;
42};
43#define	TAG_END  ((struct tag *)&taglist)
44static struct taglist taglist = { TAG_END, TAG_END };
45struct tag {
46	struct tag *next, *prev; /* List links */
47	char *tag_file;		/* Source file containing the tag */
48	off_t tag_linenum;	/* Appropriate line number in source file */
49	char *tag_pattern;	/* Pattern used to find the tag */
50	int tag_endline;	/* True if the pattern includes '$' */
51};
52static struct tag *curtag;
53
54#define	TAG_INS(tp) \
55	(tp)->next = TAG_END; \
56	(tp)->prev = taglist.tl_last; \
57	taglist.tl_last->next = (tp); \
58	taglist.tl_last = (tp);
59
60#define	TAG_RM(tp) \
61	(tp)->next->prev = (tp)->prev; \
62	(tp)->prev->next = (tp)->next;
63
64/*
65 * Delete tag structures.
66 */
67void
68cleantags(void)
69{
70	struct tag *tp;
71
72	/*
73	 * Delete any existing tag list.
74	 * {{ Ideally, we wouldn't do this until after we know that we
75	 *    can load some other tag information. }}
76	 */
77	while ((tp = taglist.tl_first) != TAG_END) {
78		TAG_RM(tp);
79		free(tp->tag_file);
80		free(tp->tag_pattern);
81		free(tp);
82	}
83	curtag = NULL;
84	total = curseq = 0;
85}
86
87/*
88 * Create a new tag entry.
89 */
90static struct tag *
91maketagent(char *file, off_t linenum, char *pattern, int endline)
92{
93	struct tag *tp;
94
95	tp = ecalloc(sizeof (struct tag), 1);
96	tp->tag_file = estrdup(file);
97	tp->tag_linenum = linenum;
98	tp->tag_endline = endline;
99	if (pattern == NULL)
100		tp->tag_pattern = NULL;
101	else
102		tp->tag_pattern = estrdup(pattern);
103	return (tp);
104}
105
106/*
107 * Find tags in tag file.
108 */
109void
110findtag(char *tag)
111{
112	enum tag_result result;
113
114	result = findctag(tag);
115	switch (result) {
116	case TAG_FOUND:
117	case TAG_INTR:
118		break;
119	case TAG_NOFILE:
120		error("No tags file", NULL);
121		break;
122	case TAG_NOTAG:
123		error("No such tag in tags file", NULL);
124		break;
125	case TAG_NOTYPE:
126		error("unknown tag type", NULL);
127		break;
128	}
129}
130
131/*
132 * Search for a tag.
133 */
134off_t
135tagsearch(void)
136{
137	if (curtag == NULL)
138		return (-1);	/* No tags loaded! */
139	if (curtag->tag_linenum != 0)
140		return (find_pos(curtag->tag_linenum));
141	return (ctagsearch());
142}
143
144/*
145 * Go to the next tag.
146 */
147char *
148nexttag(int n)
149{
150	char *tagfile = NULL;
151
152	while (n-- > 0)
153		tagfile = nextctag();
154	return (tagfile);
155}
156
157/*
158 * Go to the previous tag.
159 */
160char *
161prevtag(int n)
162{
163	char *tagfile = NULL;
164
165	while (n-- > 0)
166		tagfile = prevctag();
167	return (tagfile);
168}
169
170/*
171 * Return the total number of tags.
172 */
173int
174ntags(void)
175{
176	return (total);
177}
178
179/*
180 * Return the sequence number of current tag.
181 */
182int
183curr_tag(void)
184{
185	return (curseq);
186}
187
188/*
189 * Find tags in the "tags" file.
190 * Sets curtag to the first tag entry.
191 */
192static enum tag_result
193findctag(char *tag)
194{
195	char *p;
196	FILE *f;
197	int taglen;
198	off_t taglinenum;
199	char *tagfile;
200	char *tagpattern;
201	int tagendline;
202	int search_char;
203	int err;
204	char tline[TAGLINE_SIZE];
205	struct tag *tp;
206
207	p = shell_unquote(tags);
208	f = fopen(p, "r");
209	free(p);
210	if (f == NULL)
211		return (TAG_NOFILE);
212
213	cleantags();
214	total = 0;
215	taglen = strlen(tag);
216
217	/*
218	 * Search the tags file for the desired tag.
219	 */
220	while (fgets(tline, sizeof (tline), f) != NULL) {
221		if (tline[0] == '!')
222			/* Skip header of extended format. */
223			continue;
224		if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen]))
225			continue;
226
227		/*
228		 * Found it.
229		 * The line contains the tag, the filename and the
230		 * location in the file, separated by white space.
231		 * The location is either a decimal line number,
232		 * or a search pattern surrounded by a pair of delimiters.
233		 * Parse the line and extract these parts.
234		 */
235		tagpattern = NULL;
236
237		/*
238		 * Skip over the whitespace after the tag name.
239		 */
240		p = skipsp(tline+taglen);
241		if (*p == '\0')
242			/* File name is missing! */
243			continue;
244
245		/*
246		 * Save the file name.
247		 * Skip over the whitespace after the file name.
248		 */
249		tagfile = p;
250		while (!WHITESP(*p) && *p != '\0')
251			p++;
252		*p++ = '\0';
253		p = skipsp(p);
254		if (*p == '\0')
255			/* Pattern is missing! */
256			continue;
257
258		/*
259		 * First see if it is a line number.
260		 */
261		tagendline = 0;
262		taglinenum = getnum(&p, 0, &err);
263		if (err) {
264			/*
265			 * No, it must be a pattern.
266			 * Delete the initial "^" (if present) and
267			 * the final "$" from the pattern.
268			 * Delete any backslash in the pattern.
269			 */
270			taglinenum = 0;
271			search_char = *p++;
272			if (*p == '^')
273				p++;
274			tagpattern = p;
275			while (*p != search_char && *p != '\0') {
276				if (*p == '\\')
277					p++;
278				p++;
279			}
280			tagendline = (p[-1] == '$');
281			if (tagendline)
282				p--;
283			*p = '\0';
284		}
285		tp = maketagent(tagfile, taglinenum, tagpattern, tagendline);
286		TAG_INS(tp);
287		total++;
288	}
289	fclose(f);
290	if (total == 0)
291		return (TAG_NOTAG);
292	curtag = taglist.tl_first;
293	curseq = 1;
294	return (TAG_FOUND);
295}
296
297/*
298 * Edit current tagged file.
299 */
300int
301edit_tagfile(void)
302{
303	if (curtag == NULL)
304		return (1);
305	return (edit(curtag->tag_file));
306}
307
308/*
309 * Search for a tag.
310 * This is a stripped-down version of search().
311 * We don't use search() for several reasons:
312 *   -	We don't want to blow away any search string we may have saved.
313 *   -	The various regular-expression functions (from different systems:
314 *	regcmp vs. re_comp) behave differently in the presence of
315 *	parentheses (which are almost always found in a tag).
316 */
317static off_t
318ctagsearch(void)
319{
320	off_t pos, linepos;
321	off_t linenum;
322	int len;
323	char *line;
324
325	pos = ch_zero();
326	linenum = find_linenum(pos);
327
328	for (;;) {
329		/*
330		 * Get lines until we find a matching one or
331		 * until we hit end-of-file.
332		 */
333		if (abort_sigs())
334			return (-1);
335
336		/*
337		 * Read the next line, and save the
338		 * starting position of that line in linepos.
339		 */
340		linepos = pos;
341		pos = forw_raw_line(pos, &line, (int *)NULL);
342		if (linenum != 0)
343			linenum++;
344
345		if (pos == -1) {
346			/*
347			 * We hit EOF without a match.
348			 */
349			error("Tag not found", NULL);
350			return (-1);
351		}
352
353		/*
354		 * If we're using line numbers, we might as well
355		 * remember the information we have now (the position
356		 * and line number of the current line).
357		 */
358		if (linenums)
359			add_lnum(linenum, pos);
360
361		/*
362		 * Test the line to see if we have a match.
363		 * Use strncmp because the pattern may be
364		 * truncated (in the tags file) if it is too long.
365		 * If tagendline is set, make sure we match all
366		 * the way to end of line (no extra chars after the match).
367		 */
368		len = strlen(curtag->tag_pattern);
369		if (strncmp(curtag->tag_pattern, line, len) == 0 &&
370		    (!curtag->tag_endline || line[len] == '\0' ||
371		    line[len] == '\r')) {
372			curtag->tag_linenum = find_linenum(linepos);
373			break;
374		}
375	}
376
377	return (linepos);
378}
379
380static int circular = 0;	/* 1: circular tag structure */
381
382/*
383 * Return the filename required for the next tag in the queue that was setup
384 * by findctag().  The next call to ctagsearch() will try to position at the
385 * appropriate tag.
386 */
387static char *
388nextctag(void)
389{
390	struct tag *tp;
391
392	if (curtag == NULL)
393		/* No tag loaded */
394		return (NULL);
395
396	tp = curtag->next;
397	if (tp == TAG_END) {
398		if (!circular)
399			return (NULL);
400		/* Wrapped around to the head of the queue */
401		curtag = taglist.tl_first;
402		curseq = 1;
403	} else {
404		curtag = tp;
405		curseq++;
406	}
407	return (curtag->tag_file);
408}
409
410/*
411 * Return the filename required for the previous ctag in the queue that was
412 * setup by findctag().  The next call to ctagsearch() will try to position
413 * at the appropriate tag.
414 */
415static char *
416prevctag(void)
417{
418	struct tag *tp;
419
420	if (curtag == NULL)
421		/* No tag loaded */
422		return (NULL);
423
424	tp = curtag->prev;
425	if (tp == TAG_END) {
426		if (!circular)
427			return (NULL);
428		/* Wrapped around to the tail of the queue */
429		curtag = taglist.tl_last;
430		curseq = total;
431	} else {
432		curtag = tp;
433		curseq--;
434	}
435	return (curtag->tag_file);
436}
437