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