/*
 * find the next error in mentioned in the shell output window.
 * written for vile by paul fox.
 * rewritten to use regular expressions by T.Dickey
 *
 * Copyright (c) 1990-2003 by Paul Fox and Thomas Dickey
 *
 * $Header: /usr/build/vile/vile/RCS/finderr.c,v 1.118 2003/07/27 17:07:07 tom Exp $
 *
 */

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

#if OPT_FINDERR

typedef enum {
    W_VERB = 0
    ,W_FILE
    ,W_LINE
    ,W_COLM
    ,W_TEXT
    ,W_LAST
} ErrTokens;

typedef struct {
    char *exp_text;
    regexp *exp_comp;
    int words[W_LAST];
} 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 cC_base;
static int lL_base;
static long fe_colm;
static long 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)
 *      %C - column 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 */
    "^%F:\\s*%L:%C:\\s*%T",	/* antic */
    "^%F:\\s*%L:\\s*%T",	/* "grep -n" */

#if SYS_VMS
    "[ \t]*At line number %L in %[^;].*",	/* crude support for DEC C
						 * compilers. Unfortunately,
						 * these compilers place error
						 * text on previous _lines_.
						 */
#endif

#if SYS_SUNOS && defined(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 */
    "File = %F, Line = %L",	/* SGI MIPSpro 7.3 compilers    */
    "^\"%F\":line %L:%T",	/* Solaris lex */
#if defined(_AIX)
    "^\"%[^\" \t]\", line %L\\.%C:%T",	/* AIX C compilers */
#endif
#if defined(clipper) || defined(__clipper__)
    "^\"%[^\" \t]\", line %L (col. %C\\+):%T",	/* CLIX C compiler */
#endif

#ifdef _MSC_VER
    "^[ \t]*%F\\s*(%L)\\s*:%T",	/* MSVC++ */
#endif

    "^%[^\"(](%L)[ \t]\\?:%T",	/* weblint */
#if SYS_UNIX && defined(SYSTEM_HAS_LINT_PROG)
    /* sys5 lint */
    "^    [^ \t]\\+[ \t]\\+%[^(](%L)$",
    "^    [^(].*( arg %L ) \t%[^( \t](%L) :: [^(]\\+(%L))",
    "^    .* :: %[^(](%L)",
#endif

#if SYS_OS2 || SYS_OS2_EMX
    "^%[^(](%L:%C) : %T",	/* CSet compiler */
#endif
#if CC_WATCOM
    "^%[^(](%L): %T",
#endif

    /* Borland C++ */
    "^Error\\( [^ ]\\+\\)\\? %F %L: %T",
    "^Warning\\( [^ ]\\+\\)\\? %F %L: %T",

    "^%B:%L:%T",		/* "pp" in scratch buf */
    "^[^:]\\+: %V directory `%[^']'",	/* GNU make */
    "%T at %F line %L.*",	/* perl 5 */
    "^%F\\[%L\\]:%T",		/* hgrep */
    "^\"%[^\"]\", line %L, col %C, %T",		/* ncurses, atac */
    "^\"%[^\"]\", line %L, %T",	/* ncurses */
};

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

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

const char *
get_febuff(void)
{
    return febuff;
}

static const char *
get_token_name(ErrTokens n)
{
    char *result;

    switch (n) {
    case W_VERB:
	result = "verb";
	break;
    case W_FILE:
	result = "filename";
	break;
    case W_LINE:
	result = "line";
	break;
    case W_COLM:
	result = "column";
	break;
    case W_TEXT:
	result = "text";
	break;
    default:
	result = "unknown";
	break;
    }
    return result;
}

static int
marks_in(const char *expr)
{
    int result = 0;
    int escaped = FALSE;

    while (*expr != EOS) {
	if (escaped) {
	    if (*expr == L_PAREN)
		result++;
	    escaped = FALSE;
	} else if (*expr == BACKSLASH) {
	    escaped = TRUE;
	}
	expr++;
    }
    return result;
}

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

#define	APP_T(S) if (pass == 1) want += strlen(S); \
		 else dst = lsprintf(dst, "%s", S)
