/* vi:set sts=4 sw=4:
 *
 * VIM - Vi IMproved	by Bram Moolenaar
 *
 * Do ":help uganda"  in Vim to read copying and usage conditions.
 * Do ":help credits" in Vim to see a list of people who contributed.
 */

/*
 * Code to handle tags and the tag stack
 */

#include "vim.h"
#include "globals.h"
#include "proto.h"
#include "option.h"

struct tag_pointers
{
    char_u	*tagname;	/* start of tag name (skip "file:") */
    char_u	*tagname_end;	/* char after tag name */
    char_u	*fname;		/* first char of file name */
    char_u	*fname_end;	/* char after file name */
    char_u	*command;	/* first char of command */
};

static int get_tagfname __ARGS((int, char_u *));

#ifdef EMACS_TAGS
static int parse_tag_line __ARGS((char_u *, int, struct tag_pointers *));
static int jumpto_tag __ARGS((char_u *, int, char_u *, char_u *, int));
#else
static int parse_tag_line __ARGS((char_u *, struct tag_pointers *));
static int jumpto_tag __ARGS((char_u *, char_u *, int));
#endif
static int test_for_static __ARGS((struct tag_pointers *));
static char_u *expand_rel_name __ARGS((char_u *fname, char_u *tag_fname));
#ifdef EMACS_TAGS
static int test_for_current __ARGS((int, char_u *, char_u *, char_u *));
#else
static int test_for_current __ARGS((char_u *, char_u *, char_u *));
#endif

static char_u *bottommsg = (char_u *)"at bottom of tag stack";
static char_u *topmsg = (char_u *)"at top of tag stack";

/*
 * Jump to tag; handling of tag stack
 *
 * *tag != NUL (:tag): jump to new tag, add to tag stack
 * type == 1 (:pop) || type == 2 (CTRL-T): jump to old position
 * type == 0 (:tag): jump to old tag
 */
    void
do_tag(tag, type, count, forceit)
    char_u  *tag;
    int	    type;
    int	    count;
    int	    forceit;	    /* :ta with ! */
{
    int		    i;
    struct taggy    *tagstack = curwin->w_tagstack;
    int		    tagstackidx = curwin->w_tagstackidx;
    int		    tagstacklen = curwin->w_tagstacklen;
    int		    oldtagstackidx = tagstackidx;

    if (*tag != NUL)			    /* new pattern, add to the stack */
    {
	/*
	 * if last used entry is not at the top, delete all tag stack entries
	 * above it.
	 */
	while (tagstackidx < tagstacklen)
	    vim_free(tagstack[--tagstacklen].tagname);

		/* if tagstack is full: remove oldest entry */
	if (++tagstacklen > TAGSTACKSIZE)
	{
	    tagstacklen = TAGSTACKSIZE;
	    vim_free(tagstack[0].tagname);
	    for (i = 1; i < tagstacklen; ++i)
		tagstack[i - 1] = tagstack[i];
	    --tagstackidx;
	}

	/*
	 * put the tag name in the tag stack
	 * the position is added below
	 */
	tagstack[tagstackidx].tagname = vim_strsave(tag);
    }
    else if (tagstacklen == 0)			/* empty stack */
    {
	EMSG("tag stack empty");
	goto end_do_tag;
    }
    else if (type)				/* go to older position */
    {
	if ((tagstackidx -= count) < 0)
	{
	    emsg(bottommsg);
	    if (tagstackidx + count == 0)
	    {
		/* We did ^T (or <num>^T) from the bottom of the stack */
		tagstackidx = 0;
		goto end_do_tag;
	    }
	    /* We weren't at the bottom of the stack, so jump all the way to
	     * the bottom.
	     */
	    tagstackidx = 0;
	}
	else if (tagstackidx >= tagstacklen)	/* must have been count == 0 */
	{
	    emsg(topmsg);
	    goto end_do_tag;
	}
	if (tagstack[tagstackidx].fmark.fnum != curbuf->b_fnum)
	{
	    /*
	     * Jump to other file. If this fails (e.g. because the file was
	     * changed) keep original position in tag stack.
	     */
	    if (buflist_getfile(tagstack[tagstackidx].fmark.fnum,
		    tagstack[tagstackidx].fmark.mark.lnum,
		    GETF_SETMARK, forceit) == FAIL)
	    {
		tagstackidx = oldtagstackidx;	/* back to old position */
		goto end_do_tag;
	    }
	}
	else
	    curwin->w_cursor.lnum = tagstack[tagstackidx].fmark.mark.lnum;
	curwin->w_cursor.col = tagstack[tagstackidx].fmark.mark.col;
	curwin->w_set_curswant = TRUE;
	goto end_do_tag;
    }
    else				    /* go to newer pattern */
    {
	if ((tagstackidx += count - 1) >= tagstacklen)
	{
	    /*
	     * beyond the last one, just give an error message and go to the
	     * last one
	     */
	    tagstackidx = tagstacklen - 1;
	    emsg(topmsg);
	}
	else if (tagstackidx < 0)	    /* must have been count == 0 */
	{
	    emsg(bottommsg);
	    tagstackidx = 0;
	    goto end_do_tag;
	}
    }
    /*
     * For :tag [arg], remember position before the jump
     */
    if (type == 0)
    {
	tagstack[tagstackidx].fmark.mark = curwin->w_cursor;
	tagstack[tagstackidx].fmark.fnum = curbuf->b_fnum;
    }
    /* curwin will change in the call to find_tags() if ^W^] was used -- webb */
    curwin->w_tagstackidx = tagstackidx;
    curwin->w_tagstacklen = tagstacklen;
    if (find_tags(tagstack[tagstackidx].tagname, NULL, NULL, NULL,
							  FALSE, forceit) > 0)
	++tagstackidx;

end_do_tag:
    curwin->w_tagstackidx = tagstackidx;
    curwin->w_tagstacklen = tagstacklen;
}

