From b1dd01f67a48785678209b90c1b0e4f44621c146 Mon Sep 17 00:00:00 2001 From: Paulo Cesar Pereira de Andrade Date: Wed, 12 Mar 2008 21:53:48 -0300 Subject: Add a tags interface to xedit. To use the tags, first create a tags file with a command like "ctags -R". The interface can be disabled with resources, see the updated man page. Tag files are searched descending to the root directory. Multiple tags files are properly handled, and multiple symbol definitions can be searched. --- Makefile.am | 1 + Xedit-noxprint.ad | 5 +- tags.c | 635 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ util.c | 2 + xedit.c | 7 +- xedit.h | 8 + xedit.man | 8 + 7 files changed, 664 insertions(+), 2 deletions(-) create mode 100644 tags.c diff --git a/Makefile.am b/Makefile.am index cc57781..3a94ed0 100644 --- a/Makefile.am +++ b/Makefile.am @@ -113,6 +113,7 @@ xedit_SOURCES = \ ispell.c \ lisp.c \ options.c \ + tags.c \ util.c \ xedit.c \ xedit.h diff --git a/Xedit-noxprint.ad b/Xedit-noxprint.ad index 2b8bf25..0743f0b 100644 --- a/Xedit-noxprint.ad +++ b/Xedit-noxprint.ad @@ -39,7 +39,9 @@ Use Control-X,k to close file being edited.\n\ Use Control-X,o to switch to another splitted window.\n\ Use Control-X,u to undo. Control-G to switch between Undo and Redo.\n\ Use Insert to toggle Overwrite mode.\n\ -Use Control-G to interrupt the lisp subprocess +Use Control-G to interrupt the lisp subprocess.\n\ +Use Escape to enter or leave regex search and replace mode.\n\ +Use Alt-. to search tags for the selected symbol or find the next match. *formWindow*defaultDistance: 2 *formWindow.?.borderWidth: 0 @@ -201,6 +203,7 @@ mI: no-op(r) X,!u:undo()\n\ G: xedit-keyboard-reset()\n\ J: xedit-print-lisp-eval()\n\ +:m.: tags()\n\ Tab: insert-char()\n\ !l @Num_Lockb:insert-char()\n\ !l b: insert-char()\n\ diff --git a/tags.c b/tags.c new file mode 100644 index 0000000..b8f3970 --- /dev/null +++ b/tags.c @@ -0,0 +1,635 @@ +/* + * Copyright © 2007 Paulo César Pereira de Andrade + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Author: Paulo César Pereira de Andrade + */ + +/* + * Certain tag files may require quite some time and memory to load. + * Linux kernel 2.6x is an example, the tags file itself is almost 80Mb + * and xedit will use over 100Mb to store the data, and take quite some + * time to load it (and can grow drastically with every loaded files + * due to the memory used by the file contents and internal structures, + * like the syntax highlight ones). + * Possible workarounds could be to load the tags file in a separate + * process or thread. The memory problem would be hard to circunvent, + * as the tags file metadata would need to be stored in some very fast + * database, or at least some special format that would not require + * a linear search in a huge tags file. + */ + +#include "xedit.h" +#include "util.h" +#include "re.h" +#include + +/* + * Types + */ +typedef struct _TagsEntry TagsEntry; +typedef struct _RegexEntry RegexEntry; + +struct _TagsEntry { + hash_key *symbol; + TagsEntry *next; + + int nentries; + hash_entry **filenames; + char **patterns; +}; + +struct _RegexEntry { + hash_key *pattern; + RegexEntry *next; + + re_cod regex; +}; + +struct _XeditTagsInfo { + hash_key *pathname; + XeditTagsInfo *next; + + hash_table *entries; + hash_table *filenames; + hash_table *patterns; + + /* Used when searching for alternate tags and failing descending to + * root directory */ + Boolean visited; + + /* Flag to know if tags file is in xedit cwd and allow using relative + * pathnames when loading a file with some tag definition, so that + * other code will not fail to write file (or even worse, write to + * wrong file) if file is edited and tags is not in the current dir */ + Boolean incwd; + + /* Cache information for circulating over multiple definitions */ + XeditTagsInfo *tags; /* If trying another TagsInfo */ + TagsEntry *entry; /* Entry in tags->tags */ + int offset; + Widget textwindow; + XawTextPosition position; +}; + +/* + * Prototypes + */ +static XeditTagsInfo *LoadTagsFile(char *tagsfile); +static XeditTagsInfo *DoLoadTagsFile(char *tagsfile, int length); +static void FindTagFirst(XeditTagsInfo *tags, char *symbol, int length); +static void FindTagNext(XeditTagsInfo *tags, + Widget window, XawTextPosition position); +static void FindTag(XeditTagsInfo *tags); + +/* + * Initialization + */ +extern Widget texts[3]; +static hash_table *ht_tags; + +/* + * Implementation + */ +void +TagsAction(Widget w, XEvent *event, String *params, Cardinal *num_params) +{ + xedit_flist_item *item; + char buffer[1024]; + XawTextPosition position, left, right; + XawTextBlock block; + int length; + Widget source; + + source = XawTextGetSource(w); + item = FindTextSource(source, NULL); + if (item->tags == NULL) + SearchTagsFile(item); + + if (item->tags) { + position = XawTextGetInsertionPoint(w); + XawTextGetSelectionPos(w, &left, &right); + if (right > left) { + XawTextSourceRead(source, left, &block, right - left); + length = block.length + 1; + if (length >= sizeof(buffer)) + length = sizeof(buffer); + XmuSnprintf(buffer, length, "%s", block.ptr); + item->tags->textwindow = w; + item->tags->position = position; + FindTagFirst(item->tags, buffer, length - 1); + } + else + FindTagNext(item->tags, w, position); + } + else + Feep(); +} + +void +SearchTagsFile(xedit_flist_item *item) +{ + if (app_resources.loadTags) { + char buffer[BUFSIZ]; + char *ptr, *tagsfile; + int length; + Boolean exists; + FileAccess file_access; + + tagsfile = NULL; + + /* If path fully specified in resource */ + if (app_resources.tagsName[0] == '/') + tagsfile = ResolveName(app_resources.tagsName); + /* Descend up to root directory searching for a tags file */ + else { + /* *scratch* buffer */ + if (item->filename[0] != '/') { + ptr = ResolveName(app_resources.tagsName); + strncpy(buffer, ptr ? ptr : "", sizeof(buffer)); + } + else + strncpy(buffer, item->filename, sizeof(buffer)); + + /* Make sure buffer is nul terminated */ + buffer[sizeof(buffer) - 1] = '\0'; + ptr = buffer + strlen(buffer); + + for (;;) { + while (ptr > buffer && ptr[-1] != '/') + --ptr; + if (ptr <= buffer) + break; + length = ptr - buffer; + if (length >= sizeof(buffer)) + length = sizeof(buffer); + strncpy(ptr, app_resources.tagsName, + sizeof(buffer) - length); + buffer[sizeof(buffer) - 1] = '\0'; + + /* Check if tags filename exists */ + tagsfile = ResolveName(buffer); + if (tagsfile != NULL) { + file_access = CheckFilePermissions(tagsfile, &exists); + /* Check if can read tagsfile */ + if (exists && + (file_access == READ_OK || file_access == WRITE_OK)) + break; + else + tagsfile = NULL; + } + *--ptr = '\0'; + } + } + + if (tagsfile) + item->tags = LoadTagsFile(tagsfile); + else { + XeditPrintf("No tags file found." + " Run \"ctags -R\" to build a tags file.\n"); + item->tags = NULL; + } + } +} + +static void +FindTagFirst(XeditTagsInfo *tags, char *symbol, int length) +{ + char *ptr; + TagsEntry *entry; + char buffer[BUFSIZ]; + + /* Check for malformed parameters */ + ptr = symbol; + while (*ptr) { + if (*ptr == ' ' || *ptr == '\t' || *ptr == '\n' || *ptr == '\r' || + *ptr == '(' || *ptr == ')') { + Feep(); + return; + } + ptr++; + } + + /* First try in buffer tags */ + tags->tags = tags; + entry = (TagsEntry *)hash_check(tags->entries, symbol, length); + if (entry == NULL) { + /* Try to find in alternate tags */ + strncpy(buffer, tags->pathname->value, tags->pathname->length); + buffer[tags->pathname->length] = '\0'; + ptr = buffer + tags->pathname->length - 1; + + for (tags->tags = (XeditTagsInfo *)hash_iter_first(ht_tags); + tags->tags; + tags->tags = (XeditTagsInfo *)hash_iter_next(ht_tags)) + tags->tags->visited = False; + + tags->visited = True; + + while (ptr > buffer && entry == NULL) { + --ptr; + while (ptr > buffer && ptr[-1] != '/') + --ptr; + if (ptr <= buffer) + break; + *ptr = '\0'; + + /* Try an upper directory tags */ + tags->tags = (XeditTagsInfo *) + hash_check(ht_tags, buffer, ptr - buffer); + if (tags->tags) { + tags->tags->visited = True; + entry = (TagsEntry *) + hash_check(tags->tags->entries, symbol, length); + } + } + + /* If still failed, check other available tags + * for possible different projects */ + if (entry == NULL) { + for (tags->tags = (XeditTagsInfo *)hash_iter_first(ht_tags); + tags->tags; + tags->tags = (XeditTagsInfo *)hash_iter_next(ht_tags)) { + if (tags->tags->visited == False) { + entry = (TagsEntry *) + hash_check(tags->tags->entries, symbol, length); + /* Stop on first match */ + if (entry != NULL) + break; + } + } + } + + if (entry == NULL) { + XeditPrintf("Symbol %s not in tags\n", symbol); + Feep(); + return; + } + } + + tags->entry = entry; + tags->offset = 0; + + FindTag(tags); +} + +static void +FindTagNext(XeditTagsInfo *tags, Widget window, XawTextPosition position) +{ + if (window != tags->textwindow || position != tags->position) + Feep(); + else { + if (tags->entry->nentries > 1) { + if (++tags->offset >= tags->entry->nentries) + tags->offset = 0; + FindTag(tags); + } + else + Feep(); + } +} + +static XeditTagsInfo * +LoadTagsFile(char *tagsfile) +{ + XeditTagsInfo *tags; + int length; + + if (ht_tags == NULL) + ht_tags = hash_new(11, NULL); + + /* tags key is only the directory name with ending '/' */ + length = strlen(tagsfile) - strlen(app_resources.tagsName); + tags = (XeditTagsInfo *)hash_check(ht_tags, tagsfile, length); + + return (tags ? tags : DoLoadTagsFile(tagsfile, length)); +} + +static XeditTagsInfo * +DoLoadTagsFile(char *tagsfile, int length) +{ + char *ptr; + FILE *file; + XeditTagsInfo *tags; + TagsEntry *entry; + hash_entry *file_entry; + char buffer[BUFSIZ]; + char *symbol, *filename, *pattern; + + file = fopen(tagsfile, "r"); + if (file) { + char *cwd; + + tags = XtNew(XeditTagsInfo); + + cwd = getcwd(buffer, sizeof(buffer)); + tags->incwd = cwd && + (strlen(cwd) == length - 1 && + memcmp(cwd, tagsfile, length - 1) == 0); + + /* Build pathname as a nul terminated directory specification string */ + tags->pathname = XtNew(hash_key); + tags->pathname->value = XtMalloc(length + 1); + tags->pathname->length = length; + memcpy(tags->pathname->value, tagsfile, length); + tags->pathname->value[length] = '\0'; + tags->next = NULL; + + tags->entries = hash_new(809, NULL); + tags->filenames = hash_new(31, NULL); + tags->patterns = hash_new(47, NULL); + + /* Cache information */ + tags->tags = tags; /* :-) */ + tags->entry = NULL; + tags->offset = 0; + tags->textwindow = NULL; + tags->position = 0; + + while (fgets(buffer, sizeof(buffer) - 1, file)) { + /* XXX Ignore malformed lines and tags file format information */ + if (isspace(buffer[0]) || buffer[0] == '!') + continue; + + /* Symbol name */ + symbol = ptr = buffer; + while (*ptr && !isspace(*ptr)) + ptr++; + *ptr++ = '\0'; + while (isspace(*ptr)) + ptr++; + + /* Filename with basename of tagsfile for symbol definition */ + filename = ptr; + while (*ptr && !isspace(*ptr)) + ptr++; + *ptr++ = '\0'; + while (isspace(*ptr)) + ptr++; + + pattern = ptr; + /* Check for regex */ + if (*pattern == '/' || *pattern == '?') { + ptr++; + while (*ptr && *ptr != *pattern) { + if (*ptr == '\\') { + if (ptr[1] == *pattern || ptr[1] == '\\') { + /* XXX tags will escape pattern end, and backslash + * not sure about other special characters */ + memmove(ptr, ptr + 1, strlen(ptr)); + } + else { + ++ptr; + if (!*ptr) + break; + } + } + ptr++; + } + + if (*ptr != *pattern) + continue; + ++pattern; + /* Will do a RE_NOSPEC search, that means ^ and $ + * would be literally search (do this to avoid escaping + * other regex characters and building a fast/simple literal + * string search pattern. + * Expect patterns to be full line */ + if (*pattern == '^' && ptr[-1] == '$') { + ++pattern; + --ptr; + } + } + /* Check for line number */ + else if (isdigit(*ptr)) { + while (isdigit(*ptr)) + ptr++; + } + /* Format not understood */ + else + continue; + + *ptr = '\0'; + + length = strlen(symbol); + entry = (TagsEntry *)hash_check(tags->entries, + symbol, length); + if (entry == NULL) { + entry = XtNew(TagsEntry); + entry->symbol = XtNew(hash_key); + entry->symbol->value = XtNewString(symbol); + entry->symbol->length = length; + entry->next = NULL; + entry->nentries = 0; + entry->filenames = NULL; + entry->patterns = NULL; + hash_put(tags->entries, (hash_entry *)entry); + } + + length = strlen(filename); + file_entry = hash_check(tags->filenames, filename, length); + if (file_entry == NULL) { + file_entry = XtNew(hash_entry); + file_entry->key = XtNew(hash_key); + file_entry->key->value = XtNewString(filename); + file_entry->key->length = length; + file_entry->next = NULL; + hash_put(tags->filenames, file_entry); + } + + if ((entry->nentries % 4) == 0) { + entry->filenames = (hash_entry **) + XtRealloc((char *)entry->filenames, + sizeof(hash_entry *) * + (entry->nentries + 4)); + entry->patterns = (char **) + XtRealloc((char *)entry->patterns, + sizeof(char *) * + (entry->nentries + 4)); + } + entry->filenames[entry->nentries] = file_entry; + entry->patterns[entry->nentries] = XtNewString(pattern); + ++entry->nentries; + } + fclose(file); + + /* Add tags information to global hash table */ + hash_put(ht_tags, (hash_entry *)tags); + XeditPrintf("Tags file %s loaded\n", tagsfile); + } + else { + XeditPrintf("Failed to load tags file %s\n", tagsfile); + tags = NULL; + } + + return (tags); +} + +static void +FindTag(XeditTagsInfo *tags) +{ + static String params[] = { "vertical", NULL }; + + char buffer[BUFSIZ]; + char *pattern; + int length; + char *line; + char *text; + RegexEntry *regex; + re_mat match; + XawTextPosition position, left, right, last; + Widget source; + XawTextBlock block; + int size; + int lineno; + Boolean found; + xedit_flist_item *item; + Widget otherwindow; + + XmuSnprintf(buffer, sizeof(buffer), "%s%s", tags->tags->pathname->value, + tags->entry->filenames[tags->offset]->key->value); + + pattern = tags->entry->patterns[tags->offset]; + if (isdigit(*pattern)) { + lineno = atoi(pattern); + regex = NULL; + } + else { + lineno = 0; + length = strlen(pattern); + regex = (RegexEntry *)hash_check(tags->patterns, pattern, length); + if (regex == NULL) { + regex = XtNew(RegexEntry); + regex->pattern = XtNew(hash_key); + regex->pattern->value = XtNewString(pattern); + regex->pattern->length = length; + regex->next = NULL; + if (recomp(®ex->regex, pattern, RE_NOSUB | RE_NOSPEC)) { + XeditPrintf("Failed to compile regex %s\n", pattern); + Feep(); + return; + } + hash_put(tags->patterns, (hash_entry *)regex); + } + } + + /* Short circuit to know if split horizontally */ + if (!XtIsManaged(texts[1])) + XtCallActionProc(textwindow, "split-window", NULL, params, 1); + + /* Switch to "other" buffer */ + XtCallActionProc(textwindow, "other-window", NULL, NULL, 0); + + /* This should print an error message if tags file cannot be read */ + if (!LoadFileInTextwindow(tags->incwd ? + tags->entry->filenames[tags->offset]->key->value : + buffer, buffer)) + return; + + otherwindow = textwindow; + + item = FindTextSource(XawTextGetSource(textwindow), NULL); + source = item->source; + left = XawTextSourceScan(source, 0, XawstAll, XawsdLeft, 1, True); + + found = False; + + if (lineno) { + right = RSCAN(left, lineno, False); + left = LSCAN(right, 1, False); + found = True; + } + else { + right = RSCAN(left, 1, True); + last = XawTextSourceScan(source, 0, XawstAll, XawsdRight, 1, True); + text = buffer; + + size = sizeof(buffer); + for (;;) { + length = right - left; + match.rm_so = 0; + match.rm_eo = length; + XawTextSourceRead(source, left, &block, right - left); + if (block.length >= length) + line = block.ptr; + else { + if (length > size) { + if (text == buffer) + text = XtMalloc(length); + else + text = XtRealloc(text, length); + size = length; + } + line = text; + memcpy(line, block.ptr, block.length); + length = block.length; + for (position = left + length; + position < right; + position += block.length) { + XawTextSourceRead(source, position, &block, right - position); + memcpy(line + length, block.ptr, block.length); + length += block.length; + } + } + + /* If not last line or if it ends in a newline */ + if (right < last || + (right > left && line[match.rm_eo - 1] == '\n')) { + --match.rm_eo; + length = match.rm_eo; + } + + /* Accept as a match when matching the entire line, as the regex + * search pattern is optmized to not need to start with ^ and not + * need to end with $*/ + if (reexec(®ex->regex, line, 1, &match, RE_STARTEND) == 0 && + match.rm_eo > match.rm_so && + match.rm_so == 0 && match.rm_eo == length) { + right = left + match.rm_so + (match.rm_eo - match.rm_so); + found = True; + break; + } + else if (right >= last) { + XeditPrintf("Failed to match regex %s\n", pattern); + Feep(); + break; + } + else { + left = LSCAN(right + 1, 1, False); + right = RSCAN(left, 1, True); + } + } + + if (text != buffer) + XtFree(text); + } + + /* Switch back to editing buffer */ + XtCallActionProc(otherwindow, "other-window", NULL, NULL, 0); + + if (found) { + if (source != XawTextGetSource(tags->textwindow) || + right < tags->position || left > tags->position) { + XawTextSetInsertionPoint(otherwindow, left); + XawTextSetSelection(otherwindow, left, right); + } + } +} diff --git a/util.c b/util.c index 1657697..60b5078 100644 --- a/util.c +++ b/util.c @@ -246,6 +246,8 @@ AddTextSource(Widget source, char *name, char *filename, int flags, item->mode = 0; item->properties = NULL; item->xldata = NULL; + /* Try to load associated tags file */ + SearchTagsFile(item); flist.itens = (xedit_flist_item**) XtRealloc((char*)flist.itens, sizeof(xedit_flist_item*) diff --git a/xedit.c b/xedit.c index f71b696..7917d23 100644 --- a/xedit.c +++ b/xedit.c @@ -60,7 +60,8 @@ static XtActionsRec actions[] = { {"xedit-keyboard-reset",XeditKeyboardReset}, #endif {"ispell", IspellAction}, -{"line-edit", LineEditAction} +{"line-edit", LineEditAction}, +{"tags", TagsAction} }; #define DEF_HINT_INTERVAL 300 /* in seconds, 5 minutes */ @@ -109,6 +110,10 @@ static XtResource resources[] = { Offset(position_format), XtRString, "L%l"}, {"autoReplace", "Replace", XtRString, sizeof(char*), Offset(auto_replace), XtRImmediate, NULL}, + {"tagsName", "TagsName", XtRString, sizeof(char *), + Offset(tagsName), XtRString, "tags"}, + {"loadTags", "LoadTags", XtRBoolean, sizeof(Boolean), + Offset(loadTags), XtRImmediate, (XtPointer)TRUE}, }; #undef Offset diff --git a/xedit.h b/xedit.h index 1c86f6b..326071e 100644 --- a/xedit.h +++ b/xedit.h @@ -66,6 +66,7 @@ typedef struct _xedit_hints { typedef enum {NO_READ, READ_OK, WRITE_OK} FileAccess; typedef struct _XeditLispData XeditLispData; +typedef struct _XeditTagsInfo XeditTagsInfo; #define CHANGED_BIT 0x01 #define EXISTS_BIT 0x02 @@ -81,6 +82,7 @@ typedef struct _xedit_flist_item { XawTextPropertyList *properties; XawTextWrapMode wrap; XeditLispData *xldata; + XeditTagsInfo *tags; } xedit_flist_item; extern struct _xedit_flist { @@ -101,6 +103,8 @@ extern struct _app_resources { char *changed_pixmap_name; char *position_format; char *auto_replace; + char *tagsName; + Boolean loadTags; } app_resources; extern Widget topwindow, textwindow, labelwindow, filenamewindow, messwidget; @@ -177,6 +181,10 @@ void UnsetTextProperties(xedit_flist_item*); void CreateEditModePopup(Widget); void SetEditModeMenu(void); +/* tags.c */ +void TagsAction(Widget, XEvent*, String*, Cardinal*); +void SearchTagsFile(xedit_flist_item *item); + /* externs for system replacement functions */ #ifdef NEED_STRCASECMP int strcasecmp(const char *s1, const char *s2); diff --git a/xedit.man b/xedit.man index 0c2f540..3491bd1 100644 --- a/xedit.man +++ b/xedit.man @@ -245,6 +245,14 @@ of the this button and displays it in the Edit window. .SH RESOURCES For \fIxedit\fP the available resources are: .TP 8 +.B tagsName (\fPClass\fB TagsName) +Specifies the name of the tags file to search when loading a new file. +Default value is \fItags\fP. +.TP 8 +.B loadTags (\fPClass\fB LoadTags) +Boolean value to enable or disabling searching for tags files. +Default is \fITrue\fP. +.TP 8 .B enableBackups (\fPClass\fB EnableBackups) Specifies that, when edits made to an existing file are saved, .I xedit -- cgit v1.2.3