/* Find the next error in mentioned in the shell output window.
 * written for vile: Copyright (c) 1990, 1995 by Paul Fox
 * rewritten to use regular expressions, 1995 by T.Dickey (dickey@clark.net)
 *
 * $Header: /usr/build/vile/vile/RCS/finderr.c,v 1.70 1998/04/30 09:19:03 kev Exp $
 *
 */

#include "estruct.h"
#include "edef.h"

#if OPT_FINDERR

#define W_VERB  0
#define W_FILE  1
#define W_LINE  2
#define W_COLM  3
#define W_TEXT  4

typedef	struct	{
	regexp *exp_comp;
	int	words[5];
	} ERR_PATTERN;

static	LINE *	getdot (BUFFER *bp);
static	void	putdotback (BUFFER *bp, LINE *dotp);

static	char febuff[NBUFN];	/* name of buffer to find errors in */
static	int	newfebuff = TRUE; /* is the name new since last time? */

static	TBUFF *	fe_verb;
static	TBUFF *	fe_file;
static	TBUFF *	fe_text;
static	int	fe_colm;
static	int	fe_line;

	/*
	 * This is the list of predefined regular expressions for the error
	 * finder.  The user can substitute a new list at runtime by loading
	 * the buffer [Error Expressions].  Basically, they're normal regular
	 * expressions, with embedded stuff that the error finder can parse to
	 * find the verb, file, line and text fields that the regular
	 * expression may contain.  These fields may be in any order, and all
	 * except the file are optional.
	 *
	 *	%V - verb, for tracking gmake-style Entering/Leaving messages
	 *	%F - range of characters to match filename.
	 *	%B - range of characters to match scratch-buffer.
	 *	%L - line number (this has to be an integer)
	 *	%T - text to display in the message line. If no field is given,
	 *		the error finder will display the entire line from
	 *		the error-buffer.
	 *
	 * The %V, %F, %T fields may be given in alternate form, using ranges.
	 * The default field is a blank-delimited token, which is enough for
	 * %V, marginal for %F and useless for %T.  Vile takes each %-marked
	 * field and replaces it by a subexpression, to use the subexpression
	 * number to obtain the actual field mapping.
	 *
	 * FIXME:  some lint programs put the file, line-number and text on
	 * separate lines.  Maybe we should add another control code that
	 * specifies sequences of regular expressions.
	 *
	 * FIXME:  it might be useful to autoconf for the existing lint
	 * program, to select the bsd/sys5 lint regular expressions.
	 */
static const
char *const predefined[] = {
	"^\"%[^\" \t]\", line %L:%T",		/* various C compilers */
	"^%[^: \t]:\\s*%L:\\s*%T",		/* "grep -n" */
#if OPT_MSDOS_PATH
	"^%F:\\s*%L:\\s*%T",			/* "grep -n", handles */
	                                        /* dos drive letter   */
#endif
#if SYS_APOLLO
	" Line %L of \"%[^\" \t]\"",		/* C compiler */
#endif
#if SYS_SUNOS && SYSTEM_HAS_LINT_PROG
	"%[^:( \t](%L):%T",  					/* bsd lint) */
	"  ::  %[^( \t](%L)",					/* bsd lint) */
	"used[ \t]*([ \t]%[^(](%L)[ \t]*)",			/* bsd lint) */
#endif
	/* 	ultrix, sgi, osf1 (alpha only?)  use:			*/
	/* 	compiler-name: Error: filename, line line-number ...	*/
	"[^ ]\\+ [^ ]\\+ \"%[^, \t\"]\", line %L",
	"[^ ]\\+ [^ ]\\+ %[^, \t], line %L",
	"[^ ]\\+ \"%[^\"]\", line %L",			/* HP/UX C compiler */
#if defined(_AIX)
	"^\"%[^\" \t]\", line %L\\.[0-9]\\+:%T",	/* AIX C compilers */
#endif
#if defined(clipper) || defined(__clipper__)
	"^\"%[^\" \t]\", line %L (col. [0-9]\\+):%T",	/* CLIX C compiler */
#endif
	"^%[^(](%L)[ \t]\\+:%T",

#if SYS_UNIX && SYSTEM_HAS_LINT_PROG
	"^    [^ \t]\\+[ \t]\\+%[^(](%L)$",			/* sys5 lint) */
	"^    [^(].*( arg %L ) \t%[^( \t](%L) :: [^(]\\+(%L))",	/* sys5 lint))*/
	"^    .* :: %[^(](%L)",					/* sys5 lint) */
#endif
#if CC_CSETPP
	"^%[^(](%L:%C) : %T",
#endif
#if CC_TURBO
	"^Error %[^ ] %L:",
	"^Warning %[^ ] %L:",
#endif
#if CC_WATCOM
	"^%[^(](%L): %T",
#endif
	"^%B:%L:%T",					/* "pp" in scratch buf*/
	"^[^:]\\+: %V directory `%[^']'",		/* GNU make */
	"%T at %F line %L.*",		/* perl 5 */
	"%F\\[%L\\]:%T",		/* hgrep */
	};

