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