diff options
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | Xedit-noxprint.ad | 5 | ||||
-rw-r--r-- | tags.c | 635 | ||||
-rw-r--r-- | util.c | 2 | ||||
-rw-r--r-- | xedit.c | 7 | ||||
-rw-r--r-- | xedit.h | 8 | ||||
-rw-r--r-- | xedit.man | 8 |
7 files changed, 664 insertions, 2 deletions
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 @@ m<Key>I: no-op(r) <Ctrl>X,!<Key>u:undo()\n\ <Ctrl>G: xedit-keyboard-reset()\n\ <Ctrl>J: xedit-print-lisp-eval()\n\ +:m<Key>.: tags()\n\ <Key>Tab: insert-char()\n\ !l @Num_Lock<Key>b:insert-char()\n\ !l <Key>b: insert-char()\n\ @@ -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 <unistd.h> + +/* + * 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); + } + } +} @@ -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*) @@ -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 @@ -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); @@ -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 |