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