/*
 * Print the tag stack
 */
    void
do_tags()
{
    int		    i;
    char_u	    *name;
    struct taggy    *tagstack = curwin->w_tagstack;
    int		    tagstackidx = curwin->w_tagstackidx;
    int		    tagstacklen = curwin->w_tagstacklen;

    /* Highlight title */
    MSG_PUTS_TITLE("\n  # TO tag      FROM line in file");
    for (i = 0; i < tagstacklen; ++i)
    {
	if (tagstack[i].tagname != NULL)
	{
	    name = fm_getname(&(tagstack[i].fmark));
	    if (name == NULL)	    /* file name not available */
		continue;

	    msg_putchar('\n');
	    sprintf((char *)IObuff, "%c%2d %-15s %4ld  %s",
		i == tagstackidx ? '>' : ' ',
		i + 1,
		tagstack[i].tagname,
		tagstack[i].fmark.mark.lnum,
		name);
	    msg_outtrans(IObuff);
	    vim_free(name);
	}
	flushbuf();		    /* show one line at a time */
    }
    if (tagstackidx == tagstacklen)	/* idx at top of stack */
	MSG_PUTS("\n>");
}

/*
 * find_tags() - goto tag or search for tags in tags files
 *
 * If "tag" is not NULL, search for a single tag and jump to it.
 *   return FAIL for failure, OK for success
 * If "tag" is NULL, find all tags matching the regexp given with 'prog'.
 *   return FAIL if search completely failed, OK otherwise.
 *
 * There is a priority in which type of tag is recognized. It is computed
 * from the PRI_ defines below.
 *
 *  6.	A static or global tag with a full matching tag for the current file.
 *  5.	A global tag with a full matching tag for another file.
 *  4.	A static tag with a full matching tag for another file.
 *  2.	A static or global tag with an ignore-case matching tag for the
 *	current file.
 *  1.	A global tag with an ignore-case matching tag for another file.
 *  0.	A static tag with an ignore-case matching tag for another file.
 *
 *  Tags in an emacs-style tags file are always global.
 */
#define PRI_GLOBAL	    1	    /* global or emacs tag */
#define PRI_CURRENT	    2	    /* tag for current file */
#define PRI_FULL_MATCH	    4	    /* case of tag matches */

    int