static	ERR_PATTERN *	exp_table = 0;
static	ALLOC_T		exp_count = 0;

void
set_febuff(const char *name)
{
	(void)strncpy0(febuff, name, NBUFN);
	newfebuff = TRUE;
}

/*
 * Convert a given error-pattern to regular expression
 */

#define	APP_S(S) if (pass == 1) want += sizeof(S); else dst = lsprintf(dst, "%s", S)
#define APP_C    if (pass != 1) *dst++ = *src

static void
convert_pattern(ERR_PATTERN *errp, LINE *lp)
{
	static	const	char	before[] = "\\(";
	static	const	char	after [] = "\\+\\)";
	static	const	char	number[] = "\\([0-9]\\+\\)";
	static	const	char	normal[] = "\\([^ \t]\\+\\)";
	static	const	char	remain[] = "\\(.\\+\\)";

	char	*temp = 0, *src, *dst = 0;
	regexp	*exp = 0;
	int	pass;
	int	word;
	int	mark;
	int	range;
	size_t	want  = llength(lp);
	char *	first = lp->l_text;
	char *	last  = first + want;

	(void) memset(errp, 0, sizeof(*errp));

	/* In the first pass, find the number of fields we'll substitute.
	 * Then allocate a new string that's a genuine regular expression
	 */
	for (pass = 1; pass <= 2; pass++) {
		for (src = first, word = 0, range = FALSE; src < last; src++) {
			if (*src == '\\') {
				APP_C;
				if (++src == last)
					break;
				APP_C;
			} else if (*src == '%') {
				mark = -1;
				switch(*++src) {
				case 'V':	mark = W_VERB;	break;
				case 'F':	mark = W_FILE;	break;
				case 'B':
					APP_S("\\(\\[[^:]\\+]\\)");
					errp->words[W_FILE] = ++word;
					break;
				case 'T':
					APP_S(remain);
					errp->words[W_TEXT] = ++word;
					break;
				case 'C':
					APP_S(number);
					errp->words[W_COLM] = ++word;
					break;
				case 'L':
					APP_S(number);
					errp->words[W_LINE] = ++word;
					break;
				case LBRACK:
					range = TRUE;
					APP_S(before);
					APP_C;
					if (src[1] == '^') {
						src++;
						APP_C;
					}
					if (src[1] == RBRACK) {
						src++;
						APP_C;
					}
					break;
				default:
					src--;
					break;
				}
				if (mark >= 0) {
					APP_S(normal);
					errp->words[mark] = ++word;
				}
			} else if ((*src == RBRACK) && range) {
				APP_C;
				APP_S(after);
				range = FALSE;
				if (src+1 < last) {
					switch(*++src) {
					case 'V':	mark = W_VERB;	break;
					default:	src--;
							/* FALLTHRU */
					case 'F':	mark = W_FILE;	break;
					case 'T':	mark = W_TEXT;	break;
					}
				} else {
					mark = W_FILE;
				}
				errp->words[mark] = ++word;
			} else {
				APP_C;
			}
		}
		if (pass == 1) {
			dst = temp = typeallocn(char, want);
			if (dst == 0)
				break;
		} else
			*dst = EOS;
	}
	if (temp != 0) {
		exp = regcomp(temp, TRUE);
		free(temp);
	}
	errp->exp_comp = exp;
}

/*
 * Free the storage currently used in this module
 */
static void
free_patterns(void)
{
	if (exp_table != 0) {
		while (exp_count != 0)
			free((char *)(exp_table[--exp_count].exp_comp));
		free((char *)exp_table);
	}
}

#if OPT_UPBUFF
/*ARGSUSED*/
static int
update_patterns(BUFFER *bp GCC_UNUSED)
{
	free_patterns();
	return TRUE;
}
#endif

/*
 * Initialize this module.  If the expressions buffer doesn't exist, load it
 * from the internal table. If our cached regexp list doesn't match, recompute
 * that as well.
 */
