1 /* 2 * Copyright (C) 1984-2024 Mark Nudelman 3 * 4 * You may distribute under the terms of either the GNU General Public 5 * License or the Less License, as specified in the README file. 6 * 7 * For more information, see the README file. 8 */ 9 10 11 #include "less.h" 12 13 #define WHITESP(c) ((c)==' ' || (c)=='\t') 14 15 #if TAGS 16 17 public constant char ztags[] = "tags"; 18 public constant char *tags = ztags; 19 20 static int total; 21 static int curseq; 22 23 extern int linenums; 24 extern int sigs; 25 extern int ctldisp; 26 27 enum tag_result { 28 TAG_FOUND, 29 TAG_NOFILE, 30 TAG_NOTAG, 31 TAG_NOTYPE, 32 TAG_INTR 33 }; 34 35 /* 36 * Tag type 37 */ 38 enum { 39 T_CTAGS, /* 'tags': standard and extended format (ctags) */ 40 T_CTAGS_X, /* stdin: cross reference format (ctags) */ 41 T_GTAGS, /* 'GTAGS': function definition (global) */ 42 T_GRTAGS, /* 'GRTAGS': function reference (global) */ 43 T_GSYMS, /* 'GSYMS': other symbols (global) */ 44 T_GPATH /* 'GPATH': path name (global) */ 45 }; 46 47 static enum tag_result findctag(constant char *tag); 48 static enum tag_result findgtag(constant char *tag, int type); 49 static constant char *nextgtag(void); 50 static constant char *prevgtag(void); 51 static POSITION ctagsearch(void); 52 static POSITION gtagsearch(void); 53 static int getentry(char *buf, constant char **tag, constant char **file, constant char **line); 54 55 /* 56 * The list of tags generated by the last findgtag() call. 57 * 58 * Use either pattern or line number. 59 * findgtag() always uses line number, so pattern is always NULL. 60 * findctag() uses either pattern (in which case line number is 0), 61 * or line number (in which case pattern is NULL). 62 */ 63 struct taglist { 64 struct tag *tl_first; 65 struct tag *tl_last; 66 }; 67 struct tag { 68 struct tag *next, *prev; /* List links */ 69 char *tag_file; /* Source file containing the tag */ 70 LINENUM tag_linenum; /* Appropriate line number in source file */ 71 char *tag_pattern; /* Pattern used to find the tag */ 72 lbool tag_endline; /* True if the pattern includes '$' */ 73 }; 74 #define TAG_END ((struct tag *) &taglist) 75 static struct taglist taglist = { TAG_END, TAG_END }; 76 static struct tag *curtag; 77 78 #define TAG_INS(tp) \ 79 (tp)->next = TAG_END; \ 80 (tp)->prev = taglist.tl_last; \ 81 taglist.tl_last->next = (tp); \ 82 taglist.tl_last = (tp); 83 84 #define TAG_RM(tp) \ 85 (tp)->next->prev = (tp)->prev; \ 86 (tp)->prev->next = (tp)->next; 87 88 /* 89 * Delete tag structures. 90 */ 91 public void cleantags(void) 92 { 93 struct tag *tp; 94 95 /* 96 * Delete any existing tag list. 97 * {{ Ideally, we wouldn't do this until after we know that we 98 * can load some other tag information. }} 99 */ 100 while ((tp = taglist.tl_first) != TAG_END) 101 { 102 TAG_RM(tp); 103 free(tp->tag_file); 104 free(tp->tag_pattern); 105 free(tp); 106 } 107 curtag = NULL; 108 total = curseq = 0; 109 } 110 111 /* 112 * Create a new tag entry. 113 */ 114 static struct tag * maketagent(constant char *file, LINENUM linenum, constant char *pattern, lbool endline) 115 { 116 struct tag *tp; 117 118 tp = (struct tag *) ecalloc(sizeof(struct tag), 1); 119 tp->tag_file = (char *) ecalloc(strlen(file) + 1, sizeof(char)); 120 strcpy(tp->tag_file, file); 121 tp->tag_linenum = linenum; 122 tp->tag_endline = endline; 123 if (pattern == NULL) 124 tp->tag_pattern = NULL; 125 else 126 { 127 tp->tag_pattern = (char *) ecalloc(strlen(pattern) + 1, sizeof(char)); 128 strcpy(tp->tag_pattern, pattern); 129 } 130 return (tp); 131 } 132 133 /* 134 * Get tag mode. 135 */ 136 public int gettagtype(void) 137 { 138 int f; 139 140 if (strcmp(tags, "GTAGS") == 0) 141 return T_GTAGS; 142 if (strcmp(tags, "GRTAGS") == 0) 143 return T_GRTAGS; 144 if (strcmp(tags, "GSYMS") == 0) 145 return T_GSYMS; 146 if (strcmp(tags, "GPATH") == 0) 147 return T_GPATH; 148 if (strcmp(tags, "-") == 0) 149 return T_CTAGS_X; 150 f = open(tags, OPEN_READ); 151 if (f >= 0) 152 { 153 close(f); 154 return T_CTAGS; 155 } 156 return T_GTAGS; 157 } 158 159 /* 160 * Find tags in tag file. 161 * Find a tag in the "tags" file. 162 * Sets "tag_file" to the name of the file containing the tag, 163 * and "tagpattern" to the search pattern which should be used 164 * to find the tag. 165 */ 166 public void findtag(constant char *tag) 167 { 168 int type = gettagtype(); 169 enum tag_result result; 170 171 if (type == T_CTAGS) 172 result = findctag(tag); 173 else 174 result = findgtag(tag, type); 175 switch (result) 176 { 177 case TAG_FOUND: 178 case TAG_INTR: 179 break; 180 case TAG_NOFILE: 181 error("No tags file", NULL_PARG); 182 break; 183 case TAG_NOTAG: 184 error("No such tag in tags file", NULL_PARG); 185 break; 186 case TAG_NOTYPE: 187 error("unknown tag type", NULL_PARG); 188 break; 189 } 190 } 191 192 /* 193 * Search for a tag. 194 */ 195 public POSITION tagsearch(void) 196 { 197 if (curtag == NULL) 198 return (NULL_POSITION); /* No gtags loaded! */ 199 if (curtag->tag_linenum != 0) 200 return gtagsearch(); 201 else 202 return ctagsearch(); 203 } 204 205 /* 206 * Go to the next tag. 207 */ 208 public constant char * nexttag(int n) 209 { 210 constant char *tagfile = (char *) NULL; 211 212 while (n-- > 0) 213 tagfile = nextgtag(); 214 return tagfile; 215 } 216 217 /* 218 * Go to the previous tag. 219 */ 220 public constant char * prevtag(int n) 221 { 222 constant char *tagfile = (char *) NULL; 223 224 while (n-- > 0) 225 tagfile = prevgtag(); 226 return tagfile; 227 } 228 229 /* 230 * Return the total number of tags. 231 */ 232 public int ntags(void) 233 { 234 return total; 235 } 236 237 /* 238 * Return the sequence number of current tag. 239 */ 240 public int curr_tag(void) 241 { 242 return curseq; 243 } 244 245 /***************************************************************************** 246 * ctags 247 */ 248 249 /* 250 * Find tags in the "tags" file. 251 * Sets curtag to the first tag entry. 252 */ 253 static enum tag_result findctag(constant char *tag) 254 { 255 char *p; 256 char *q; 257 FILE *f; 258 size_t taglen; 259 LINENUM taglinenum; 260 char *tagfile; 261 char *tagpattern; 262 lbool tagendline; 263 int search_char; 264 lbool err; 265 char tline[TAGLINE_SIZE]; 266 struct tag *tp; 267 268 p = shell_unquote(tags); 269 f = fopen(p, "r"); 270 free(p); 271 if (f == NULL) 272 return TAG_NOFILE; 273 274 cleantags(); 275 total = 0; 276 taglen = strlen(tag); 277 278 /* 279 * Search the tags file for the desired tag. 280 */ 281 while (fgets(tline, sizeof(tline), f) != NULL) 282 { 283 if (tline[0] == '!') 284 /* Skip header of extended format. */ 285 continue; 286 if (strncmp(tag, tline, taglen) != 0 || !WHITESP(tline[taglen])) 287 continue; 288 289 /* 290 * Found it. 291 * The line contains the tag, the filename and the 292 * location in the file, separated by white space. 293 * The location is either a decimal line number, 294 * or a search pattern surrounded by a pair of delimiters. 295 * Parse the line and extract these parts. 296 */ 297 tagpattern = NULL; 298 299 /* 300 * Skip over the whitespace after the tag name. 301 */ 302 p = skipsp(tline+taglen); 303 if (*p == '\0') 304 /* File name is missing! */ 305 continue; 306 307 /* 308 * Save the file name. 309 * Skip over the whitespace after the file name. 310 */ 311 tagfile = p; 312 while (!WHITESP(*p) && *p != '\0') 313 p++; 314 *p++ = '\0'; 315 p = skipsp(p); 316 if (*p == '\0') 317 /* Pattern is missing! */ 318 continue; 319 320 /* 321 * First see if it is a line number. 322 */ 323 tagendline = FALSE; 324 taglinenum = getnum(&p, 0, &err); 325 if (err) 326 { 327 /* 328 * No, it must be a pattern. 329 * Delete the initial "^" (if present) and 330 * the final "$" from the pattern. 331 * Delete any backslash in the pattern. 332 */ 333 taglinenum = 0; 334 search_char = *p++; 335 if (*p == '^') 336 p++; 337 tagpattern = q = p; 338 while (*p != search_char && *p != '\0') 339 { 340 if (*p == '\\') 341 p++; 342 if (q != p) 343 { 344 *q++ = *p++; 345 } else 346 { 347 q++; 348 p++; 349 } 350 } 351 tagendline = (q[-1] == '$'); 352 if (tagendline) 353 q--; 354 *q = '\0'; 355 } 356 tp = maketagent(tagfile, taglinenum, tagpattern, tagendline); 357 TAG_INS(tp); 358 total++; 359 } 360 fclose(f); 361 if (total == 0) 362 return TAG_NOTAG; 363 curtag = taglist.tl_first; 364 curseq = 1; 365 return TAG_FOUND; 366 } 367 368 /* 369 * Edit current tagged file. 370 */ 371 public int edit_tagfile(void) 372 { 373 if (curtag == NULL) 374 return (1); 375 return (edit(curtag->tag_file)); 376 } 377 378 static int curtag_match(char constant *line, POSITION linepos) 379 { 380 /* 381 * Test the line to see if we have a match. 382 * Use strncmp because the pattern may be 383 * truncated (in the tags file) if it is too long. 384 * If tagendline is set, make sure we match all 385 * the way to end of line (no extra chars after the match). 386 */ 387 size_t len = strlen(curtag->tag_pattern); 388 if (strncmp(curtag->tag_pattern, line, len) == 0 && 389 (!curtag->tag_endline || line[len] == '\0' || line[len] == '\r')) 390 { 391 curtag->tag_linenum = find_linenum(linepos); 392 return 1; 393 } 394 return 0; 395 } 396 397 /* 398 * Search for a tag. 399 * This is a stripped-down version of search(). 400 * We don't use search() for several reasons: 401 * - We don't want to blow away any search string we may have saved. 402 * - The various regular-expression functions (from different systems: 403 * regcmp vs. re_comp) behave differently in the presence of 404 * parentheses (which are almost always found in a tag). 405 */ 406 static POSITION ctagsearch(void) 407 { 408 POSITION pos, linepos; 409 LINENUM linenum; 410 size_t line_len; 411 constant char *line; 412 int found; 413 414 pos = ch_zero(); 415 linenum = find_linenum(pos); 416 417 for (found = 0; !found;) 418 { 419 /* 420 * Get lines until we find a matching one or 421 * until we hit end-of-file. 422 */ 423 if (ABORT_SIGS()) 424 return (NULL_POSITION); 425 426 /* 427 * Read the next line, and save the 428 * starting position of that line in linepos. 429 */ 430 linepos = pos; 431 pos = forw_raw_line(pos, &line, &line_len); 432 if (linenum != 0) 433 linenum++; 434 435 if (pos == NULL_POSITION) 436 { 437 /* 438 * We hit EOF without a match. 439 */ 440 error("Tag not found", NULL_PARG); 441 return (NULL_POSITION); 442 } 443 444 /* 445 * If we're using line numbers, we might as well 446 * remember the information we have now (the position 447 * and line number of the current line). 448 */ 449 if (linenums) 450 add_lnum(linenum, pos); 451 452 if (ctldisp != OPT_ONPLUS) 453 { 454 if (curtag_match(line, linepos)) 455 found = 1; 456 } else 457 { 458 int cvt_ops = CVT_ANSI; 459 size_t cvt_len = cvt_length(line_len, cvt_ops); 460 int *chpos = cvt_alloc_chpos(cvt_len); 461 char *cline = (char *) ecalloc(1, cvt_len); 462 cvt_text(cline, line, chpos, &line_len, cvt_ops); 463 if (curtag_match(cline, linepos)) 464 found = 1; 465 free(chpos); 466 free(cline); 467 } 468 } 469 470 return (linepos); 471 } 472 473 /******************************************************************************* 474 * gtags 475 */ 476 477 /* 478 * Find tags in the GLOBAL's tag file. 479 * The findgtag() will try and load information about the requested tag. 480 * It does this by calling "global -x tag" and storing the parsed output 481 * for future use by gtagsearch(). 482 * Sets curtag to the first tag entry. 483 */ 484 static enum tag_result findgtag(constant char *tag, int type) 485 { 486 char buf[1024]; 487 FILE *fp; 488 struct tag *tp; 489 490 if (type != T_CTAGS_X && tag == NULL) 491 return TAG_NOFILE; 492 493 cleantags(); 494 total = 0; 495 496 /* 497 * If type == T_CTAGS_X then read ctags's -x format from stdin 498 * else execute global(1) and read from it. 499 */ 500 if (type == T_CTAGS_X) 501 { 502 fp = stdin; 503 /* Set tag default because we cannot read stdin again. */ 504 tags = ztags; 505 } else 506 { 507 #if !HAVE_POPEN 508 return TAG_NOFILE; 509 #else 510 char *command; 511 char *flag; 512 char *qtag; 513 constant char *cmd = lgetenv("LESSGLOBALTAGS"); 514 515 if (isnullenv(cmd)) 516 return TAG_NOFILE; 517 /* Get suitable flag value for global(1). */ 518 switch (type) 519 { 520 case T_GTAGS: 521 flag = "" ; 522 break; 523 case T_GRTAGS: 524 flag = "r"; 525 break; 526 case T_GSYMS: 527 flag = "s"; 528 break; 529 case T_GPATH: 530 flag = "P"; 531 break; 532 default: 533 return TAG_NOTYPE; 534 } 535 536 /* Get our data from global(1). */ 537 qtag = shell_quote(tag); 538 if (qtag == NULL) 539 qtag = save(tag); 540 command = (char *) ecalloc(strlen(cmd) + strlen(flag) + 541 strlen(qtag) + 5, sizeof(char)); 542 sprintf(command, "%s -x%s %s", cmd, flag, qtag); 543 free(qtag); 544 fp = popen(command, "r"); 545 free(command); 546 #endif 547 } 548 if (fp != NULL) 549 { 550 while (fgets(buf, sizeof(buf), fp)) 551 { 552 constant char *name; 553 constant char *file; 554 constant char *line; 555 size_t len; 556 557 if (sigs) 558 { 559 #if HAVE_POPEN 560 if (fp != stdin) 561 pclose(fp); 562 #endif 563 return TAG_INTR; 564 } 565 len = strlen(buf); 566 if (len > 0 && buf[len-1] == '\n') 567 buf[len-1] = '\0'; 568 else 569 { 570 int c; 571 do { 572 c = fgetc(fp); 573 } while (c != '\n' && c != EOF); 574 } 575 576 if (getentry(buf, &name, &file, &line)) 577 { 578 /* 579 * Couldn't parse this line for some reason. 580 * We'll just pretend it never happened. 581 */ 582 break; 583 } 584 585 /* Make new entry and add to list. */ 586 tp = maketagent(file, (LINENUM) atoi(line), NULL, FALSE); 587 TAG_INS(tp); 588 total++; 589 } 590 if (fp != stdin) 591 { 592 if (pclose(fp)) 593 { 594 curtag = NULL; 595 total = curseq = 0; 596 return TAG_NOFILE; 597 } 598 } 599 } 600 601 /* Check to see if we found anything. */ 602 tp = taglist.tl_first; 603 if (tp == TAG_END) 604 return TAG_NOTAG; 605 curtag = tp; 606 curseq = 1; 607 return TAG_FOUND; 608 } 609 610 static int circular = 0; /* 1: circular tag structure */ 611 612 /* 613 * Return the filename required for the next gtag in the queue that was setup 614 * by findgtag(). The next call to gtagsearch() will try to position at the 615 * appropriate tag. 616 */ 617 static constant char * nextgtag(void) 618 { 619 struct tag *tp; 620 621 if (curtag == NULL) 622 /* No tag loaded */ 623 return NULL; 624 625 tp = curtag->next; 626 if (tp == TAG_END) 627 { 628 if (!circular) 629 return NULL; 630 /* Wrapped around to the head of the queue */ 631 curtag = taglist.tl_first; 632 curseq = 1; 633 } else 634 { 635 curtag = tp; 636 curseq++; 637 } 638 return (curtag->tag_file); 639 } 640 641 /* 642 * Return the filename required for the previous gtag in the queue that was 643 * setup by findgtat(). The next call to gtagsearch() will try to position 644 * at the appropriate tag. 645 */ 646 static constant char * prevgtag(void) 647 { 648 struct tag *tp; 649 650 if (curtag == NULL) 651 /* No tag loaded */ 652 return NULL; 653 654 tp = curtag->prev; 655 if (tp == TAG_END) 656 { 657 if (!circular) 658 return NULL; 659 /* Wrapped around to the tail of the queue */ 660 curtag = taglist.tl_last; 661 curseq = total; 662 } else 663 { 664 curtag = tp; 665 curseq--; 666 } 667 return (curtag->tag_file); 668 } 669 670 /* 671 * Position the current file at at what is hopefully the tag that was chosen 672 * using either findtag() or one of nextgtag() and prevgtag(). Returns -1 673 * if it was unable to position at the tag, 0 if successful. 674 */ 675 static POSITION gtagsearch(void) 676 { 677 if (curtag == NULL) 678 return (NULL_POSITION); /* No gtags loaded! */ 679 return (find_pos(curtag->tag_linenum)); 680 } 681 682 /* 683 * The getentry() parses both standard and extended ctags -x format. 684 * 685 * [standard format] 686 * <tag> <lineno> <file> <image> 687 * +------------------------------------------------ 688 * |main 30 main.c main(argc, argv) 689 * |func 21 subr.c func(arg) 690 * 691 * The following commands write this format. 692 * o Traditional Ctags with -x option 693 * o Global with -x option 694 * See <http://www.gnu.org/software/global/global.html> 695 * 696 * [extended format] 697 * <tag> <type> <lineno> <file> <image> 698 * +---------------------------------------------------------- 699 * |main function 30 main.c main(argc, argv) 700 * |func function 21 subr.c func(arg) 701 * 702 * The following commands write this format. 703 * o Exuberant Ctags with -x option 704 * See <http://ctags.sourceforge.net> 705 * 706 * Returns 0 on success, -1 on error. 707 * The tag, file, and line will each be NUL-terminated pointers 708 * into buf. 709 */ 710 static int getentry(char *buf, constant char **tag, constant char **file, constant char **line) 711 { 712 char *p = buf; 713 714 for (*tag = p; *p && !IS_SPACE(*p); p++) /* tag name */ 715 ; 716 if (*p == 0) 717 return (-1); 718 *p++ = 0; 719 for ( ; *p && IS_SPACE(*p); p++) /* (skip blanks) */ 720 ; 721 if (*p == 0) 722 return (-1); 723 /* 724 * If the second part begin with other than digit, 725 * it is assumed tag type. Skip it. 726 */ 727 if (!IS_DIGIT(*p)) 728 { 729 for ( ; *p && !IS_SPACE(*p); p++) /* (skip tag type) */ 730 ; 731 for (; *p && IS_SPACE(*p); p++) /* (skip blanks) */ 732 ; 733 } 734 if (!IS_DIGIT(*p)) 735 return (-1); 736 *line = p; /* line number */ 737 for (*line = p; *p && !IS_SPACE(*p); p++) 738 ; 739 if (*p == 0) 740 return (-1); 741 *p++ = 0; 742 for ( ; *p && IS_SPACE(*p); p++) /* (skip blanks) */ 743 ; 744 if (*p == 0) 745 return (-1); 746 *file = p; /* file name */ 747 for (*file = p; *p && !IS_SPACE(*p); p++) 748 ; 749 if (*p == 0) 750 return (-1); 751 *p = 0; 752 753 /* value check */ 754 if (strlen(*tag) && strlen(*line) && strlen(*file) && atoi(*line) > 0) 755 return (0); 756 return (-1); 757 } 758 759 #endif 760