tags.c revision 1.10
1/*	$OpenBSD: tags.c,v 1.10 2014/11/16 00:59:25 guenther Exp $	*/
2
3/*
4 * This file is in the public domain.
5 *
6 * Author: Sunil Nimmagadda <sunil@sunilnimmagadda.com>
7 */
8
9#include <sys/queue.h>
10#include <sys/stat.h>
11#include <sys/tree.h>
12#include <sys/types.h>
13
14#include <ctype.h>
15#include <err.h>
16#include <stdlib.h>
17#include <string.h>
18#include <stdio.h>
19#include <util.h>
20
21#include "def.h"
22
23struct ctag;
24
25static int               addctag(char *);
26static int               atbow(void);
27void                     closetags(void);
28static int               ctagcmp(struct ctag *, struct ctag *);
29static int               loadbuffer(char *);
30static int               loadtags(const char *);
31static int               pushtag(char *);
32static int               searchpat(char *);
33static struct ctag       *searchtag(char *);
34static char              *strip(char *, size_t);
35static void              unloadtags(void);
36
37#define DEFAULTFN "tags"
38
39char *tagsfn = NULL;
40int  loaded  = FALSE;
41
42/* ctags(1) entries are parsed and maintained in a tree. */
43struct ctag {
44	RB_ENTRY(ctag) entry;
45	char *tag;
46	char *fname;
47	char *pat;
48};
49RB_HEAD(tagtree, ctag) tags = RB_INITIALIZER(&tags);
50RB_GENERATE(tagtree, ctag, entry, ctagcmp);
51
52struct tagpos {
53	SLIST_ENTRY(tagpos) entry;
54	int    doto;
55	int    dotline;
56	char   *bname;
57};
58SLIST_HEAD(tagstack, tagpos) shead = SLIST_HEAD_INITIALIZER(shead);
59
60int
61ctagcmp(struct ctag *s, struct ctag *t)
62{
63	return strcmp(s->tag, t->tag);
64}
65
66/*
67 * Record the filename that contain tags to be used while loading them
68 * on first use. If a filename is already recorded, ask user to retain
69 * already loaded tags (if any) and unload them if user chooses not to.
70 */
71/* ARGSUSED */
72int
73tagsvisit(int f, int n)
74{
75	char fname[NFILEN], *bufp, *temp;
76	struct stat sb;
77
78	if (getbufcwd(fname, sizeof(fname)) == FALSE)
79		fname[0] = '\0';
80
81	if (strlcat(fname, DEFAULTFN, sizeof(fname)) >= sizeof(fname)) {
82		dobeep();
83		ewprintf("Filename too long");
84		return (FALSE);
85	}
86
87	bufp = eread("visit tags table (default %s): ", fname,
88	    NFILEN, EFFILE | EFCR | EFNEW | EFDEF, DEFAULTFN);
89
90	if (stat(bufp, &sb) == -1) {
91		dobeep();
92		ewprintf("stat: %s", strerror(errno));
93		return (FALSE);
94	} else if (S_ISREG(sb.st_mode) == 0) {
95		dobeep();
96		ewprintf("Not a regular file");
97		return (FALSE);
98	} else if (access(bufp, R_OK) == -1) {
99		dobeep();
100		ewprintf("Cannot access file %s", bufp);
101		return (FALSE);
102	}
103
104	if (tagsfn == NULL) {
105		if (bufp == NULL)
106			return (ABORT);
107		else if (bufp[0] == '\0') {
108			if ((tagsfn = strdup(fname)) == NULL) {
109				dobeep();
110				ewprintf("Out of memory");
111				return (FALSE);
112			}
113		} else {
114			/* bufp points to local variable, so duplicate. */
115			if ((tagsfn = strdup(bufp)) == NULL) {
116				dobeep();
117				ewprintf("Out of memory");
118				return (FALSE);
119			}
120		}
121	} else {
122		if ((temp = strdup(bufp)) == NULL) {
123			dobeep();
124			ewprintf("Out of memory");
125			return (FALSE);
126		}
127		free(tagsfn);
128		tagsfn = temp;
129		if (eyorn("Keep current list of tags table also") == FALSE) {
130			ewprintf("Starting a new list of tags table");
131			unloadtags();
132		}
133		loaded = FALSE;
134	}
135	return (TRUE);
136}
137
138/*
139 * Ask user for a tag while treating word at dot as default. Visit tags
140 * file if not yet done, load tags and jump to definition of the tag.
141 */
142int
143findtag(int f, int n)
144{
145	char utok[MAX_TOKEN], dtok[MAX_TOKEN];
146	char *tok, *bufp;
147	int  ret;
148
149	if (curtoken(f, n, dtok) == FALSE) {
150		dtok[0] = '\0';
151		bufp = eread("Find tag:", utok, MAX_TOKEN, EFNUL | EFNEW);
152	} else
153		bufp = eread("Find tag (default %s):", utok, MAX_TOKEN,
154		    EFNUL | EFNEW, dtok);
155
156	if (bufp == NULL)
157		return (ABORT);
158	else if	(bufp[0] == '\0')
159		tok = dtok;
160	else
161		tok = utok;
162
163	if (tok[0] == '\0') {
164		dobeep();
165		ewprintf("There is no default tag");
166		return (FALSE);
167	}
168
169	if (tagsfn == NULL)
170		if ((ret = tagsvisit(f, n)) != TRUE)
171			return (ret);
172	if (!loaded) {
173		if (loadtags(tagsfn) == FALSE) {
174			free(tagsfn);
175			tagsfn = NULL;
176			return (FALSE);
177		}
178		loaded = TRUE;
179	}
180	return pushtag(tok);
181}
182
183/*
184 * Free tags tree.
185 */
186void
187unloadtags(void)
188{
189	struct ctag *var, *nxt;
190
191	for (var = RB_MIN(tagtree, &tags); var != NULL; var = nxt) {
192		nxt = RB_NEXT(tagtree, &tags, var);
193		RB_REMOVE(tagtree, &tags, var);
194		/* line parsed with fparseln needs to be freed */
195		free(var->tag);
196		free(var);
197	}
198}
199
200/*
201 * Lookup tag passed in tree and if found, push current location and
202 * buffername onto stack, load the file with tag definition into a new
203 * buffer and position dot at the pattern.
204 */
205/*ARGSUSED */
206int
207pushtag(char *tok)
208{
209	struct ctag *res;
210	struct tagpos *s;
211	char bname[NFILEN];
212	int doto, dotline;
213
214	if ((res = searchtag(tok)) == NULL)
215		return (FALSE);
216
217	doto = curwp->w_doto;
218	dotline = curwp->w_dotline;
219	/* record absolute filenames. Fixes issues when mg's cwd is not the
220	 * same as buffer's directory.
221	 */
222	if (strlcpy(bname, curbp->b_cwd, sizeof(bname)) >= sizeof(bname)) {
223		dobeep();
224		ewprintf("filename too long");
225		return (FALSE);
226	}
227	if (strlcat(bname, curbp->b_bname, sizeof(bname)) >= sizeof(bname)) {
228		dobeep();
229		ewprintf("filename too long");
230		return (FALSE);
231	}
232
233	if (loadbuffer(res->fname) == FALSE)
234		return (FALSE);
235
236	if (searchpat(res->pat) == TRUE) {
237		if ((s = malloc(sizeof(struct tagpos))) == NULL) {
238			dobeep();
239			ewprintf("Out of memory");
240			return (FALSE);
241		}
242		if ((s->bname = strdup(bname)) == NULL) {
243			dobeep();
244			ewprintf("Out of memory");
245			free(s);
246			return (FALSE);
247		}
248		s->doto = doto;
249		s->dotline = dotline;
250		SLIST_INSERT_HEAD(&shead, s, entry);
251		return (TRUE);
252	} else {
253		dobeep();
254		ewprintf("%s: pattern not found", res->tag);
255		return (FALSE);
256	}
257	/* NOTREACHED */
258	return (FALSE);
259}
260
261/*
262 * If tag stack is not empty pop stack and jump to recorded buffer, dot.
263 */
264/* ARGSUSED */
265int
266poptag(int f, int n)
267{
268	struct line *dotp;
269	struct tagpos *s;
270
271	if (SLIST_EMPTY(&shead)) {
272		dobeep();
273		ewprintf("No previous location for find-tag invocation");
274		return (FALSE);
275	}
276	s = SLIST_FIRST(&shead);
277	SLIST_REMOVE_HEAD(&shead, entry);
278	if (loadbuffer(s->bname) == FALSE)
279		return (FALSE);
280	curwp->w_dotline = s->dotline;
281	curwp->w_doto = s->doto;
282
283	/* storing of dotp in tagpos wouldn't work out in cases when
284	 * that buffer is killed by user(dangling pointer). Explicitly
285	 * traverse till dotline for correct handling.
286	 */
287	dotp = curwp->w_bufp->b_headp;
288	while (s->dotline--)
289		dotp = dotp->l_fp;
290
291	curwp->w_dotp = dotp;
292	free(s->bname);
293	free(s);
294	return (TRUE);
295}
296
297/*
298 * Parse the tags file and construct the tags tree. Remove escape
299 * characters while parsing the file.
300 */
301int
302loadtags(const char *fn)
303{
304	char *l;
305	FILE *fd;
306
307	if ((fd = fopen(fn, "r")) == NULL) {
308		dobeep();
309		ewprintf("Unable to open tags file: %s", fn);
310		return (FALSE);
311	}
312	while ((l = fparseln(fd, NULL, NULL, "\\\\\0",
313	    FPARSELN_UNESCCONT | FPARSELN_UNESCREST)) != NULL) {
314		if (addctag(l) == FALSE) {
315			fclose(fd);
316			return (FALSE);
317		}
318	}
319	fclose(fd);
320	return (TRUE);
321}
322
323/*
324 * Cleanup and destroy tree and stack.
325 */
326void
327closetags(void)
328{
329	struct tagpos *s;
330
331	while (!SLIST_EMPTY(&shead)) {
332		s = SLIST_FIRST(&shead);
333		SLIST_REMOVE_HEAD(&shead, entry);
334		free(s->bname);
335		free(s);
336	}
337	unloadtags();
338	free(tagsfn);
339}
340
341/*
342 * Strip away any special characters in pattern.
343 * The pattern in ctags isn't a true regular expression. Its of the form
344 * /^xxx$/ or ?^xxx$? and in some cases the "$" would be missing. Strip
345 * the leading and trailing special characters so the pattern matching
346 * would be a simple string compare. Escape character is taken care by
347 * fparseln.
348 */
349char *
350strip(char *s, size_t len)
351{
352	/* first strip trailing special chars */
353	s[len - 1] = '\0';
354	if (s[len - 2] == '$')
355		s[len - 2] = '\0';
356
357	/* then strip leading special chars */
358	s++;
359	if (*s == '^')
360		s++;
361
362	return s;
363}
364
365/*
366 * tags line is of the format "<tag>\t<filename>\t<pattern>". Split them
367 * by replacing '\t' with '\0'. This wouldn't alter the size of malloc'ed
368 * l, and can be freed during cleanup.
369 */
370int
371addctag(char *l)
372{
373	struct ctag *t;
374
375	if ((t = malloc(sizeof(struct ctag))) == NULL) {
376		dobeep();
377		ewprintf("Out of memory");
378		return (FALSE);
379	}
380	t->tag = l;
381	if ((l = strchr(l, '\t')) == NULL)
382		goto cleanup;
383	*l++ = '\0';
384	t->fname = l;
385	if ((l = strchr(l, '\t')) == NULL)
386		goto cleanup;
387	*l++ = '\0';
388	if (*l == '\0')
389		goto cleanup;
390	t->pat = strip(l, strlen(l));
391	RB_INSERT(tagtree, &tags, t);
392	return (TRUE);
393cleanup:
394	free(t);
395	free(l);
396	return (TRUE);
397}
398
399/*
400 * Search through each line of buffer for pattern.
401 */
402int
403searchpat(char *pat)
404{
405	struct line *lp;
406	int dotline;
407	size_t plen;
408
409	plen = strlen(pat);
410	dotline = 1;
411	lp = lforw(curbp->b_headp);
412	while (lp != curbp->b_headp) {
413		if (ltext(lp) != NULL && plen <= llength(lp) &&
414		    (strncmp(pat, ltext(lp), plen) == 0)) {
415			curwp->w_doto = 0;
416			curwp->w_dotp = lp;
417			curwp->w_dotline = dotline;
418			return (TRUE);
419		} else {
420			lp = lforw(lp);
421			dotline++;
422		}
423	}
424	return (FALSE);
425}
426
427/*
428 * Return TRUE if dot is at beginning of a word or at beginning
429 * of line, else FALSE.
430 */
431int
432atbow(void)
433{
434	if (curwp->w_doto == 0)
435		return (TRUE);
436	if (ISWORD(curwp->w_dotp->l_text[curwp->w_doto]) &&
437	    !ISWORD(curwp->w_dotp->l_text[curwp->w_doto - 1]))
438	    	return (TRUE);
439	return (FALSE);
440}
441
442/*
443 * Extract the word at dot without changing dot position.
444 */
445int
446curtoken(int f, int n, char *token)
447{
448	struct line *odotp;
449	int odoto, tdoto, odotline, size, r;
450	char c;
451
452	/* Underscore character is to be treated as "inword" while
453	 * processing tokens unlike mg's default word traversal. Save
454	 * and restore it's cinfo value so that tag matching works for
455	 * identifier with underscore.
456	 */
457	c = cinfo['_'];
458	cinfo['_'] = _MG_W;
459
460	odotp = curwp->w_dotp;
461	odoto = curwp->w_doto;
462	odotline = curwp->w_dotline;
463
464	/* Move backword unless we are at the beginning of a word or at
465	 * beginning of line.
466	 */
467	if (!atbow())
468		if ((r = backword(f, n)) == FALSE)
469			goto cleanup;
470
471	tdoto = curwp->w_doto;
472
473	if ((r = forwword(f, n)) == FALSE)
474		goto cleanup;
475
476	/* strip away leading whitespace if any like emacs. */
477	while (ltext(curwp->w_dotp) &&
478	    isspace(lgetc(curwp->w_dotp, tdoto)))
479		tdoto++;
480
481	size = curwp->w_doto - tdoto;
482	if (size <= 0 || size >= MAX_TOKEN ||
483	    ltext(curwp->w_dotp) == NULL) {
484		r = FALSE;
485		goto cleanup;
486	}
487	strncpy(token, ltext(curwp->w_dotp) + tdoto, size);
488	token[size] = '\0';
489	r = TRUE;
490
491cleanup:
492	cinfo['_'] = c;
493	curwp->w_dotp = odotp;
494	curwp->w_doto = odoto;
495	curwp->w_dotline = odotline;
496	return (r);
497}
498
499/*
500 * Search tagstree for a given token.
501 */
502struct ctag *
503searchtag(char *tok)
504{
505	struct ctag t, *res;
506
507	t.tag = tok;
508	if ((res = RB_FIND(tagtree, &tags, &t)) == NULL) {
509		dobeep();
510		ewprintf("No tag containing %s", tok);
511		return (NULL);
512	}
513	return res;
514}
515
516/*
517 * This is equivalent to filevisit from file.c.
518 * Look around to see if we can find the file in another buffer; if we
519 * can't find it, create a new buffer, read in the text, and switch to
520 * the new buffer. *scratch*, *grep*, *compile* needs to be handled
521 * differently from other buffers which have "filenames".
522 */
523int
524loadbuffer(char *bname)
525{
526	struct buffer *bufp;
527	char *adjf;
528
529	/* check for special buffers which begin with '*' */
530	if (bname[0] == '*') {
531		if ((bufp = bfind(bname, FALSE)) != NULL) {
532			curbp = bufp;
533			return (showbuffer(bufp, curwp, WFFULL));
534		} else {
535			return (FALSE);
536		}
537	} else {
538		if ((adjf = adjustname(bname, TRUE)) == NULL)
539			return (FALSE);
540		if ((bufp = findbuffer(adjf)) == NULL)
541			return (FALSE);
542	}
543	curbp = bufp;
544	if (showbuffer(bufp, curwp, WFFULL) != TRUE)
545		return (FALSE);
546	if (bufp->b_fname[0] == '\0') {
547		if (readin(adjf) != TRUE) {
548			killbuffer(bufp);
549			return (FALSE);
550		}
551	}
552	return (TRUE);
553}
554