static int
load_patterns(void)
{
	BUFFER	*bp;
	LINE	*lp;
	SIZE_T	n;

	/* find the error-expressions buffer */
	if ((bp = find_b_name(ERRORS_BufName)) == 0) {
		if ((bp = bfind(ERRORS_BufName, BFINVS)) == NULL)
			return FALSE;

		for (n = 0; n < TABLESIZE(predefined); n++)
			addline(bp, predefined[n], -1);
		set_rdonly(bp, bp->b_fname, MDVIEW);
		free_patterns();
	}
	bsizes(bp);
	if (bp->b_linecount == 0)
		return FALSE;

	/* any change makes the patterns obsolete */
#if OPT_UPBUFF
	update_scratch(ERRORS_BufName, update_patterns);
	bp->b_rmbuff = update_patterns;
#endif

	if (exp_count == 0) {
		exp_count = bp->b_linecount;
		exp_table = typeallocn(ERR_PATTERN, exp_count);

		n = 0;
		for_each_line(lp,bp)
			convert_pattern(&exp_table[n++], lp);
	}

	return TRUE;
}

/*
 * Initialize this module by converting the error-patterns to regular
 * expressions.  Return the count'th item in the error-patterns list, or null
 * if count is out of range.
 */
static ERR_PATTERN *
next_pattern(ALLOC_T count)
{
	ERR_PATTERN *result = 0;

	if (count < exp_count)
		result = &exp_table[count];
	return (result);
}

/*
 * Decode the matched ERR_PATTERN
 */
static void
decode_exp (ERR_PATTERN *exp)
{
	regexp *p = exp->exp_comp;
	int	n;
	TBUFF	*temp;

	tb_free(&fe_verb);
	tb_free(&fe_file);
	tb_free(&fe_text);
	fe_colm = 1;
	fe_line = 0;

	n = 0;
	for (n = 1; (n < NSUBEXP) && p->startp[n] && p->endp[n]; n++) {
		temp = 0;
		if (tb_bappend(&temp,
			p->startp[n],
			(ALLOC_T)(p->endp[n] - p->startp[n])) == 0
		 || tb_append(&temp, EOS) == 0)
			return;

		if (n == exp->words[W_VERB]) {
			fe_verb = temp;
		} else if (n == exp->words[W_FILE]) {
			fe_file = temp;
		} else if (n == exp->words[W_TEXT]) {
			fe_text = temp;
		} else {
			if (n == exp->words[W_LINE])
				fe_line = atoi(tb_values(temp));
			else if (n == exp->words[W_COLM])
				fe_colm = atoi(tb_values(temp));
			tb_free(&temp);
		}
	}
}

/* edits the file and goes to the line pointed at by the next compiler
        error in the "[output]" window.  It unfortunately doesn't mark
        the lines for you, so adding lines to the file throws off the
        later numbering.  Solutions to this seem messy at the moment */