find_tags(tag, prog, num_file, file, help_only, forceit)
    char_u	*tag;		    /* NULL or tag to search for */
    vim_regexp	*prog;		    /* regexp program or NULL */
    int		*num_file;	    /* return value: number of matches found */
    char_u	***file;	    /* return value: array of matches found */
    int		help_only;	    /* if TRUE: only tags for help files */
    int		forceit;	    /* :ta with ! */
{
    FILE       *fp;
    char_u     *lbuf;			/* line buffer */
    char_u     *tag_fname;		/* name of tag file */
    int		first_file;		/* trying first tag file */
    struct tag_pointers tagp;
    int		did_open = FALSE;	/* did open a tag file */
    int		stop_searching = FALSE;	/* stop when match found or error */
    int		retval = FAIL;		/* return value */
    int		is_static;		/* current tag line is static */
    int		is_current;		/* file name matches */
    char_u	*p;
#ifdef EMACS_TAGS
    char_u     *ebuf;			/* aditional buffer for etag fname */
    int		is_etag;		/* current file is emaces style */
#endif

/*
 * Variables used only when "tag" != NULL
 */
    int		taglen = 0;		/* init for GCC */
    int		cmplen;
    int		full_match;
    int		icase_match;
    int		priority;		/* priority of current line */

    char_u	*bestmatch_line = NULL;	/* saved line for best match found so
					    far */
    char_u	*bestmatch_tag_fname = NULL;
					/* copy of tag_fname for best match */
    int		bestmatch_priority = 0; /* best match priority */

#ifdef EMACS_TAGS
    /*
     * Stack for included emacs-tags file.
     * It has a fixed size, to truncate cyclic includes. jw
     */
# define INCSTACK_SIZE 42
    struct
    {
	FILE	*fp;
	char_u	*tag_fname;
    } incstack[INCSTACK_SIZE];

    int		incstack_idx = 0;	    /* index in incstack */
    char_u     *bestmatch_ebuf = NULL;	    /* copy of ebuf for best match */
    int		bestmatch_is_etag = FALSE;  /* copy of is_etag for best match */
#endif

/*
 * Variables used when "tag" == NULL
 */
    char_u  **matches = NULL;		/* array of matches found */
    char_u  **new_matches;
    int	    match_limit = 100;		/* max. number of matches stored */
    int	    match_count = 0;		/* number of matches found */
    int	    i;
    int	    help_save;

    help_save = curbuf->b_help;
/*
 * Allocate memory for the buffers that are used
 */
    lbuf = alloc(LSIZE);
#ifdef EMACS_TAGS
    ebuf = alloc(LSIZE);
#endif
    tag_fname = alloc(LSIZE + 1);
				    /* check for out of memory situation */
    if ((tag == NULL && prog == NULL) || lbuf == NULL || tag_fname == NULL
#ifdef EMACS_TAGS
							 || ebuf == NULL
#endif
									)
	goto findtag_end;
    if (tag == NULL)
    {
	matches = (char_u **)alloc((unsigned)(match_limit * sizeof(char_u *)));
	if (matches == NULL)
	    goto findtag_end;
    }

/*
 * Initialize a few variables
 */
    if (tag != NULL)
    {
	taglen = STRLEN(tag);
	if (p_tl != 0 && taglen > p_tl)	    /* adjust for 'taglength' */
	    taglen = p_tl;
    }
    else if (help_only)		    /* want tags from help file */
	curbuf->b_help = TRUE;

/*
 * Try tag file names from tags option one by one.
 */
    for (first_file = TRUE; get_tagfname(first_file, tag_fname) == OK;
							    first_file = FALSE)
    {
	/*
	 * A file that doesn't exist is silently ignored.
	 */
	if ((fp = fopen((char *)tag_fname, "r")) == NULL)
	    continue;
	did_open = TRUE;    /* remember that we found at least one file */

#ifdef EMACS_TAGS
	is_etag = 0;	    /* default is: not emacs style */
#endif
	/*
	 * Read and parse the lines in the file one by one
	 */
	while (!got_int)
	{
	    line_breakcheck();

	    if (vim_fgets(lbuf, LSIZE, fp))
#ifdef EMACS_TAGS
		if (incstack_idx)	/* this was an included file */
		{
		    --incstack_idx;
		    fclose(fp);		/* end of this file ... */
		    fp = incstack[incstack_idx].fp;
		    STRCPY(tag_fname, incstack[incstack_idx].tag_fname);
		    vim_free(incstack[incstack_idx].tag_fname);
		    is_etag = 1;	/* (only etags can include) */
		    continue;		/* ... continue with parent file */
		}
		else
#endif
		    break;			    /* end of file */

#ifdef EMACS_TAGS
	    /*
	     * Emacs tags line with CTRL-L: New file name on next line.
	     * The file name is followed by a ','.
	     */
	    if (*lbuf == Ctrl('L'))	/* remember etag file name in ebuf */
	    {
		is_etag = 1;
		if (!vim_fgets(ebuf, LSIZE, fp))
		{
		    for (p = ebuf; *p && *p != ','; p++)
			;
		    *p = NUL;

		    /*
		     * atoi(p+1) is the number of bytes before the next ^L
		     * unless it is an include statement.
		     */
		    if (STRNCMP(p + 1, "include", 7) == 0 &&
						 incstack_idx < INCSTACK_SIZE)
		    {
			if ((incstack[incstack_idx].tag_fname =
					      vim_strsave(tag_fname)) != NULL)
			{
			    incstack[incstack_idx].fp = fp;
			    if ((fp = fopen((char *)ebuf, "r")) == NULL)
			    {
				fp = incstack[incstack_idx].fp;
				vim_free(incstack[incstack_idx].tag_fname);
			    }
			    else
			    {
				STRCPY(tag_fname, ebuf);
				++incstack_idx;
			    }
			    is_etag = 0;    /* we can include anything */
			}
		    }
		}
		continue;
	    }
#endif

	    /*
	     * Figure out where the different strings are in this line.
	     * For "normal" tags: Do a quick check if the tag matches.
	     * This speeds up tag searching a lot!
	     */
	    if (tag != NULL
#ifdef EMACS_TAGS
			    && !is_etag
#endif
					)
	    {
		tagp.tagname = lbuf;
		tagp.tagname_end = skiptowhite(lbuf);

#ifdef OLD_STATIC_TAGS
		/*
		 * Check for old style static tag: "file:tag file .."
		 */
		tagp.fname = NULL;
		for (p = lbuf; p < tagp.tagname_end; ++p)
		{
		    if (*p == ':')
		    {
			if (tagp.fname == NULL)
			    tagp.fname = skipwhite(tagp.tagname_end);
			if (	   fnamencmp(lbuf, tagp.fname, p - lbuf) == 0
				&& vim_iswhite(tagp.fname[p - lbuf]))
			{
			    /* found one */
			    tagp.tagname = p + 1;
			    break;
			}
		    }
		}
#endif

		/*
		 * Skip this line if the length of the tag is different.
		 */
		cmplen = tagp.tagname_end - tagp.tagname;
		if (p_tl != 0 && cmplen > p_tl)	    /* adjust for 'taglength' */
		    cmplen = p_tl;
		if (taglen != cmplen)
		    continue;

		/*
		 * Skip this line if the tag does not match (ignoring case).
		 */
		if (STRNICMP(tagp.tagname, tag, cmplen))
		    continue;

		/*
		 * This is a useful tag, isolate the file name and command.
		 */
		if (tagp.fname == NULL)
		    tagp.fname = skipwhite(tagp.tagname_end);
		tagp.fname_end = skiptowhite(tagp.fname);
		tagp.command = skipwhite(tagp.fname_end);
		if (*tagp.command == NUL)
		    i = FAIL;
		else
		    i = OK;
	    }
	    else
		i = parse_tag_line(lbuf,
#ifdef EMACS_TAGS
				       is_etag,
#endif
					       &tagp);
	    if (i == FAIL)
	    {
		EMSG2("Format error in tags file \"%s\"", tag_fname);
		stop_searching = TRUE;
		break;
	    }

#ifdef EMACS_TAGS
	    is_static = FALSE;
	    if (!is_etag)	/* emacs tags are never static */
#endif
	    {
		if (tagp.tagname != lbuf)	/* detected static tag before */
		    is_static = TRUE;
		else
		    is_static = test_for_static(&tagp);
	    }
#ifdef EMACS_TAGS
	    if (is_etag)
		tagp.fname = ebuf;
#endif
	    /*
	     * "tag" == NULL: find tags matching regexp "prog"
	     */
	    if (tag == NULL)
	    {
		*tagp.tagname_end = NUL;
		if (vim_regexec(prog, tagp.tagname, TRUE))
		{
		    is_current = test_for_current(
#ifdef EMACS_TAGS
			    is_etag,
#endif
				     tagp.fname, tagp.fname_end, tag_fname);
		    if (!is_static || is_current)
		    {
			/*
			 * Found a match, add it to matches[].
			 * May need to make matches[] larger.
			 */
			if (match_count == match_limit)
			{
			    match_limit += 100;
			    new_matches = (char_u **)alloc(
				    (unsigned)(match_limit * sizeof(char_u *)));
			    if (new_matches == NULL)
			    {
				/* Out of memory! Just forget about the rest
				 * of the matches. */
				retval = OK;
				stop_searching = TRUE;
				break;
			    }
			    for (i = 0; i < match_count; i++)
				new_matches[i] = matches[i];
			    vim_free(matches);
			    matches = new_matches;
			}
			if (help_only)
			{
			    int	    len;

			    /*
			     * Append the help-heuristic number after the
			     * tagname, for sorting it later.
			     */
			    len = STRLEN(tagp.tagname);
			    p = alloc(len + 10);
			    if (p != NULL)
			    {
				STRCPY(p, tagp.tagname);
				sprintf((char *)p + len + 1, "%06d",
						  help_heuristic(tagp.tagname,
				      (int)(prog->startp[0] - tagp.tagname)));
			    }
			    matches[match_count++] = p;
			}
			else
			    matches[match_count++] = vim_strsave(tagp.tagname);
		    }
		}
	    }
	    /*
	     * "tag" != NULL: find tag and jump to it
	     */
	    else
	    {
		/*
		 * If tag length does not match, skip the rest
		 */
		cmplen = tagp.tagname_end - tagp.tagname;
		if (p_tl != 0 && cmplen > p_tl)	    /* adjust for 'taglength' */
		    cmplen = p_tl;
		if (taglen == cmplen)
		{
		    /*
		     * Check for match (ignoring case).
		     */
		    icase_match = (STRNICMP(tagp.tagname, tag, cmplen) == 0);
		    if (icase_match)	/* Tag matches somehow */
		    {
			/*
			 * If it's a full match for the current file, jump to
			 * it now.
			 */
			full_match = (STRNCMP(tagp.tagname, tag, cmplen) == 0);
			is_current = test_for_current(
#ifdef EMACS_TAGS
				is_etag,
#endif
					 tagp.fname, tagp.fname_end, tag_fname);
			if (full_match && is_current)
			{
			    retval = jumpto_tag(lbuf,
#ifdef EMACS_TAGS
						    is_etag, ebuf,
#endif
							  tag_fname, forceit);
			    stop_searching = TRUE;
			    break;
			}

			/*
			 * If the priority of the current line is higher than
			 * the best match so far, store it as the best match
			 */
			if (full_match)
			    priority = PRI_FULL_MATCH;
			else
			    priority = 0;
			if (is_current)
			    priority += PRI_CURRENT;
			if (!is_static)
			    priority += PRI_GLOBAL;

			if (priority > bestmatch_priority)
			{
			    vim_free(bestmatch_line);
			    bestmatch_line = vim_strsave(lbuf);
			    vim_free(bestmatch_tag_fname);
			    bestmatch_tag_fname = vim_strsave(tag_fname);
			    bestmatch_priority = priority;
#ifdef EMACS_TAGS
			    bestmatch_is_etag = is_etag;
			    if (is_etag)
			    {
				vim_free(bestmatch_ebuf);
				bestmatch_ebuf = vim_strsave(ebuf);
			    }
#endif
			}
		    }
		}
	    }
	}
	fclose(fp);
#ifdef EMACS_TAGS
	while (incstack_idx)
	{
	    --incstack_idx;
	    fclose(incstack[incstack_idx].fp);
	    vim_free(incstack[incstack_idx].tag_fname);
	}
#endif
	if (stop_searching)
	    break;

	/*
	 * Stop searching if a tag was found in the current tags file and
	 * we got a global match with matching case or 'ignorecase' is set.
	 */
	if (tag != NULL && bestmatch_line != NULL &&
	       bestmatch_priority >= (p_ic ? 0 : PRI_FULL_MATCH) + PRI_GLOBAL)
	    break;
    }

    if (!stop_searching)
    {
	if (!did_open)			    /* never opened any tags file */
	    EMSG("No tags file");
	else if (tag == NULL)
	{
	    retval = OK;	/* It's OK even when no tag found */
	}
	else
	{
	    /*
	     * If we didn't find a static full match, use the best match found.
	     */
	    if (bestmatch_line != NULL)
	    {
		if (bestmatch_priority < PRI_FULL_MATCH)
		{
		    MSG("Only found tag with different case!");
		    if (!msg_scrolled)
		    {
			flushbuf();
			ui_delay(1000L, TRUE);
		    }
		}
		retval = jumpto_tag(bestmatch_line,
#ifdef EMACS_TAGS
			bestmatch_is_etag, bestmatch_ebuf,
#endif
			bestmatch_tag_fname, forceit);
	    }
	    else
		EMSG("tag not found");
	}
    }

findtag_end:
    vim_free(lbuf);
    vim_free(tag_fname);
    vim_free(bestmatch_line);
    vim_free(bestmatch_tag_fname);
#ifdef EMACS_TAGS
    vim_free(ebuf);
    vim_free(bestmatch_ebuf);
#endif

    if (tag == NULL)
    {
	if (retval == FAIL)	    /* free all matches found */
	    while (match_count > 0)
		vim_free(matches[--match_count]);
	if (match_count == 0)	    /* nothing found, free matches[] */
	{
	    vim_free(matches);
	    matches = NULL;
	}
	*file = matches;
	*num_file = match_count;
    }
    curbuf->b_help = help_save;

    return retval;
}