#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));
    TPRINTF(("error-pattern %*S\n", want, first));

    /* 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 == BACKSLASH) {
		APP_C;
		if (++src == last)
		    break;
		if (*src == L_PAREN)	/* a group we don't own... */
		    word++;
		APP_C;
	    } else if (*src == '%') {
		mark = -1;
		switch (*++src) {
		case 'V':
		    mark = W_VERB;
		    break;
		case 'F':
		    APP_S(before);
		    APP_T(tb_values(filename_expr));
		    APP_S(after);
		    errp->words[W_FILE] = ++word;
		    word += marks_in(tb_values(filename_expr));
		    break;
		case 'B':
		    APP_S("\\(\\[[^:]\\+]\\)");
		    errp->words[W_FILE] = ++word;
		    break;
		case 'T':
		    APP_S(remain);
		    errp->words[W_TEXT] = ++word;
		    break;
		case 'c':
		case 'C':
		    cC_base = ((*src) == 'C');
		    APP_S(number);
		    errp->words[W_COLM] = ++word;
		    break;
		case 'l':
		case 'L':
		    lL_base = ((*src) == 'L');
		    APP_S(number);
		    errp->words[W_LINE] = ++word;
		    break;
		case L_BLOCK:
		    range = TRUE;
		    APP_S(before);
		    APP_C;
		    if (src[1] == '^') {
			src++;
			APP_C;
		    }
		    if (src[1] == R_BLOCK) {
			src++;
			APP_C;
		    }
		    break;
		default:
		    src--;
		    break;
		}
		if (mark >= 0) {
		    APP_S(normal);
		    errp->words[mark] = ++word;
		}
	    } else if ((*src == R_BLOCK) && 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) {
	    beginDisplay();
	    dst = temp = typeallocn(char, want + 1);
	    endofDisplay();
	    if (dst == 0)
		break;
	} else {
	    *dst = EOS;
	}
    }
    if (temp != 0) {
#if OPT_TRACE
	TRACE(("COMPILE %s\n", temp));
	for (word = 0; word < W_LAST; word++)
	    TRACE(("word[%d] = %d (%s)\n", word, errp->words[word],
		   get_token_name(word)));
#endif
	TPRINTF(("-> %s\n", temp));
	exp = regcomp(temp, strlen(temp), TRUE);
    }
    errp->exp_text = temp;
    errp->exp_comp = exp;
}

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