/* ARGSUSED */
int
finderr(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register BUFFER *sbp;
	register int s;
	LINE *dotp;
	int moveddot = FALSE;
	ERR_PATTERN *exp;

	char *errverb;
	char *errfile;
	char *errtext;
	char ferrfile[NFILEN];
	ALLOC_T len;

	static int oerrline = -1;
	static TBUFF *oerrfile;
	static TBUFF *oerrtext;

#define DIRLEVELS 20
	static int l = 0;
	static char *dirs[DIRLEVELS];

	if (!comp_err_exps(FALSE,1))
		return(FALSE);

	/* look up the right buffer */
	if ((sbp = find_b_name(febuff)) == NULL) {
		mlforce("[No buffer to search for errors.]");
		return(FALSE);
	}

	if (newfebuff) {
		oerrline = -1;
		oerrfile = tb_init(&oerrfile, EOS);
		oerrtext = tb_init(&oerrtext, EOS);
		while (l)
			free(dirs[l--]);
	}
	newfebuff = FALSE;

	dotp = getdot(sbp);

	for_ever {
		/* To use this line, we need both the filename and the line
		 * number in the expected places, and a different line than
		 * last time.
		 */
		if (lisreal(dotp)) {
			ALLOC_T	count = 0;

			while ((exp = next_pattern(count++)) != 0
			  && !lregexec(exp->exp_comp, dotp, 0, llength(dotp)))
				;

			if (exp != 0) {
				decode_exp(exp);

				errverb = tb_values(fe_verb);
				errfile = tb_values(fe_file);
				errtext = tb_values(fe_text);

				if (errfile != 0
				 && fe_line > 0) {
					if (oerrline != fe_line
					 || strcmp(tb_values(oerrfile),errfile))
						break;
					if (oerrline == fe_line
					 && errtext != 0
					 && strcmp(tb_values(oerrtext),errtext))
					 	break;
				} else if (errverb != 0
				    &&     errfile != 0) {
					if (!strcmp("Entering", errverb)) {
						if (l < DIRLEVELS) {
							dirs[++l] = strmalloc(errfile);
						}
					} else if (!strcmp("Leaving", errverb)) {
						if (l > 0)
							free(dirs[l--]);
					}
				}
			}
		}

		if (lforw(dotp) == buf_head(sbp)) {
			mlwarn("[No more errors in %s buffer]", febuff);
			/* start over at the top of file */
			putdotback(sbp, lforw(buf_head(sbp)));
			while (l)
				free(dirs[l--]);
			return FALSE;
		}
		dotp = lforw(dotp);
		moveddot = TRUE;
	}
	/* put the new dot back, before possible changes to contents
				of current window from getfile() */
	if (moveddot)
		putdotback(sbp,dotp);

	(void)pathcat(ferrfile, dirs[l], errfile);

	if (!eql_bname(curbp, ferrfile) &&
		strcmp(ferrfile,curbp->b_fname)) {
		/* if we must change windows */
		WINDOW *wp;
		for_each_visible_window(wp) {
			if (eql_bname(wp->w_bufp, ferrfile)
			 || !strcmp(wp->w_bufp->b_fname,ferrfile))
				break;
		}
		if (wp) {
			curwp = wp;
			make_current(curwp->w_bufp);
			upmode();
		} else {
			s = getfile(ferrfile,TRUE);
			if (s != TRUE)
				return s;
		}
	}

	if (errtext) {
		mlforce("%s", errtext);
		len = strlen(errtext);
	} else {
		mlforce("Error: %*S", dotp->l_used, dotp->l_text);
		errtext = dotp->l_text;
		len = dotp->l_used;
	}
	if ((oerrtext = tb_init(&oerrtext, EOS)) != 0) {
		tb_bappend(&oerrtext, errtext, len);
		tb_append(&oerrtext, EOS);
	}

	/* it's an absolute move */
	curwp->w_lastdot = DOT;
	s = gotoline(TRUE, -(curbp->b_lines_on_disk - fe_line + 1));
	DOT.o = fe_colm ? fe_colm - 1 : 0;

	oerrline = fe_line;
	(void)tb_scopy(&oerrfile, errfile);

	return s;
}

static LINE *
getdot(BUFFER *bp)
{
	register WINDOW *wp;
	if (bp->b_nwnd) {
		/* scan for windows holding that buffer,
					pull dot from the first */
		for_each_visible_window(wp) {
			if (wp->w_bufp == bp) {
				return wp->w_dot.l;
			}
		}
	}
	return bp->b_dot.l;
}

static void
putdotback(BUFFER *bp, LINE *dotp)
{
	register WINDOW *wp;

	if (bp->b_nwnd) {
		for_each_visible_window(wp) {
			if (wp->w_bufp == bp) {
				wp->w_dot.l = dotp;
				wp->w_dot.o = 0;
				wp->w_flag |= WFMOVE;
			}
		}
		return;
	}
	/* then the buffer isn't displayed */
	bp->b_dot.l = dotp;
	bp->b_dot.o = 0;
}

/*
 * Ask for a new finderr buffer name
 */
/* ARGSUSED */
int
finderrbuf(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register int    s;
	char name[NFILEN+1];
	BUFFER *bp;

	(void)strcpy(name, febuff);
	if ((s = mlreply("Buffer to scan for \"errors\": ", name, sizeof(name))) == ABORT)
		return s;
	if (s == FALSE) {
		set_febuff(OUTPUT_BufName);
	} else {
		if ((bp = find_any_buffer(name)) == 0)
			return FALSE;
		set_febuff(bp->b_bname);
	}
	return TRUE;
}

/*
 * (Re)compile the error-expressions buffer.  This is needed as an entrypoint
 * so that macros can manipulate the set of expressions (including reading it
 * from a file).
 */
/*ARGSUSED*/
int
comp_err_exps(int f GCC_UNUSED, int n GCC_UNUSED)
{
	if (!load_patterns()) {
		mlforce("[No error-expressions are defined.]");
		return(FALSE);
	}
	return TRUE;
}
#endif
