1 /* 2 * Copyright 2007 Paulo Csar Pereira de Andrade 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a 5 * copy of this software and associated documentation files (the "Software"), 6 * to deal in the Software without restriction, including without limitation 7 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 * and/or sell copies of the Software, and to permit persons to whom the 9 * Software is furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice (including the next 12 * paragraph) shall be included in all copies or substantial portions of the 13 * Software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 18 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 * DEALINGS IN THE SOFTWARE. 22 * 23 * Author: Paulo Csar Pereira de Andrade 24 */ 25 26 /* 27 * Certain tag files may require quite some time and memory to load. 28 * Linux kernel 2.6x is an example, the tags file itself is almost 80Mb 29 * and xedit will use over 100Mb to store the data, and take quite some 30 * time to load it (and can grow drastically with every loaded files 31 * due to the memory used by the file contents and internal structures, 32 * like the syntax highlight ones). 33 * Possible workarounds could be to load the tags file in a separate 34 * process or thread. The memory problem would be hard to circunvent, 35 * as the tags file metadata would need to be stored in some very fast 36 * database, or at least some special format that would not require 37 * a linear search in a huge tags file. 38 */ 39 40 #include "xedit.h" 41 #include "util.h" 42 #include "re.h" 43 #include <unistd.h> 44 45 /* 46 * Types 47 */ 48 typedef struct _TagsEntry TagsEntry; 49 typedef struct _RegexEntry RegexEntry; 50 51 struct _TagsEntry { 52 hash_key *symbol; 53 TagsEntry *next; 54 55 int nentries; 56 hash_entry **filenames; 57 char **patterns; 58 }; 59 60 struct _RegexEntry { 61 hash_key *pattern; 62 RegexEntry *next; 63 64 re_cod regex; 65 }; 66 67 struct _XeditTagsInfo { 68 hash_key *pathname; 69 XeditTagsInfo *next; 70 71 hash_table *entries; 72 hash_table *filenames; 73 hash_table *patterns; 74 75 /* Used when searching for alternate tags and failing descending to 76 * root directory */ 77 Boolean visited; 78 79 /* Flag to know if tags file is in xedit cwd and allow using relative 80 * pathnames when loading a file with some tag definition, so that 81 * other code will not fail to write file (or even worse, write to 82 * wrong file) if file is edited and tags is not in the current dir */ 83 Boolean incwd; 84 85 /* Cache information for circulating over multiple definitions */ 86 XeditTagsInfo *tags; /* If trying another TagsInfo */ 87 TagsEntry *entry; /* Entry in tags->tags */ 88 int offset; 89 Widget textwindow; 90 XawTextPosition position; 91 }; 92 93 /* 94 * Prototypes 95 */ 96 static XeditTagsInfo *LoadTagsFile(char *tagsfile); 97 static XeditTagsInfo *DoLoadTagsFile(char *tagsfile, int length); 98 static void FindTagFirst(XeditTagsInfo *tags, char *symbol, int length); 99 static void FindTagNext(XeditTagsInfo *tags, 100 Widget window, XawTextPosition position); 101 static void FindTag(XeditTagsInfo *tags); 102 103 /* 104 * Initialization 105 */ 106 extern Widget texts[3]; 107 static hash_table *ht_tags; 108 109 /* 110 * Implementation 111 */ 112 void 113 TagsAction(Widget w, XEvent *event, String *params, Cardinal *num_params) 114 { 115 xedit_flist_item *item; 116 char buffer[1024]; 117 XawTextPosition position, left, right; 118 XawTextBlock block; 119 int length, bytes; 120 Widget source; 121 122 source = XawTextGetSource(w); 123 item = FindTextSource(source, NULL); 124 if (item->tags == NULL) 125 SearchTagsFile(item); 126 127 if (item->tags) { 128 position = XawTextGetInsertionPoint(w); 129 XawTextGetSelectionPos(w, &left, &right); 130 if (right > left) { 131 length = 0; 132 do { 133 bytes = right - left; 134 left = XawTextSourceRead(source, left, &block, bytes); 135 if (block.length < bytes) 136 bytes = block.length; 137 if (length + bytes + 1 >= sizeof(buffer)) 138 bytes = sizeof(buffer) - length - 1; 139 XmuSnprintf(buffer + length, bytes + 1, "%s", block.ptr); 140 length += bytes; 141 } while (left < right); 142 item->tags->textwindow = w; 143 item->tags->position = position; 144 FindTagFirst(item->tags, buffer, length); 145 } 146 else 147 FindTagNext(item->tags, w, position); 148 } 149 else 150 Feep(); 151 } 152 153 void 154 SearchTagsFile(xedit_flist_item *item) 155 { 156 if (app_resources.loadTags) { 157 char buffer[BUFSIZ]; 158 char *ptr, *tagsfile; 159 int length; 160 Boolean exists; 161 FileAccess file_access; 162 163 tagsfile = NULL; 164 165 /* If path fully specified in resource */ 166 if (app_resources.tagsName[0] == '/') 167 tagsfile = ResolveName(app_resources.tagsName); 168 /* Descend up to root directory searching for a tags file */ 169 else { 170 /* *scratch* buffer */ 171 if (item->filename[0] != '/') { 172 ptr = ResolveName(app_resources.tagsName); 173 strncpy(buffer, ptr ? ptr : "", sizeof(buffer)); 174 } 175 else 176 strncpy(buffer, item->filename, sizeof(buffer)); 177 178 /* Make sure buffer is nul terminated */ 179 buffer[sizeof(buffer) - 1] = '\0'; 180 ptr = buffer + strlen(buffer); 181 182 for (;;) { 183 while (ptr > buffer && ptr[-1] != '/') 184 --ptr; 185 if (ptr <= buffer) 186 break; 187 length = ptr - buffer; 188 if (length >= sizeof(buffer)) 189 length = sizeof(buffer); 190 strncpy(ptr, app_resources.tagsName, 191 sizeof(buffer) - length); 192 buffer[sizeof(buffer) - 1] = '\0'; 193 194 /* Check if tags filename exists */ 195 tagsfile = ResolveName(buffer); 196 if (tagsfile != NULL) { 197 file_access = CheckFilePermissions(tagsfile, &exists); 198 /* Check if can read tagsfile */ 199 if (exists && 200 (file_access == READ_OK || file_access == WRITE_OK)) 201 break; 202 else 203 tagsfile = NULL; 204 } 205 *--ptr = '\0'; 206 } 207 } 208 209 if (tagsfile) 210 item->tags = LoadTagsFile(tagsfile); 211 else { 212 XeditPrintf("No tags file found." 213 " Run \"ctags -R\" to build a tags file.\n"); 214 item->tags = NULL; 215 } 216 } 217 } 218 219 static void 220 FindTagFirst(XeditTagsInfo *tags, char *symbol, int length) 221 { 222 char *ptr; 223 TagsEntry *entry; 224 char buffer[BUFSIZ]; 225 226 /* Check for malformed parameters */ 227 ptr = symbol; 228 while (*ptr) { 229 if (*ptr == ' ' || *ptr == '\t' || *ptr == '\n' || *ptr == '\r' || 230 *ptr == '(' || *ptr == ')') { 231 Feep(); 232 return; 233 } 234 ptr++; 235 } 236 237 /* First try in buffer tags */ 238 tags->tags = tags; 239 entry = (TagsEntry *)hash_check(tags->entries, symbol, length); 240 if (entry == NULL) { 241 /* Try to find in alternate tags */ 242 strncpy(buffer, tags->pathname->value, tags->pathname->length); 243 buffer[tags->pathname->length] = '\0'; 244 ptr = buffer + tags->pathname->length - 1; 245 246 for (tags->tags = (XeditTagsInfo *)hash_iter_first(ht_tags); 247 tags->tags; 248 tags->tags = (XeditTagsInfo *)hash_iter_next(ht_tags)) 249 tags->tags->visited = False; 250 251 tags->visited = True; 252 253 while (ptr > buffer && entry == NULL) { 254 --ptr; 255 while (ptr > buffer && ptr[-1] != '/') 256 --ptr; 257 if (ptr <= buffer) 258 break; 259 *ptr = '\0'; 260 261 /* Try an upper directory tags */ 262 tags->tags = (XeditTagsInfo *) 263 hash_check(ht_tags, buffer, ptr - buffer); 264 if (tags->tags) { 265 tags->tags->visited = True; 266 entry = (TagsEntry *) 267 hash_check(tags->tags->entries, symbol, length); 268 } 269 } 270 271 /* If still failed, check other available tags 272 * for possible different projects */ 273 if (entry == NULL) { 274 for (tags->tags = (XeditTagsInfo *)hash_iter_first(ht_tags); 275 tags->tags; 276 tags->tags = (XeditTagsInfo *)hash_iter_next(ht_tags)) { 277 if (tags->tags->visited == False) { 278 entry = (TagsEntry *) 279 hash_check(tags->tags->entries, symbol, length); 280 /* Stop on first match */ 281 if (entry != NULL) 282 break; 283 } 284 } 285 } 286 287 if (entry == NULL) { 288 XeditPrintf("Symbol %s not in tags\n", symbol); 289 Feep(); 290 return; 291 } 292 } 293 294 tags->entry = entry; 295 tags->offset = 0; 296 297 FindTag(tags); 298 } 299 300 static void 301 FindTagNext(XeditTagsInfo *tags, Widget window, XawTextPosition position) 302 { 303 if (window != tags->textwindow || position != tags->position) 304 Feep(); 305 else { 306 if (tags->entry->nentries > 1) { 307 if (++tags->offset >= tags->entry->nentries) 308 tags->offset = 0; 309 FindTag(tags); 310 } 311 else 312 Feep(); 313 } 314 } 315 316 static XeditTagsInfo * 317 LoadTagsFile(char *tagsfile) 318 { 319 XeditTagsInfo *tags; 320 int length; 321 322 if (ht_tags == NULL) 323 ht_tags = hash_new(11, NULL); 324 325 /* tags key is only the directory name with ending '/' */ 326 length = strlen(tagsfile) - strlen(app_resources.tagsName); 327 tags = (XeditTagsInfo *)hash_check(ht_tags, tagsfile, length); 328 329 return (tags ? tags : DoLoadTagsFile(tagsfile, length)); 330 } 331 332 static XeditTagsInfo * 333 DoLoadTagsFile(char *tagsfile, int length) 334 { 335 char *ptr; 336 FILE *file; 337 XeditTagsInfo *tags; 338 TagsEntry *entry; 339 hash_entry *file_entry; 340 char buffer[BUFSIZ]; 341 char *symbol, *filename, *pattern; 342 343 file = fopen(tagsfile, "r"); 344 if (file) { 345 char *cwd; 346 347 tags = XtNew(XeditTagsInfo); 348 349 cwd = getcwd(buffer, sizeof(buffer)); 350 tags->incwd = cwd && 351 (strlen(cwd) == length - 1 && 352 memcmp(cwd, tagsfile, length - 1) == 0); 353 354 /* Build pathname as a nul terminated directory specification string */ 355 tags->pathname = XtNew(hash_key); 356 tags->pathname->value = XtMalloc(length + 1); 357 tags->pathname->length = length; 358 memcpy(tags->pathname->value, tagsfile, length); 359 tags->pathname->value[length] = '\0'; 360 tags->next = NULL; 361 362 tags->entries = hash_new(809, NULL); 363 tags->filenames = hash_new(31, NULL); 364 tags->patterns = hash_new(47, NULL); 365 366 /* Cache information */ 367 tags->tags = tags; /* :-) */ 368 tags->entry = NULL; 369 tags->offset = 0; 370 tags->textwindow = NULL; 371 tags->position = 0; 372 373 while (fgets(buffer, sizeof(buffer) - 1, file)) { 374 /* XXX Ignore malformed lines and tags file format information */ 375 if (isspace(buffer[0]) || buffer[0] == '!') 376 continue; 377 378 /* Symbol name */ 379 symbol = ptr = buffer; 380 while (*ptr && !isspace(*ptr)) 381 ptr++; 382 *ptr++ = '\0'; 383 while (isspace(*ptr)) 384 ptr++; 385 386 /* Filename with basename of tagsfile for symbol definition */ 387 filename = ptr; 388 while (*ptr && !isspace(*ptr)) 389 ptr++; 390 *ptr++ = '\0'; 391 while (isspace(*ptr)) 392 ptr++; 393 394 pattern = ptr; 395 /* Check for regex */ 396 if (*pattern == '/' || *pattern == '?') { 397 ptr++; 398 while (*ptr && *ptr != *pattern) { 399 if (*ptr == '\\') { 400 if (ptr[1] == *pattern || ptr[1] == '\\') { 401 /* XXX tags will escape pattern end, and backslash 402 * not sure about other special characters */ 403 memmove(ptr, ptr + 1, strlen(ptr)); 404 } 405 else { 406 ++ptr; 407 if (!*ptr) 408 break; 409 } 410 } 411 ptr++; 412 } 413 414 if (*ptr != *pattern) 415 continue; 416 ++pattern; 417 /* Will do a RE_NOSPEC search, that means ^ and $ 418 * would be literally search (do this to avoid escaping 419 * other regex characters and building a fast/simple literal 420 * string search pattern. 421 * Expect patterns to be full line */ 422 if (*pattern == '^' && ptr[-1] == '$') { 423 ++pattern; 424 --ptr; 425 } 426 } 427 /* Check for line number */ 428 else if (isdigit(*ptr)) { 429 while (isdigit(*ptr)) 430 ptr++; 431 } 432 /* Format not understood */ 433 else 434 continue; 435 436 *ptr = '\0'; 437 438 length = strlen(symbol); 439 entry = (TagsEntry *)hash_check(tags->entries, 440 symbol, length); 441 if (entry == NULL) { 442 entry = XtNew(TagsEntry); 443 entry->symbol = XtNew(hash_key); 444 entry->symbol->value = XtNewString(symbol); 445 entry->symbol->length = length; 446 entry->next = NULL; 447 entry->nentries = 0; 448 entry->filenames = NULL; 449 entry->patterns = NULL; 450 hash_put(tags->entries, (hash_entry *)entry); 451 } 452 453 length = strlen(filename); 454 file_entry = hash_check(tags->filenames, filename, length); 455 if (file_entry == NULL) { 456 file_entry = XtNew(hash_entry); 457 file_entry->key = XtNew(hash_key); 458 file_entry->key->value = XtNewString(filename); 459 file_entry->key->length = length; 460 file_entry->next = NULL; 461 hash_put(tags->filenames, file_entry); 462 } 463 464 if ((entry->nentries % 4) == 0) { 465 entry->filenames = (hash_entry **) 466 XtRealloc((char *)entry->filenames, 467 sizeof(hash_entry *) * 468 (entry->nentries + 4)); 469 entry->patterns = (char **) 470 XtRealloc((char *)entry->patterns, 471 sizeof(char *) * 472 (entry->nentries + 4)); 473 } 474 entry->filenames[entry->nentries] = file_entry; 475 entry->patterns[entry->nentries] = XtNewString(pattern); 476 ++entry->nentries; 477 } 478 fclose(file); 479 480 /* Add tags information to global hash table */ 481 hash_put(ht_tags, (hash_entry *)tags); 482 XeditPrintf("Tags file %s loaded\n", tagsfile); 483 } 484 else { 485 XeditPrintf("Failed to load tags file %s\n", tagsfile); 486 tags = NULL; 487 } 488 489 return (tags); 490 } 491 492 static void 493 FindTag(XeditTagsInfo *tags) 494 { 495 static String params[] = { "vertical", NULL }; 496 497 char buffer[BUFSIZ]; 498 char *pattern; 499 int length; 500 char *line; 501 char *text; 502 RegexEntry *regex; 503 re_mat match; 504 XawTextPosition position, left, right, last; 505 Widget source; 506 XawTextBlock block; 507 int size; 508 int lineno; 509 Boolean found; 510 xedit_flist_item *item; 511 Widget otherwindow; 512 513 XmuSnprintf(buffer, sizeof(buffer), "%s%s", tags->tags->pathname->value, 514 tags->entry->filenames[tags->offset]->key->value); 515 516 pattern = tags->entry->patterns[tags->offset]; 517 if (isdigit(*pattern)) { 518 lineno = atoi(pattern); 519 regex = NULL; 520 } 521 else { 522 lineno = 0; 523 length = strlen(pattern); 524 regex = (RegexEntry *)hash_check(tags->patterns, pattern, length); 525 if (regex == NULL) { 526 regex = XtNew(RegexEntry); 527 regex->pattern = XtNew(hash_key); 528 regex->pattern->value = XtNewString(pattern); 529 regex->pattern->length = length; 530 regex->next = NULL; 531 if (recomp(®ex->regex, pattern, RE_NOSUB | RE_NOSPEC)) { 532 XeditPrintf("Failed to compile regex %s\n", pattern); 533 Feep(); 534 return; 535 } 536 hash_put(tags->patterns, (hash_entry *)regex); 537 } 538 } 539 540 /* Short circuit to know if split horizontally */ 541 if (!XtIsManaged(texts[1])) 542 XtCallActionProc(textwindow, "split-window", NULL, params, 1); 543 544 /* Switch to "other" buffer */ 545 XtCallActionProc(textwindow, "other-window", NULL, NULL, 0); 546 547 /* This should print an error message if tags file cannot be read */ 548 if (!LoadFileInTextwindow(tags->incwd ? 549 tags->entry->filenames[tags->offset]->key->value : 550 buffer, buffer)) 551 return; 552 553 otherwindow = textwindow; 554 555 item = FindTextSource(XawTextGetSource(textwindow), NULL); 556 source = item->source; 557 left = XawTextSourceScan(source, 0, XawstAll, XawsdLeft, 1, True); 558 559 found = False; 560 561 if (lineno) { 562 right = RSCAN(left, lineno, False); 563 left = LSCAN(right, 1, False); 564 found = True; 565 } 566 else { 567 right = RSCAN(left, 1, True); 568 last = XawTextSourceScan(source, 0, XawstAll, XawsdRight, 1, True); 569 text = buffer; 570 571 size = sizeof(buffer); 572 for (;;) { 573 length = right - left; 574 match.rm_so = 0; 575 match.rm_eo = length; 576 XawTextSourceRead(source, left, &block, right - left); 577 if (block.length >= length) 578 line = block.ptr; 579 else { 580 if (length > size) { 581 if (text == buffer) 582 text = XtMalloc(length); 583 else 584 text = XtRealloc(text, length); 585 size = length; 586 } 587 line = text; 588 memcpy(line, block.ptr, block.length); 589 length = block.length; 590 for (position = left + length; 591 position < right; 592 position += block.length) { 593 XawTextSourceRead(source, position, &block, right - position); 594 memcpy(line + length, block.ptr, block.length); 595 length += block.length; 596 } 597 } 598 599 /* If not last line or if it ends in a newline */ 600 if (right < last || 601 (right > left && line[match.rm_eo - 1] == '\n')) { 602 --match.rm_eo; 603 length = match.rm_eo; 604 } 605 606 /* Accept as a match when matching the entire line, as the regex 607 * search pattern is optmized to not need to start with ^ and not 608 * need to end with $*/ 609 if (reexec(®ex->regex, line, 1, &match, RE_STARTEND) == 0 && 610 match.rm_eo > match.rm_so && 611 match.rm_so == 0 && match.rm_eo == length) { 612 right = left + match.rm_so + (match.rm_eo - match.rm_so); 613 found = True; 614 break; 615 } 616 else if (right >= last) { 617 XeditPrintf("Failed to match regex %s\n", pattern); 618 Feep(); 619 break; 620 } 621 else { 622 left = LSCAN(right + 1, 1, False); 623 right = RSCAN(left, 1, True); 624 } 625 } 626 627 if (text != buffer) 628 XtFree(text); 629 } 630 631 /* Switch back to editing buffer */ 632 XtCallActionProc(otherwindow, "other-window", NULL, NULL, 0); 633 634 if (found) { 635 if (source != XawTextGetSource(tags->textwindow) || 636 right < tags->position || left > tags->position) { 637 XawTextSetInsertionPoint(otherwindow, left); 638 XawTextSetSelection(otherwindow, left, right); 639 } 640 } 641 } 642