/*
 * Get the next name of a tag file from the tag file list.
 * For help files, use "tags" file only.
 *
 * Return FAIL if no more tag file names, OK otherwise.
 */
    static int
get_tagfname(first, buf)
    int	    first;	    /* TRUE when first file name is wanted */
    char_u  *buf;	    /* pointer to buffer of LSIZE chars */
{
    static char_u   *np = NULL;
    char_u	    *fname;
    size_t	    path_len, fname_len;
    /*
     * A list is kept of the files that have been visited.
     */
    struct visited
    {
	struct visited	*v_next;
#if defined(UNIX)
	struct stat	v_st;
#else
	char_u		v_fname[1];	/* actually longer */
#endif
    };
    static struct visited   *first_visited = NULL;
    struct visited	    *vp;
#ifdef UNIX
    struct stat		    st;
#else
    char_u		    *expand_buf;
#endif

    if (first)
    {
	np = p_tags;
	while (first_visited != NULL)
	{
	    vp = first_visited->v_next;
	    vim_free(first_visited);
	    first_visited = vp;
	}
    }

    if (np == NULL)	    /* tried allready (or bogus call) */
	return FAIL;

    /*
     * For a help window only try the file 'tags' in the same
     * directory as 'helpfile'.
     */
    if (curbuf->b_help)
    {
	path_len = gettail(p_hf) - p_hf;
	if (path_len + 9 >= LSIZE)
	    return FAIL;
	vim_memmove(buf, p_hf, path_len);
	STRCPY(buf + path_len, "tags");

	np = NULL;		/* try only once */
    }

    else
    {
#ifndef UNIX
	expand_buf = alloc(MAXPATHL);
	if (expand_buf == NULL)
	    return FAIL;
#endif

	/*
	 * Loop until we have found a file name that can be used.
	 */
	for (;;)
	{
	    if (*np == NUL)	    /* tried all possibilities */
	    {
#ifndef UNIX
		vim_free(expand_buf);
#endif
		return FAIL;
	    }

	    /*
	     * Copy next file name into buf.
	     */
	    (void)copy_option_part(&np, buf, LSIZE, " ,");

	    /*
	     * Tag file name starting with "./": Replace '.' with path of
	     * current file.
	     * Only do this when 't' flag not included in 'cpo'.
	     */
	    if (buf[0] == '.' && ispathsep(buf[1]) &&
					vim_strchr(p_cpo, CPO_DOTTAG) == NULL)
	    {
		if (curbuf->b_fname == NULL)	/* skip if no file name */
		    continue;

		path_len = gettail(curbuf->b_fname) - curbuf->b_fname;
		fname = buf + 1;
		while (ispathsep(*fname))	/* skip '/' and the like */
		    ++fname;
		fname_len = STRLEN(fname);
		if (fname_len + path_len + 1 > LSIZE)
		    continue;
		vim_memmove(buf + path_len, fname, fname_len + 1);
		vim_memmove(buf, curbuf->b_fname, path_len);
	    }

	    /*
	     * Check if this tags file has been used already.
	     * If file doesn't exist, skip it.
	     */
#if defined(UNIX)
	    if (stat((char *)buf, &st) < 0)
#else
	    if (mch_FullName(buf, expand_buf, MAXPATHL, TRUE) == FAIL)
#endif
		continue;

	    for (vp = first_visited; vp != NULL; vp = vp->v_next)
#if defined(UNIX)
		if (vp->v_st.st_dev == st.st_dev &&
						 vp->v_st.st_ino == st.st_ino)
#else
		if (fnamecmp(vp->v_fname, expand_buf) == 0)
#endif
		    break;

	    if (vp != NULL)	    /* already visited, skip it */
		continue;

	    /*
	     * Found the next name.  Add it to the list of visited files.
	     */
#ifdef UNIX
	    vp = (struct visited *)alloc((unsigned)sizeof(struct visited));
#else
	    vp = (struct visited *)alloc((unsigned)(sizeof(struct visited) +
							 STRLEN(expand_buf)));
#endif
	    if (vp != NULL)
	    {
#ifdef UNIX
		vp->v_st = st;
#else
		STRCPY(vp->v_fname, expand_buf);
#endif
		vp->v_next = first_visited;
		first_visited = vp;
	    }
	    break;
	}
#ifndef UNIX
	vim_free(expand_buf);
#endif
    }
    return OK;
}

