Home | History | Annotate | Line # | Download | only in dist
      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(&regex->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(&regex->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