#if OPT_UPBUFF
/* ARGSUSED */
int
free_err_exps(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();
    } else if (b_is_changed(bp) || ((L_NUM) exp_count != bp->b_linecount)) {
	free_patterns();
    }
    bsizes(bp);
    if (bp->b_linecount == 0)
	return FALSE;

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

    if (exp_count == 0) {
	beginDisplay();
	exp_count = bp->b_linecount;
	exp_table = typeallocn(ERR_PATTERN, exp_count);
	for (n = 0; n < W_LAST; n++)
	    exp_table->words[n] = -1;
	endofDisplay();

	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(size_t count)
{
    ERR_PATTERN *result = 0;

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

/*
 * Decode the matched ERR_PATTERN
 */
static int
decode_exp(ERR_PATTERN * exp)
{
    /* *INDENT-OFF* */
    static struct {
	ErrTokens code;
	TBUFF **buffer;
	long *number;
    } lookup[] = {
	{ W_VERB, &fe_verb, 0 },
	{ W_FILE, &fe_file, 0 },
	{ W_LINE, 0, &fe_line },
	{ W_COLM, 0, &fe_colm },
	{ W_TEXT, &fe_text, 0 },
    };
    /* *INDENT-ON* */

    regexp *p = exp->exp_comp;
    int j, n;
    int failed = FALSE;
    TBUFF *temp;

    TRACE(("decode_exp{%s}\n", exp->exp_text));

    for (j = 0; j < W_LAST; j++) {
	if (lookup[j].buffer != 0) {
	    tb_free(lookup[j].buffer);
	} else {
	    *(lookup[j].number) = 0;
	}
    }
    fe_colm = cC_base;

    /*
     * Count the atoms separately from the loop indices because when
     * we do
     *          setv $filename-expr='\([a-zA-Z]:\)\?[^ ^I\[:]'
     * the resulting array will have an extra entry with null pointers
     * for the nested atom in the %F expression:
     *          \([a-zA-Z]:\)
     */
    for (n = 1; !failed && (n < NSUBEXP); n++) {
	if (p->startp[n] == 0 || p->endp[n] == 0)
	    continue;		/* discount nested atom */
	if (p->startp[n] >= p->endp[n])
	    continue;		/* discount empty atom */
	temp = 0;
	if (tb_bappend(&temp,
		       p->startp[n],
		       (size_t) (p->endp[n] - p->startp[n])) == 0
	    || tb_append(&temp, EOS) == 0) {
	    (void) no_memory("finderr");
	    failed = TRUE;
	} else if (tb_length(temp) == 0) {
	    mlforce("BUG: marker %d is empty string", n);
	    failed = TRUE;
	} else {
	    for (j = 0; j < W_LAST; j++) {
		ErrTokens code = lookup[j].code;
		if (n == exp->words[code]) {
		    if (lookup[j].buffer) {
			*(lookup[j].buffer) = temp;
			TRACE(("matched %s:%s\n", get_token_name(code),
			       tb_values(temp)));
		    } else {
			*(lookup[j].number) = vl_atol(tb_values(temp), 10, &failed);
			TRACE(("matched %s:%s(%ld)\n", get_token_name(code),
			       tb_values(temp), *(lookup[j].number)));
			if (failed) {
			    mlforce("BUG: \"%s\" (marker %d) is not a number: %s",
				    get_token_name(code),
				    n,
				    tb_values(temp));
			}
			tb_free(&temp);
		    }
		}
	    }
	}
    }

    if (!failed
	&& (fe_colm != cC_base && fe_line == 0)) {
	mlforce("BUG: found column %ld but no line", fe_colm);
	failed = TRUE;
    }

    /*
     * There's not enough room on the message line to show which expression
     * failed - but update $error-expr anyway.
     */
    if (failed)
	var_ERROR_EXPR((TBUFF **) 0, exp->exp_text);

    return failed;
}

/* 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)
{
    BUFFER *sbp;
    int status;
    LINE *dotp;
    int moveddot = FALSE;
    ERR_PATTERN *exp;
    size_t count;

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

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

    static LINE *odotp = 0;

#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) {
	beginDisplay();
	oerrline = -1;
	oerrfile = tb_init(&oerrfile, EOS);
	oerrtext = tb_init(&oerrtext, EOS);
	while (l)
	    free(dirs[l--]);
	odotp = 0;
	endofDisplay();
    }
    dotp = getdot(sbp);

    if (newfebuff || dotp != odotp) {

	LINE *tdotp;

	beginDisplay();
	while (l)
	    free(dirs[l--]);
	endofDisplay();

	tdotp = lforw(buf_head(sbp));

	/* check for Entering/Leaving lines from top of buffer
	 * to dot.  if we'd gotten here in sequence, then we'd
	 * already have tracked them, but since we seem to
	 * have jumped out of sequence we need to recalibrate
	 * the directory stack against our current position.
	 */
	TRACE(("check for Entering/Leaving lines\n"));
	while (tdotp != dotp) {

	    if (lisreal(tdotp)) {
		count = 0;
		while ((exp = next_pattern(count++)) != 0) {
		    if (exp->words[W_VERB] > 0)
			if (lregexec(exp->exp_comp, tdotp, 0, llength(tdotp)))
			    break;
		}

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

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

		    if (errverb != 0 && errfile != 0) {
			beginDisplay();
			if (!strcmp("Entering", errverb)) {
			    if (l < DIRLEVELS) {
				dirs[++l] = strmalloc(errfile);
			    }
			} else if (!strcmp("Leaving", errverb)) {
			    if (l > 0)
				free(dirs[l--]);
			}
			endofDisplay();
		    }
		} else if (interrupted()) {
		    kbd_alarm();
		    return ABORT;
		}
	    }
	    tdotp = lforw(tdotp);
	}
    }
    newfebuff = FALSE;

    TRACE(("look for matching line\n"));
    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)) {
	    count = 0;
	    while ((exp = next_pattern(count++)) != 0
		   && !lregexec(exp->exp_comp, dotp, 0, llength(dotp))) ;

	    if (exp != 0) {
		TRACE(("matched TEXT:%.*s\n", llength(dotp), dotp->l_text));
		if (decode_exp(exp))
		    return ABORT;

		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) {
		    beginDisplay();
		    if (!strcmp("Entering", errverb)) {
			if (l < DIRLEVELS) {
			    dirs[++l] = strmalloc(errfile);
			}
		    } else if (!strcmp("Leaving", errverb)) {
			if (l > 0)
			    free(dirs[l--]);
		    }
		    endofDisplay();
		}
	    }
	}
	if (interrupted()) {
	    kbd_alarm();
	    return ABORT;
	} else if (lforw(dotp) == buf_head(sbp)) {
	    mlwarn("[No more errors in %s buffer]", febuff);
	    /* start over at the top of file */
	    beginDisplay();
	    putdotback(sbp, lforw(buf_head(sbp)));
	    while (l)
		free(dirs[l--]);
	    endofDisplay();
	    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);

    odotp = dotp;

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

    if (strcmp(ferrfile, curbp->b_fname)) {
	/* if we must change windows */
	WINDOW *wp;
	for_each_visible_window(wp) {
	    if (!strcmp(wp->w_bufp->b_fname, ferrfile))
		break;
	}
	if (wp) {
	    curwp = wp;
	    make_current(curwp->w_bufp);
	    upmode();
	} else {
	    status = getfile(ferrfile, TRUE);
	    if (status != TRUE)
		return status;
	}
    }
    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;
    if ((fe_line + lL_base) >= curbp->b_lines_on_disk)
	status = gotoeob(f, n);
    else
	status = gotoline(TRUE, -(curbp->b_lines_on_disk - fe_line + lL_base));
    gocol(fe_colm ? fe_colm - cC_base : 0);

    oerrline = fe_line;
    (void) tb_scopy(&oerrfile, errfile);
    if (status == TRUE) {
	TBUFF *match = 0;
	var_ERROR_EXPR((TBUFF **) 0, exp_table[count - 1].exp_text);
	if (tb_bappend(&match, dotp->l_text, dotp->l_used)
	    && tb_append(&match, EOS) != 0) {
	    var_ERROR_MATCH((TBUFF **) 0, tb_values(match));
	    tb_free(&match);
	}
	updatelistvariables();
    }

    return status;
}

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 = ask_for_bname("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;
}