/*
 * Parse one line from the tags file. Find start/end of tag name, start/end of
 * file name and start of search pattern.
 *
 * If is_etag is TRUE, tagpp->fname and tagpp->fname_end are not set.
 *
 * Return FAIL if there is a format error in this line, OK otherwise.
 */
    static int
parse_tag_line(lbuf,
#ifdef EMACS_TAGS
		    is_etag,
#endif
			      tagpp)
    char_u		*lbuf;		/* line to be parsed */
#ifdef EMACS_TAGS
    int			is_etag;
#endif
    struct tag_pointers *tagpp;
{
    char_u	*p;

#ifdef EMACS_TAGS
    char_u	*p_7f;

    if (is_etag)
    {
	/*
	 * There are two formats for an emacs tag line:
	 * 1:  struct EnvBase ^?EnvBase^A139,4627
	 * 2: #define	ARPB_WILD_WORLD ^?153,5194
	 */
	p_7f = vim_strchr(lbuf, 0x7f);
	if (p_7f == NULL)
	    return FAIL;

	/* Find ^A.  If not found the line number is after the 0x7f */
	p = vim_strchr(p_7f, Ctrl('A'));
	if (p == NULL)
	    p = p_7f + 1;
	else
	    ++p;

	if (!isdigit(*p))	    /* check for start of line number */
	    return FAIL;
	tagpp->command = p;

			
	if (p[-1] == Ctrl('A'))	    /* first format: explicit tagname given */
	{
	    tagpp->tagname = p_7f + 1;
	    tagpp->tagname_end = p - 1;
	}
	else			    /* second format: isolate tagname */
	{
	    /* find end of tagname */
	    for (p = p_7f - 1; !iswordchar(*p); --p)
		if (p == lbuf)
		    return FAIL;
	    tagpp->tagname_end = p + 1;
	    while (p >= lbuf && iswordchar(*p))
		--p;
	    tagpp->tagname = p + 1;
	}
    }
    else	/* not an Emacs tag */
    {
#endif
	/* Isolate the tagname, from lbuf up to the first white */
	tagpp->tagname = lbuf;
	p = skiptowhite(lbuf);
	tagpp->tagname_end = p;

	/* Isolate file name, from first to second white space */
	p = skipwhite(p);
	tagpp->fname = p;
	p = skiptowhite(p);
	tagpp->fname_end = p;

	/* find start of search command, after second white space */
	p = skipwhite(p);
	if (*p == NUL)
	    return FAIL;
	tagpp->command = p;
#ifdef EMACS_TAGS
    }
#endif

    return OK;
}

/*
 * Check if tagname is a static tag
 *
 * Static tags produced by the older ctags program have the format:
 *	'file:tag  file  /pattern'.
 * This is only recognized when both occurences of 'file' are the same, to
 * avoid recognizing "string::string" or ":exit".
 *
 * Static tags produced by the new ctags program have the format:
 *	'tag  file  /pattern/;"<Tab>file:'
 *
 * Return TRUE if it is a static tag and adjust *tagname to the real tag.
 * Return FALSE if it is not a static tag.
 */
    static int
test_for_static(tagpp)
    struct tag_pointers	*tagpp;
{
    char_u	*p;
    int		len;

#ifdef OLD_STATIC_TAGS
    /*
     * Check for old style static tag: "file:tag file .."
     */
    len = tagpp->fname_end - tagpp->fname;
    p = tagpp->tagname + len;
    if (       p < tagpp->tagname_end
	    && *p == ':'
	    && fnamencmp(tagpp->tagname, tagpp->fname, len) == 0)
    {
	tagpp->tagname = p + 1;
	return TRUE;
    }
#endif

    /*
     * Check for new style static tag ":...<Tab>file:[<Tab>...]"
     */
    p = tagpp->command;
    while ((p = vim_strchr(p, '\t')) != NULL)
    {
	++p;
	if (STRNCMP(p, "file:", 5) == 0)
	    return TRUE;
    }

    return FALSE;
}

/*
 * Jump to a tag that has been found in one of the tag files
 */
    static int
jumpto_tag(lbuf,
#ifdef EMACS_TAGS
		is_etag, etag_fname,
#endif
				    tag_fname, forceit)
    char_u	*lbuf;		/* line from the tags file for this tag */
#ifdef EMACS_TAGS
    int		is_etag;	/* TRUE if it's from an emacs tags file */
    char_u	*etag_fname;	/* file name for tag if is_etag is TRUE */