#define ERR_PREFIX 8

static void
make_err_regex_list(int dum1 GCC_UNUSED, void *ptr GCC_UNUSED)
{
    BUFFER *bp;
    LINE *lp;

    bprintf("--- Error Meta-Expressions and Resulting Regular Expressions %*P",
	    term.cols - 1, '-');
    if (exp_table == 0)
	load_patterns();
    if (exp_table != 0
	&& (bp = find_b_name(ERRORS_BufName)) != 0) {
	size_t j = 0;
	b_set_left_margin(curbp, ERR_PREFIX);
	for_each_line(lp, bp) {
	    int k, first = TRUE;
	    if (j >= exp_count)
		break;
	    bprintf("\n%7d ", j);
	    bprintf("%*S\n", llength(lp), lp->l_text);
	    bprintf("%*S%s", ERR_PREFIX, " ", exp_table[j].exp_text);
	    for (k = 0; k < W_LAST; k++) {
		if (exp_table[j].words[k] != 0) {
		    if (first) {
			bprintf("\n%*S", ERR_PREFIX, " ");
			first = FALSE;
		    } else {
			bprintf(", ");
		    }
		    bprintf("%s=\\%d", get_token_name((ErrTokens) k),
			    exp_table[j].words[k]);
		}
	    }
	    ++j;
	}
    }
}

/* ARGSUSED */
static int
show_ErrRegex(BUFFER *bp GCC_UNUSED)
{
    return liststuff(ERR_REGEX_BufName,
		     FALSE, make_err_regex_list, 0, (void *) 0);
}

int
show_err_regex(int f GCC_UNUSED, int n GCC_UNUSED)
{
    return show_ErrRegex(curbp);
}

#if OPT_UPBUFF
void
update_err_regex(void)
{
    update_scratch(ERR_REGEX_BufName, show_ErrRegex);
}

#else
#define update_err_regex()	/*nothing */
#endif

/*
 * (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);
    }
    update_err_regex();
    return TRUE;
}
#endif