#endif
    char_u	*tag_fname;	/* file name of the tags file itself */
    int		forceit;	/* :ta with ! */
{
    int		save_secure;
    int		save_magic;
    int		save_p_ws, save_p_scs, save_p_ic;
    char_u	*str;
    char_u	*pbuf;			/* search pattern buffer */
    char_u	*pbuf_end;
    char_u	*expanded_fname = NULL;
    char_u	*tofree_fname = NULL;
    struct tag_pointers tagp;
    char_u	*fname;
    int		retval = FAIL;
    int		getfile_result;
    int		search_options;

    pbuf = alloc(LSIZE);

    if (pbuf == NULL
#ifdef EMACS_TAGS
		    || (is_etag && etag_fname == NULL)
#endif
							|| tag_fname == NULL)
	goto erret;

    /*
     * find the search pattern (caller should check it is there)
     */
    if (parse_tag_line(lbuf,
#ifdef EMACS_TAGS
			       is_etag,
#endif
					&tagp) == FAIL)
	goto erret;
    fname = tagp.fname;

#ifdef EMACS_TAGS
    if (is_etag)
	fname = etag_fname;
    else
#endif
	*tagp.fname_end = NUL;

    /* copy the command to pbuf[], remove trailing CR/NL */
    str = tagp.command;
    for (pbuf_end = pbuf; *str && *str != '\n' && *str != '\r'; )
    {
#ifdef EMACS_TAGS
	if (is_etag && *str == ',')	/* stop at ',' after line number */
	    break;
#endif
	*pbuf_end++ = *str++;
    }
    *pbuf_end = NUL;

#ifdef EMACS_TAGS
    if (!is_etag)
#endif
    {
	/*
	 * Remove the "<Tab>fieldname:value" stuff; we don't need it here.
	 */
	if (isdigit(pbuf[0]))
	    str = skipdigits(pbuf);
	else if (pbuf[0] == '/' || pbuf[0] == '?')
	{
	    str = skip_regexp(pbuf + 1, pbuf[0], FALSE);
	    if (*str != pbuf[0])
		str = NULL;
	    else
		++str;
	}
	else
	    str = NULL;
		
	if (str != NULL && STRNCMP(str, ";\"\t", 3) == 0)
	{
	    pbuf_end = str;
	    *pbuf_end = NUL;
	}
    }

    /*
     * expand file name (for environment variables)
     */
    expanded_fname = ExpandOne((char_u *)fname, NULL, WILD_LIST_NOTFOUND,
							    WILD_EXPAND_FREE);
    if (expanded_fname != NULL)
	fname = expanded_fname;

    /*
     * if 'tagrelative' option set, may change file name
     */
    fname = expand_rel_name(fname, tag_fname);
    if (fname == NULL)
	goto erret;
    tofree_fname = fname;	/* free() it later */

    /*
     * check if file for tag exists before abandoning current file
     */
    if (mch_getperm(fname) < 0)
    {
	EMSG2("File \"%s\" does not exist", fname);
	goto erret;
    }

    ++RedrawingDisabled;

    /* if it was a CTRL-W CTRL-] command split window now */
    if (postponed_split)
	win_split(0, FALSE);

    /* A :ta from a help file will keep the b_help flag set. */
    keep_help_flag = curbuf->b_help;
    getfile_result = getfile(0, fname, NULL, TRUE, (linenr_t)0, forceit);

    if (getfile_result <= 0)		/* got to the right file */
    {
	curwin->w_set_curswant = TRUE;
	postponed_split = FALSE;

	save_secure = secure;
	secure = 1;
	save_magic = p_magic;
	p_magic = FALSE;	/* always execute with 'nomagic' */
	tag_modified = FALSE;

	/*
	 * If 'cpoptions' contains 't', store the search pattern for the "n"
	 * command.  If 'cpoptions' does not contain 't', the search pattern
	 * is not stored.
	 */
	if (vim_strchr(p_cpo, CPO_TAGPAT) != NULL)
	    search_options = 0;
	else
	    search_options = SEARCH_KEEP;

	/*
	 * If the command is a search, try here.
	 *
	 * Rather than starting at line one, just turn wrap-scan
	 * on temporarily, this ensures that tags on line 1 will
	 * be found, and makes sure our guess searches search the
	 * whole file when repeated -- webb.
	 * Also reset 'smartcase' for the search, since the search
	 * pattern was not typed by the user.
	 * Only use do_search() when there is a full search command, without
	 * anything following.
	 */
	str = pbuf;
	if (pbuf[0] == '/' || pbuf[0] == '?')
	    str = skip_regexp(pbuf + 1, pbuf[0], FALSE);
	if (str >= pbuf_end - 1)    /* search command with nothing following */
	{
	    save_p_ws = p_ws;
	    save_p_ic = p_ic;
	    save_p_scs = p_scs;
	    p_ws = TRUE;    /* Switch wrap-scan on temporarily */
	    p_ic = FALSE;   /* don't ignore case now */
	    p_scs = FALSE;
	    add_to_history(1, pbuf + 1);    /* put pattern in search history */

	    curwin->w_cursor.lnum = 0;	    /* start search before first line */
	    if (do_search(NULL, pbuf[0], pbuf + 1, (long)1, search_options))
		retval = OK;
	    else
	    {
		int	found = 1;

		/*
		 * try again, ignore case now
		 */
		p_ic = TRUE;
		if (!do_search(NULL, pbuf[0], pbuf + 1, (long)1,
							      search_options))
		{
		    /*
		     * Failed to find pattern, take a guess: "^func  ("
		     */
		    found = 2;
		    (void)test_for_static(&tagp);
		    *tagp.tagname_end = NUL;
		    sprintf((char *)pbuf, "^%s\\[ \t]\\*(", tagp.tagname);
		    if (!do_search(NULL, '/', pbuf, (long)1, search_options))
		    {
			/* Guess again: "^char * \<func  (" */
			sprintf((char *)pbuf,
				 "^\\[#a-zA-Z_]\\.\\*\\<%s\\[ \t]\\*(",
								tagp.tagname);
			if (!do_search(NULL, '/', pbuf, (long)1,
							      search_options))
			    found = 0;
		    }
		}
		if (found == 0)
		    EMSG("Can't find tag pattern");
		else
		{
		    /*
		     * Only give a message when really guessed, not when 'ic'
		     * is set and match found while ignoring case.
		     */
		    if (found == 2 || !save_p_ic)
		    {
			MSG("Couldn't find tag, just guessing!");
			if (!msg_scrolled)
			{
			    flushbuf();
			    ui_delay(1000L, TRUE);
			}
		    }
		    retval = OK;
		}
	    }
	    p_ws = save_p_ws;
	    p_ic = save_p_ic;
	    p_scs = save_p_scs;

	    /* A search command may have positioned the cursor beyond the end
	     * of the line.  May need to correct that here. */
	    adjust_cursor();
	}
	else
	{			    /* start command in line 1 */
	    curwin->w_cursor.lnum = 1;
	    do_cmdline(pbuf, NULL, NULL, DOCMD_NOWAIT|DOCMD_VERBOSE);
	    retval = OK;
	}

	/*
	 * When the command has set the b_changed flag, give a warning to the
	 * user about this.
	 */
	if (tag_modified)
	{
	    secure = 2;
	    EMSG("WARNING: tag command changed a buffer!!!");
	}
	if (secure == 2)	    /* done something that is not allowed */
	    wait_return(TRUE);
	secure = save_secure;
	p_magic = save_magic;

	/*
	 * Print the file message after redraw if jumped to another file.
	 * Don't do this for help files (avoid a hit-return message).
	 */
	if (getfile_result == -1)
	{
	    if (!curbuf->b_help)
		need_fileinfo = TRUE;
	    retval = OK;	    /* always return OK if jumped to another
				       file (at least we found the file!) */
	}

	/*
	 * For a help buffer: Put the cursor line at the top of the window,
	 * the help subject will be below it.
	 */
	if (curbuf->b_help)
	{
	    set_topline(curwin, curwin->w_cursor.lnum);
	    update_topline();		/* correct for 'so' */
	    update_screen(NOT_VALID);
	}
	--RedrawingDisabled;
    }
    else
    {
	--RedrawingDisabled;
	if (postponed_split)		/* close the window */
	{
	    close_window(curwin, FALSE);
	    postponed_split = FALSE;
	}
    }

erret:
    vim_free(pbuf);
    vim_free(tofree_fname);
    vim_free(expanded_fname);

    return retval;
}

/*
 * If 'tagrelative' option set, change fname (name of file containing tag)
 * according to tag_fname (name of tag file containing fname).
 * Returns a pointer to allocated memory (or NULL when out of memory).
 */
    static char_u *
expand_rel_name(fname, tag_fname)
    char_u	*fname;
    char_u	*tag_fname;
{
    char_u	*p;
    char_u	*retval;

    if ((p_tr || curbuf->b_help) && !mch_isFullName(fname) &&
				       (p = gettail(tag_fname)) != tag_fname)
    {
	retval = alloc(MAXPATHL);
	if (retval == NULL)
	    return NULL;

	STRCPY(retval, tag_fname);
	STRNCPY(retval + (p - tag_fname), fname, MAXPATHL - (p - tag_fname));
	/*
	 * Translate names like "src/a/../b/file.c" into "src/b/file.c".
	 */
	simplify_filename(retval);
    }
    else
	retval = vim_strsave(fname);

    return retval;
}

/*
 * Moves the tail part of the path (including the terminating NUL) pointed to
 * by "tail" to the new location pointed to by "here". This should accomodate
 * an overlapping move.
 */
#define movetail(here, tail)  vim_memmove(here, tail, STRLEN(tail) + (size_t)1)

/*
 * For MS-DOS we should check for backslash too, but that is complicated.
 */
#define DIR_SEP	    '/'		/* the directory separator character */

/*
 * Converts a file name into a canonical form. It simplifies a file name into
 * its simplest form by stripping out unneeded components, if any.  The
 * resulting file name is simplified in place and will either be the same
 * length as that supplied, or shorter.
 */
    void
simplify_filename(filename)
    char_u *filename;
{
    int	    absolute = FALSE;
    int	    components = 0;
    char_u  *p, *tail;

    p = filename;
    if (*p == DIR_SEP)
    {
	absolute = TRUE;
	++p;
    }
    do
    {
	/*  Always leave "p" pointing to character following next "/". */
	if (*p == DIR_SEP)
	    movetail(p, p+1);		    /* strip duplicate "/" */
	else if (STRNCMP(p, "./", 2) == 0)
	    movetail(p, p+2);		    /* strip "./" */
	else if (STRNCMP(p, "../", 3) == 0)
	{
	    if (components > 0)		    /* strip any prev. component */
	    {
		*(p - 1) = 0;		    /* delete "/" before  "../" */
		tail  = p + 2;		    /* skip to "/" of "../" */
		p = vim_strrchr(filename, DIR_SEP);  /* find preceding sep. */
		if (p != NULL)		    /* none found */
		    ++p;		    /* skip to char after "/" */
		else
		{
		    ++tail;		    /* strip leading "/" from tail*/
		    p = filename;	    /* go back to beginning */
		    if (absolute)	    /* skip over any leading "/" */
			++p;
		}
		movetail(p, tail);	    /* strip previous component */
		--components;
	    }
	    else if (absolute)		    /* no parent to root... */
		movetail(p, p+3);	    /*	 so strip "../" */
	    else			    /* leading series of "../" */
	    {
		p = vim_strchr(p, DIR_SEP); /* skip to next "/" */
		if (p != NULL)
		    ++p;		    /* skip to char after "/" */
	    }
	}
	else
	{
	    ++components;		    /* simple path component */
	    p = vim_strchr(p, DIR_SEP);		/* skip to next "/" */
	    if (p != NULL)
		++p;			    /* skip to char after "/" */
	}
    } while (p != NULL && *p != NUL);
}

/*
 * Check if we have a tag for the current file.
 * This is a bit slow, because of the full path compare in fullpathcmp().
 * Return TRUE if tag for file "fname" if tag file "tag_fname" is for current
 * file.
 */
    static int
#ifdef EMACS_TAGS
test_for_current(is_etag, fname, fname_end, tag_fname)
    int	    is_etag;
#else
test_for_current(fname, fname_end, tag_fname)
#endif
    char_u  *fname;
    char_u  *fname_end;
    char_u  *tag_fname;
{
    int	    c;
    int	    retval = FALSE;
    char_u  *relname;

    if (curbuf->b_ffname != NULL)	/* if the current buffer has a name */
    {
#ifdef EMACS_TAGS
	if (is_etag)
	    c = 0;	    /* to shut up GCC */
	else
#endif
	{
	    c = *fname_end;
	    *fname_end = NUL;
	}
	relname = expand_rel_name(fname, tag_fname);
	if (relname != NULL)
	{
	    retval = (fullpathcmp(relname, curbuf->b_fname) == FPC_SAME);
	    vim_free(relname);
	}
#ifdef EMACS_TAGS
	if (!is_etag)
#endif
	    *fname_end = c;
    }

    return retval;
}
