/*
 * select.c		-- selection handling code for vile.
 *
 * Author: Kevin Buettner, Paul Fox
 * Creation: 2/26/94
 *
 * Description:  The following code is an attempt to improve the selection
 * mechanism for vile/xvile.  While my initial goal is to improve selection
 * handling for xvile, there is no reason that this code can not be used on
 * other platforms with some kind of pointing device.  In addition, the
 * mechanism is general enough to be used for other kinds of persistent
 * attributed text.
 *
 * For the purposes of this code, a selection is considered to be a region of
 * text which most applications highlight in some manner.  The user may
 * transfer selected text from one application to another (or even to the
 * same application) in some platform dependent way.  The mechanics of
 * transferring the selection are not dealt with in this file.  Procedures
 * for dealing with the representation are maintained in this file.
 *
 * $Header: /usr/build/vile/vile/RCS/select.c,v 1.147 2003/06/30 00:31:33 tom Exp $
 *
 */

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

#if OPT_FILTER
#include	<filters.h>
#endif

#define BTN_BEGIN   1
#define BTN_PASTE   2
#define BTN_EXTEND  3

#define SEL_BEGIN   10		/* button 1 up */
#define SEL_PASTE   11		/* button 2 up */
#define SEL_EXTEND  12		/* button 3 up */
#define SEL_RELEASE 13		/* click on modeline */
#define SEL_FINISH  14		/* finish a selection */

#if OPT_SELECTIONS

static void detach_attrib(BUFFER *bp, AREGION * arp);
static int attribute_cntl_a_sequences(void);
static void free_line_attribs(BUFFER *bp);
static int add_line_attrib(BUFFER *bp, REGION * rp, REGIONSHAPE rs,
			   VIDEO_ATTR vattr, TBUFF * hypercmd);
static void purge_line_attribs(BUFFER *bp, REGION * rp, REGIONSHAPE rs,
			       int owner);

/*
 * startbufp and startregion are used to represent the start of a selection
 * prior to any highlighted text being displayed.  The start and end of
 * the region will both be where the selection is to start.  Although
 * startregion is attached to the buffer indicated by startbufp, nothing
 * will be displayed since the ar_vattr field is zero.  The reason for
 * attaching it to the buffer is to force the MARKs which represent the
 * start of the region to be updated.
 *
 * selbufp and selregion are used to represent a highlighted selection.
 *
 * When startbufp or selbufp are NULL, the corresponding AREGION (startregion or
 * selregion) is not attached to any buffer and is invalid.
 */

static MARK orig_region;
static MARK plus_region;
static BUFFER *startbufp = NULL;
static AREGION startregion;
static BUFFER *selbufp = NULL;
static AREGION selregion;
#if OPT_HYPERTEXT
static TBUFF *hypercmd;
#endif

typedef enum {
    ORIG_FIXED, END_FIXED, UNFIXED
} WHICHEND;

static WHICHEND whichend;

static AREGION *
alloc_AREGION(void)
{
    AREGION *arp;

    beginDisplay();
    if ((arp = typealloc(AREGION)) == NULL) {
	(void) no_memory("AREGION");
    }
    endofDisplay();
    return arp;
}

void
free_attribs(BUFFER *bp)
{
    AREGION *p, *q;

    beginDisplay();
    p = bp->b_attribs;
    while (p != NULL) {
	q = p->ar_next;
#if OPT_HYPERTEXT
	FreeAndNull(p->ar_hypercmd);
#endif
	if (p == &selregion)
	    selbufp = NULL;
	else if (p == &startregion)
	    startbufp = NULL;
	else
	    free((char *) p);
	p = q;
    }
    bp->b_attribs = NULL;

    free_line_attribs(bp);
    endofDisplay();
}

void
free_attrib(BUFFER *bp, AREGION * ap)
{
    detach_attrib(bp, ap);

    beginDisplay();
#if OPT_HYPERTEXT
    FreeAndNull(ap->ar_hypercmd);
#endif
    if (ap == &selregion)
	selbufp = NULL;
    else if (ap == &startregion)
	startbufp = NULL;
    else
	free((char *) ap);
    endofDisplay();
}

static void
detach_attrib(BUFFER *bp, AREGION * arp)
{
    if (find_bp(bp) != 0) {
	if (valid_buffer(bp)) {
	    WINDOW *wp;
	    AREGION **rpp;
	    for_each_visible_window(wp) {
		if (wp->w_bufp == bp)
		    wp->w_flag |= WFHARD;
	    }
	    rpp = &bp->b_attribs;
	    while (*rpp != NULL) {
		if (*rpp == arp) {
		    *rpp = (*rpp)->ar_next;
		    arp->ar_region.r_attr_id = 0;
		    break;
		} else
		    rpp = &(*rpp)->ar_next;
	    }
	}
    }
}

void
find_release_attr(BUFFER *bp, REGION * rp)
{
    if (valid_buffer(bp)) {
	AREGION **rpp;
	rpp = &bp->b_attribs;
	while (*rpp != NULL) {
	    if ((*rpp)->ar_region.r_attr_id == rp->r_attr_id) {
		free_attrib(bp, *rpp);
		break;
	    } else
		rpp = &(*rpp)->ar_next;
	}
    }
}

int
assign_attr_id(void)
{
    static int attr_id;
    return attr_id++;
}

static void
attach_attrib(BUFFER *bp, AREGION * arp)
{
    if (valid_buffer(bp)) {
	WINDOW *wp;
	arp->ar_next = bp->b_attribs;
	bp->b_attribs = arp;
	for_each_visible_window(wp) {
	    if (wp->w_bufp == bp)
		wp->w_flag |= WFHARD;
	}
	arp->ar_region.r_attr_id = (USHORT) assign_attr_id();
    }
}

/*
 * Adjusts dot to last char of last line if dot is past end of buffer.  This
 * can happen when selecting with the mouse.
 */

static void
fix_dot(void)
{
    if (is_header_line(DOT, curwp->w_bufp)) {
	DOT.l = lback(DOT.l);
	DOT.o = llength(DOT.l);
    }
}

/*
 * Output positional information regarding the selection to the message line.
 */

static void
show_selection_position(int yanked)
{
#ifdef WMDTERSELECT
    if (!w_val(curwp, WMDTERSELECT)) {
	mlwrite("(%d,%d) thru (%d,%d) %s",
		line_no(selbufp, selregion.ar_region.r_orig.l),
		getcol(selregion.ar_region.r_orig, FALSE) + 1,
		line_no(selbufp, selregion.ar_region.r_end.l),
		getcol(selregion.ar_region.r_end, FALSE),
		yanked ? "yanked" : "selected");

    }
#endif /* WMDTERSELECT */
    show_mark_is_set('<');
    show_mark_is_set('>');
}

/* Start a selection at dot */
int
sel_begin(void)
{
    fix_dot();
    detach_attrib(startbufp, &startregion);
    plus_region =
	orig_region =
	startregion.ar_region.r_orig =
	startregion.ar_region.r_end = DOT;
    plus_region.o += 1;
    startregion.ar_vattr = 0;
    startregion.ar_shape = EXACT;
#if OPT_HYPERTEXT
    startregion.ar_hypercmd = 0;
#endif
    startbufp = curwp->w_bufp;
    attach_attrib(startbufp, &startregion);
    whichend = UNFIXED;
    return TRUE;
}

static int
dot_vs_mark(void)
{
    int cmp = line_no(curbp, DOT.l) - line_no(curbp, MK.l);
    if (cmp == 0)
	cmp = DOT.o - MK.o;
    return cmp;
}

/* Extend the current selection to dot */
int
sel_extend(int wiping, int include_dot)
{
    REGIONSHAPE save_shape = regionshape;
    REGION a, b;
    WINDOW *wp;
    MARK saved_dot;
    MARK working_dot;

    saved_dot = DOT;
    if (valid_buffer(startbufp)) {
	detach_attrib(selbufp, &selregion);
	selbufp = startbufp;
	selregion = startregion;
	attach_attrib(selbufp, &selregion);
	detach_attrib(startbufp, &startregion);
	startbufp = NULL;
    }

    if (curwp->w_bufp != selbufp)
	return FALSE;		/* handles NULL case also */

    fix_dot();
    regionshape = selregion.ar_shape;

    if (wiping && whichend == END_FIXED)
	MK = selregion.ar_region.r_end;
    else
	MK = selregion.ar_region.r_orig;

    /* FIXME: Make sure DOT and MK are in the same buffer */

    /*
     * If we're extending in the positive direction, we want to include DOT
     * in the selection.  To include DOT, we must advance it one char since
     * a region runs from r_orig up to but not including r_end.
     */
    working_dot = DOT;
    if (include_dot
	&& (selregion.ar_shape == EXACT)
	&& dot_vs_mark() >= 0) {
	if (samepoint(MK, orig_region)) {
	    DOT.o += 1;
	} else if (samepoint(MK, plus_region)) {
	    DOT.o += 1;
	    MK = orig_region;
	}
    }
    if (getregion(&a) == FALSE) {
	return FALSE;
    }
    DOT = working_dot;

    /*
     * Build a second region in the "opposite" direction.
     */
    if (wiping && whichend == ORIG_FIXED)
	MK = selregion.ar_region.r_orig;
    else
	MK = selregion.ar_region.r_end;

    if (include_dot) {
	if (selregion.ar_shape == EXACT) {
	    if (dot_vs_mark() <= 0) {
		if (samepoint(MK, orig_region))
		    MK.o += 1;
	    }
	} else if (selregion.ar_shape == RECTANGLE) {
	    if (samepoint(MK, DOT)) {	/* avoid making empty-region */
		MK = orig_region;
		DOT = plus_region;
	    }
	}
    }
    if (getregion(&b) == FALSE) {
	return FALSE;
    }

    /*
     * The two regions, 'a' and 'b' are _usually_ identical, except for the
     * special case where we've extended one to the right to include the
     * right endpoint of the region.
     *
     * For EXACT selections, setting 'whichend' to ORIG_FIXED means that
     * we're selecting from the anchor point right/down.  Conversely,
     * setting it to END_FIXED means that we selecting left/up.
     *
     * Rectangles are specified by making MK the opposite corner from DOT.
     * If DOT is below MK, we'll say that the selection region is
     * ORIG_FIXED so that the next call on this function will build the
     * regions a/b consistently.
     *
     * If the regions a/b are empty, we've made a mistake; this will cause
     * the selection to be dropped in xvile.
     */

    if (a.r_size > b.r_size) {
	whichend = ORIG_FIXED;
	selregion.ar_region = a;
    } else {
	if (selregion.ar_shape == RECTANGLE) {
	    if (dot_vs_mark() < 0)
		whichend = END_FIXED;
	    else
		whichend = ORIG_FIXED;
	} else {		/* exact or full-line */
	    whichend = END_FIXED;
	}
	selregion.ar_region = b;
    }

    selregion.ar_vattr = VASEL | VOWN_SELECT;
    for_each_visible_window(wp) {
	if (wp->w_bufp == selbufp)
	    wp->w_flag |= WFHARD;
    }

    show_selection_position(FALSE);

    regionshape = save_shape;
    DOT = saved_dot;
    OWN_SELECTION();
    return TRUE;
}

/*
 * Detach current selection (if attached) and null the associated buffer
 * pointer.
 */
void
sel_release(void)
{
    detach_attrib(selbufp, &selregion);
    selbufp = NULL;
}

/*
 * Assert/reassert ownership of selection if appropriate.  This is necessary
 * in order to paste a selection after it's already been pasted once and then
 * modified.
 */

void
sel_reassert_ownership(BUFFER *bp)
{
    if (selbufp == bp) {
	OWN_SELECTION();
    }
}

#if OPT_SEL_YANK
/*
 * Yank the selection.  Return TRUE if selection could be yanked, FALSE
 * otherwise.  Note that this code will work even if the buffer being
 * yanked from is not attached to any window since it creates its own
 * fake window in order to perform the yanking.
 */

int
sel_yank(int reg)
{
    REGIONSHAPE save_shape;
    WINDOW *save_wp;
    BUFFER *save_bp = curbp;
    int code = FALSE;

    if (valid_window(save_wp = push_fake_win(selbufp))) {
	/*
	 * We're not guaranteed that curbp and selbufp are the same.
	 */
	save_shape = regionshape;

	curbp = selbufp;

	ukb = (short) reg;
	kregflag = 0;
	haveregion = &selregion.ar_region;
	regionshape = selregion.ar_shape;
	yankregion();
	haveregion = NULL;

	pop_fake_win(save_wp, save_bp);

	regionshape = save_shape;

	show_selection_position(TRUE);

	/* put cursor back on screen...is there a cheaper way to do this?  */
	(void) update(FALSE);
	code = TRUE;
    }
    return code;
}

/* select all text in curbp and yank to unnamed register */
int
sel_all(int f GCC_UNUSED, int n GCC_UNUSED)
{
    int rc;
    MARK savedot;

    savedot = DOT;
    gotobob(0, 0);
    sel_begin();
    gotoeob(0, 0);
    gotoeol(0, 0);
    (void) sel_setshape(EXACT);
    rc = sel_extend(TRUE, TRUE);
    DOT = savedot;
    if (rc)
	sel_yank(0);
    return (rc);
}
#endif /* OPT_SEL_YANK */

BUFFER *
sel_buffer(void)
{
    return valid_buffer(startbufp) ? startbufp : selbufp;
}

static int
get_selregion(REGION * result)
{
    if (valid_buffer(startbufp)) {
	*result = startregion.ar_region;
	return TRUE;
    } else if (valid_buffer(selbufp)) {
	*result = selregion.ar_region;
	return TRUE;
    } else {
	return FALSE;
    }
}

int
sel_get_leftmark(MARK *result)
{
    REGION my_region;
    if (get_selregion(&my_region)) {
	*result = my_region.r_orig;
	return TRUE;
    }
    return FALSE;
}

int
sel_get_rightmark(MARK *result)
{
    REGION my_region;
    if (get_selregion(&my_region)) {
	*result = my_region.r_end;
	return TRUE;
    }
    return FALSE;
}

int
sel_setshape(REGIONSHAPE shape)
{
    if (valid_buffer(startbufp)) {
	startregion.ar_shape = shape;
	return TRUE;
    } else if (valid_buffer(selbufp)) {
	selregion.ar_shape = shape;
	return TRUE;
    } else {
	return FALSE;
    }
}

/* return a region which goes from DOT to the far end of the current
	selection region.  shape is maintained.  returns pointer to static
	region struct.
*/
static REGION *
extended_region(void)
{
    REGION *rp = NULL;
    static REGION a, b;
    MARK savemark;

    savemark = MK;
    regionshape = selregion.ar_shape;
    MK = selregion.ar_region.r_orig;
    DOT.o += 1;
    if (getregion(&a) == TRUE) {
	DOT.o -= 1;
	MK = selregion.ar_region.r_end;
	if (regionshape == FULLLINE)
	    MK.l = lback(MK.l);
	/* region b is to the end of the selection */
	if (getregion(&b) == TRUE) {
	    /* if a is bigger, it's the one we want */
	    if (a.r_size > b.r_size)
		rp = &a;
	    else
		rp = &b;
	}
    } else {
	DOT.o -= 1;
    }
    MK = savemark;
    return rp;
}

static int doingopselect;

/* ARGSUSED */
int
sel_motion(int f GCC_UNUSED, int n GCC_UNUSED)
{
    if (selbufp == NULL) {
	mlwrite("[No selection exists.]");
	return FALSE;
    }

    if (selbufp != curbp) {
	/* FIXME -- sure would be nice if we could do non-destructive
	   things to other buffers, mainly yank. */
	mlwrite("[Selection not in current buffer.]");
	return FALSE;
    }

    curwp->w_flag |= WFMOVE;

    /* if this is happening on behalf of an operator, we're pretending
     * that the motion took us from one end of the selection to the
     * other, unless we're trying to select to the selection, in which
     * case that would be self-defeating
     */
    if (doingopcmd && !doingopselect) {
	pre_op_dot = selregion.ar_region.r_orig;	/* move us there */
	haveregion = &selregion.ar_region;
	regionshape = selregion.ar_shape;
	return TRUE;
    }

    if (!doingopcmd) {		/* it's a simple motion -- go to the top of selection */
	/* remember -- this can never be used with an operator, as in
	 * "delete to the selection", since that case is taken care
	 * of above, and is really the whole reason for this
	 * "motion" in the first place.  */
	DOT = selregion.ar_region.r_orig;	/* move us there */
	return TRUE;
    }

    /* we must be doing an extension */
    haveregion = extended_region();
    return haveregion ? TRUE : FALSE;

}

static int
selectregion(void)
{
    int status;
    REGION region;
    MARK savedot;
    MARK savemark;
    int hadregion = FALSE;

    savedot = DOT;
    savemark = MK;
    memset(&region, 0, sizeof(region));
    if (haveregion) {		/* getregion() will clear this, so
				   we need to save it */
	region = *haveregion;
	hadregion = TRUE;
    }
    status = yankregion();
    DOT = savedot;
    MK = savemark;
    if (status != TRUE)
	return status;
    if (hadregion || ((status = getregion(&region)) == TRUE)) {
	detach_attrib(startbufp, &startregion);
	detach_attrib(selbufp, &selregion);
	selbufp = curbp;
	selregion.ar_region = region;
	selregion.ar_vattr = VASEL | VOWN_SELECT;
	selregion.ar_shape = regionshape;
#if OPT_HYPERTEXT
	selregion.ar_hypercmd = 0;
#endif
	attach_attrib(selbufp, &selregion);
	OWN_SELECTION();
    }
    return status;
}

void
do_sweep(int flag)
{
    if (doingsweep != flag) {
	if ((flag && !doingsweep)
	    || (!flag && doingsweep))
	    term.cursorvis(!flag);
	doingsweep = flag;
    }
}

static void
sweepmsg(const char *msg)
{
    char temp[NLINE];
    (void) kcod2pstr(fnc2kcod(&f_multimotion), temp);
    mlforce("[%s (end with %*S)]", msg, *temp, temp + 1);
}

static int
release_selection(int status)
{
    TRACE(("MOUSE release selection\n"));
    if (doingsweep) {
	do_sweep(FALSE);
	if (status != TRUE)
	    mlforce("[Sweeping: Aborted]");
	else
	    mlerase();
    }
    sel_release();
    return status;
}

#if OPT_MOUSE
int
paste_selection(void)
{
    if (!doingsweep) {
	TRACE(("MOUSE paste selection\n"));
	if (b_val(curbp, MDVIEW)) {
	    kbd_alarm();
	    return FALSE;
	}
	mayneedundo();
	return putafter(FALSE, 1);
    }
    return SEL_PASTE;
}

void
on_double_click(void)
{
    MARK save;

    save = DOT;
    TRACE(("MOUSE double-click DOT %d.%d\n", line_no(curbp, DOT.l), DOT.o));
    sel_release();
    if (!is_at_end_of_line(DOT)
	&& !isSpace(char_at(DOT))) {
	while (DOT.o >= 0) {
	    DOT.o--;
	    if (isSpace(char_at(DOT))) {
		DOT.o++;
		break;
	    }
	}
	sel_begin();
	MK = DOT;
	while (!is_at_end_of_line(DOT)) {
	    DOT.o++;
	    if (is_at_end_of_line(DOT)
		|| isSpace(char_at(DOT))) {
		DOT.o--;
		break;
	    }
	}
	sel_extend(FALSE, TRUE);
    }
    DOT = save;
    update(TRUE);
}

void
on_triple_click(void)
{
    MARK save;

    save = DOT;
    TRACE(("MOUSE triple-click DOT %d.%d\n", line_no(curbp, DOT.l), DOT.o));
    sel_release();
    gotobol(FALSE, 1);
    sel_begin();
    MK = DOT;
    gotoeol(FALSE, 1);
    sel_extend(FALSE, TRUE);
    DOT = save;
    update(TRUE);
}

/*
 * On button press, we get an explicit number (1,2,3), and on release we don't
 * really know which button, but assume it is the last-pressed button.
 */
int
on_mouse_click(int button, int y, int x)
{
    static int first_x, first_y, pending;
    WINDOW *this_wp, *that_wp;
    int status;

    if (button > 0) {
	if (valid_window(this_wp = row2window(y))
	    && (y != mode_row(this_wp))) {
	    /*
	     * If we get a click on the "<" marking the left side
	     * of a shifted window, force the screen right. This
	     * makes it more consistent if there's a tab.
	     */
	    if (w_val(this_wp, WVAL_SIDEWAYS)
		&& x == 0) {
		mvleftwind(FALSE, 1);
	    }
	    if (!doingsweep) {
		if (button == BTN_EXTEND) {
		    first_x = offs2col(this_wp, this_wp->w_dot.l, this_wp->w_dot.o);
		    first_y = line_no(this_wp->w_bufp, this_wp->w_dot.l)
			- line_no(this_wp->w_bufp, this_wp->w_line.l);
		} else {
		    first_x = x;
		    first_y = y;
		}
	    }
	    status = setcursor(y, x);
	    /*
	     * Check for button1-down while we're in multimotion
	     * sweep, so we can suppress highlighting extension.
	     */
	    if (button != BTN_EXTEND
		&& status == TRUE
		&& doingsweep) {
		status = SORTOFTRUE;
		if (button == BTN_BEGIN) {
		    first_x = x;
		    first_y = y;
		}
	    }
	} else {		/* pressed button on modeline */
	    status = SORTOFTRUE;
	    first_x = x;
	    first_y = y;
	}
	pending = button;
    } else if (pending) {
	button = pending;
	pending = FALSE;
	this_wp = row2window(y);
	that_wp = row2window(first_y);
	if (this_wp == 0
	    || that_wp == 0
	    || reading_msg_line) {
	    TRACE(("MOUSE cannot move msg-line\n"));
	    status = FALSE;
	} else if (insertmode
		   && (this_wp != curwp || that_wp != curwp)) {
	    TRACE(("MOUSE cannot move from window while inserting\n"));
	    kbd_alarm();
	    status = ABORT;
	} else if (first_y == mode_row(that_wp)) {	/* drag modeline? */
	    if (first_y == y) {
		sel_release();
		status = SEL_RELEASE;
	    } else {
		WINDOW *save_wp = curwp;
		TRACE(("MOUSE dragging modeline\n"));
		set_curwp(that_wp);
		status = shrinkwind(FALSE, first_y - y);
		set_curwp(save_wp);
	    }
	} else if (y != first_y || x != first_x) {	/* drag selection */
	    if (button == BTN_PASTE) {
		(void) setcursor(y, x);
		status = paste_selection();
	    } else if (doingsweep) {
		switch (button) {
		case BTN_BEGIN:
		    (void) release_selection(TRUE);
		    status = setcursor(first_y, first_x);
		    if (status == TRUE) {
			MK = DOT;
			status = SEL_BEGIN;
			TRACE(("MOUSE setting SEL_BEGIN MK %d.%d\n",
			       line_no(curbp, MK.l), MK.o));
		    }
		    break;
		case BTN_PASTE:
		    (void) setcursor(y, x);
		    status = paste_selection();
		    break;
		default:
		    (void) setcursor(y, x);
		    status = SEL_EXTEND;
		    TRACE(("MOUSE setting SEL_EXTEND DOT %d.%d MK %d.%d\n",
			   line_no(curbp, MK.l), MK.o,
			   line_no(curbp, DOT.l), DOT.o));
		    break;
		}
	    } else {
		TRACE(("MOUSE begin multimotion on button%d-up\n", button));
		if (button == BTN_EXTEND) {
		    (void) setcursor(y, x);
		    y = first_y;
		    x = first_x;
		}
		do_sweep(SORTOFTRUE);
		(void) sel_begin();
		(void) sel_setshape(EXACT);
		(void) setcursor(y, x);
		status = multimotion(TRUE, 1);
		TRACE(("MOUSE end multimotion after button%d-up\n", button));
		if (status == SEL_PASTE)
		    status = paste_selection();
	    }
	} else {		/* position the cursor */
	    TRACE(("MOUSE button %d position cursor\n", button));
	    (void) setcursor(y, x);
	    switch (button) {
	    case BTN_BEGIN:
		status = SEL_FINISH;
		break;
	    case BTN_PASTE:
		status = paste_selection();
		break;
	    default:
		status = release_selection(TRUE);
		break;
	    }
	}
    } else {
	TRACE(("MOUSE ignored (illegal state)\n"));
	status = FALSE;
    }

    if (status == TRUE || status >= SORTOFTRUE)
	(void) update(TRUE);

    TRACE(("MOUSE status:%d\n", status));
    return status;
}
#endif

int
multimotion(int f, int n)
{
    const CMDFUNC *cfp;
    int status, c, waserr;
    int pasting;
    REGIONSHAPE shape;
    MARK savedot;
    MARK savemark;
    MARK realdot;
    BUFFER *origbp = curbp;
    static int wassweephack = FALSE;

    /* Use the repeat-count as a shortcut to specify the type of selection.
     * I'd use int-casts of the enum value, but declaring enums with
     * specific values isn't 100% portable.
     */
    if (!f || n <= 0)
	n = 1;
    if (n == 3)
	regionshape = RECTANGLE;
    else if (n == 2)
	regionshape = FULLLINE;
    else
	regionshape = EXACT;
    shape = regionshape;

    sweephack = FALSE;
    savedot = DOT;
    switch (doingsweep) {
    case TRUE:			/* the same command terminates as starts the sweep */
	if (doingsweep) {
	    do_sweep(FALSE);
	}
	mlforce("[Sweeping: Completed]");
	regionshape = shape;
	/* since the terminating 'q' is executed as a motion, we have
	   now lost the value of sweephack we were interested in, the
	   one that tells us to include DOT.o in the selection.
	   so we preserved it in wassweephack, and restore it here.
	 */
	if (wassweephack)
	    sweephack = wassweephack;
	return TRUE;
    case SORTOFTRUE:
	if (doingsweep != TRUE) {
	    do_sweep(TRUE);
	}
	sweepmsg("Begin cursor sweep...");
	sel_extend(TRUE, (regionshape != RECTANGLE && sweephack));
	savedot = MK;
	TRACE(("MOUSE BEGIN DOT: %d.%d MK %d.%d\n",
	       line_no(curbp, DOT.l), DOT.o,
	       line_no(curbp, MK.l), MK.o));
	break;
    case FALSE:
	if (doingsweep != TRUE) {
	    do_sweep(TRUE);
	}
	sweepmsg("Begin cursor sweep...");
	(void) sel_begin();
	(void) sel_setshape(shape);
	break;
    }

    waserr = TRUE;		/* to force message "state-machine" */
    realdot = DOT;
    pasting = FALSE;

    while (doingsweep) {

	/* Fix up the screen    */
	(void) update(FALSE);

	/* get the next command from the keyboard */
	c = kbd_seq();

	if (ABORTED(c)
	    || curbp != origbp) {
	    return release_selection(FALSE);
	}

	f = FALSE;
	n = 1;

	do_repeats(&c, &f, &n);

	/* and execute the command */
	cfp = SelectKeyBinding(c);
	if ((cfp != NULL)
	    && ((cfp->c_flags & (GOAL | MOTION)) != 0)) {
	    MARK testdot;

	    wassweephack = sweephack;
	    sweephack = FALSE;
	    TRACE(("MOUSE TEST DOT: %d.%d MK %d.%d\n",
		   line_no(curbp, DOT.l), DOT.o,
		   line_no(curbp, MK.l), MK.o));
	    testdot = DOT;

	    status = execute(cfp, f, n);
	    switch (status) {
	    case SEL_RELEASE:
		TRACE(("MOUSE SEL_RELEASE %d.%d\n",
		       line_no(curbp, DOT.l), DOT.o));
		return release_selection(TRUE);

	    case SEL_PASTE:
		pasting = TRUE;
		/* FALLTHRU */

	    case SEL_FINISH:
		do_sweep(FALSE);
		break;

	    case SORTOFTRUE:
		TRACE(("MOUSE selection pending %d.%d -> %d.%d\n",
		       line_no(curbp, realdot.l), realdot.o,
		       line_no(curbp, testdot.l), testdot.o));
		realdot = testdot;
		break;

	    case SEL_BEGIN:
		savedot = MK;
		TRACE(("MOUSE SEL_BEGIN...\n"));
		/*FALLTHRU */

	    case SEL_EXTEND:
		TRACE(("MOUSE SEL_EXTEND from %d.%d to %d.%d\n",
		       line_no(curbp, savedot.l), savedot.o,
		       line_no(curbp, DOT.l), DOT.o));
		/*FALLTHRU */

	    case TRUE:
		if (waserr && doingsweep) {
		    sweepmsg("Sweeping...");
		    waserr = FALSE;
		}
		realdot = DOT;
		DOT = savedot;
		(void) sel_begin();
		DOT = realdot;
		TRACE(("MOUSE LOOP save: %d.%d real %d.%d, mark %d.%d\n",
		       line_no(curbp, savedot.l), savedot.o,
		       line_no(curbp, realdot.l), realdot.o,
		       line_no(curbp, MK.l), MK.o));
		(void) sel_setshape(shape);
		/* we sometimes want to include DOT.o in the
		   selection (unless it's a rectangle, in
		   which case it's taken care of elsewhere)
		 */
		sel_extend(TRUE, (regionshape != RECTANGLE &&
				  sweephack));
		break;

	    default:
		sweepmsg("Sweeping: Motion failed.");
		waserr = TRUE;
		break;
	    }
	} else {
	    sweepmsg("Sweeping: Only motions permitted");
	    waserr = TRUE;
	}

    }
    regionshape = shape;
    /* if sweephack is set here, it's because the last motion had
       it set */
    if (doingopcmd)
	pre_op_dot = savedot;

    savedot = DOT;
    savemark = MK;
    DOT = realdot;
    TRACE(("MOUSE SAVE DOT: %d.%d MK %d.%d\n",
	   line_no(curbp, DOT.l), DOT.o,
	   line_no(curbp, MK.l), MK.o));
    if ((regionshape != RECTANGLE) && sweephack) {
	if (dot_vs_mark() < 0)
	    MK.o += 1;
	else
	    DOT.o += 1;
    }
    status = yankregion();
    DOT = savedot;
    MK = savemark;

    sweephack = wassweephack = FALSE;

    if (status == TRUE && pasting)
	status = SEL_PASTE;

    return status;
}

/*ARGSUSED*/
int
multimotionfullline(int f GCC_UNUSED, int n GCC_UNUSED)
{
    return multimotion(TRUE, 2);
}

/*ARGSUSED*/
int
multimotionrectangle(int f GCC_UNUSED, int n GCC_UNUSED)
{
    return multimotion(TRUE, 3);
}

#if OPT_PERL || OPT_TCL || SYS_WINNT
BUFFER *
get_selection_buffer_and_region(AREGION * arp)
{
    if (selbufp) {
	*arp = selregion;
    }
    return selbufp;
}
#endif

int
apply_attribute(void)
{
    return (VATTRIB(videoattribute) != 0
#if OPT_HYPERTEXT
	    || tb_length(hypercmd) != 0
#endif
	);
}

int
attributeregion(void)
{
    int status;
    REGION region;
    AREGION *arp;

    if ((status = getregion(&region)) == TRUE) {
	if (apply_attribute()) {
	    if (add_line_attrib(curbp, &region, regionshape, videoattribute,
#if OPT_HYPERTEXT
				hypercmd
#else
				0
#endif
		))
		return TRUE;

	    /* add new attribute-region */
	    if ((arp = alloc_AREGION()) == NULL) {
		return FALSE;
	    }
	    arp->ar_region = region;
	    arp->ar_vattr = videoattribute;	/* include ownership */
	    arp->ar_shape = regionshape;
#if OPT_HYPERTEXT
	    arp->ar_hypercmd = 0;
	    if (tb_length(hypercmd)) {
		arp->ar_hypercmd = strmalloc(tb_values(hypercmd));
		tb_init(&hypercmd, 0);
	    }
#endif
	    attach_attrib(curbp, arp);
	} else {		/* purge attributes in this region */
	    L_NUM rls = line_no(curbp, region.r_orig.l);
	    L_NUM rle = line_no(curbp, region.r_end.l);
	    C_NUM ros = region.r_orig.o;
	    C_NUM roe = region.r_end.o;
	    AREGION *p, *q, *n;
	    int owner;

	    owner = VOWNER(videoattribute);

	    purge_line_attribs(curbp, &region, regionshape, owner);

	    for (p = curbp->b_attribs; p != 0; p = q) {
		L_NUM pls, ple;
		C_NUM pos, poe;

		if (interrupted())
		    return FALSE;

		q = p->ar_next;

		if (owner != 0 && owner != VOWNER(p->ar_vattr))
		    continue;

		pls = line_no(curbp, p->ar_region.r_orig.l);
		ple = line_no(curbp, p->ar_region.r_end.l);
		pos = p->ar_region.r_orig.o;
		poe = p->ar_region.r_end.o;

		/* Earlier the overlapping region check was made based only
		 * on line numbers and so was right only for FULLINES shape
		 * changed it to be correct for EXACT and RECTANGLE also
		 * -kuntal 9/13/98
		 */
		/*
		 * check for overlap:
		 * for any shape of region 'p' things are fine as long as
		 * 'region' is above or below it
		 */
		if (ple < rls || pls > rle)
		    continue;
		/*
		 * for EXACT 'p' region
		 */
		if (p->ar_shape == EXACT) {
		    if (ple == rls && poe - 1 < ros)
			continue;
		    if (pls == rle && pos > roe)
			continue;
		}
		/*
		 * for RECTANGLE 'p' region
		 */
		if (p->ar_shape == RECTANGLE)
		    if (poe < ros || pos > roe)
			continue;

		/*
		 * FIXME: this removes the whole of an overlapping region;
		 * we really only want to remove the overlapping portion...
		 */

		/*
		 * we take care of this fix easily as long as neither of
		 * 'p' or 'region' are RECTANGLE. we will need to create
		 * at the most one new region in case 'region' is
		 * completely contained within 'p'
		 */
		if (p->ar_shape != RECTANGLE && regionshape != RECTANGLE) {
		    if ((rls > pls) || (rls == pls && ros > pos)) {
			p->ar_shape = EXACT;
			if ((rle < ple) || (rle == ple && roe < poe)) {
			    /* open a new region */
			    if ((n = alloc_AREGION()) == NULL) {
				return FALSE;
			    }
			    n->ar_region = p->ar_region;
			    n->ar_vattr = p->ar_vattr;
			    n->ar_shape = p->ar_shape;
#if OPT_HYPERTEXT
			    n->ar_hypercmd = p->ar_hypercmd;
#endif
			    n->ar_region.r_orig.l = (region.r_end.l);
			    n->ar_region.r_orig.o = (region.r_end.o);
			    attach_attrib(curbp, n);
			}
			p->ar_region.r_end.l = (region.r_orig.l);
			p->ar_region.r_end.o = (region.r_orig.o);
			curwp->w_flag |= WFHARD;
			continue;
		    } else if ((rle < ple) || (rle == ple && roe < poe)) {
			p->ar_region.r_orig.l = (region.r_end.l);
			p->ar_region.r_orig.o = (region.r_end.o);
			curwp->w_flag |= WFHARD;
			continue;
		    }
		}

		free_attrib(curbp, p);
	    }
	}
    }
    return status;
}

int
attributeregion_in_region(REGION * rp,
			  REGIONSHAPE shape,
			  VIDEO_ATTR vattr,
			  char *hc)
{
    haveregion = rp;
    DOT = rp->r_orig;
    MK = rp->r_end;
    if (shape == FULLLINE)
	MK.l = lback(MK.l);
    regionshape = shape;	/* Not that the following actually cares */
    videoattribute = vattr;
#if OPT_HYPERTEXT
    tb_scopy(&hypercmd, hc);
#endif /* OPT_HYPERTEXT */
    return attributeregion();
}

int
operselect(int f, int n)
{
    int status;
    opcmd = OPOTHER;
    doingopselect = TRUE;
    status = vile_op(f, n, selectregion, "Select");
    doingopselect = FALSE;
    return status;
}

int
operattrbold(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = VABOLD | VOWN_OPERS;
    return vile_op(f, n, attributeregion, "Set bold attribute");
}

int
operattrital(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = VAITAL | VOWN_OPERS;
    return vile_op(f, n, attributeregion, "Set italic attribute");
}

int
operattrno(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = 0;		/* clears no matter who "owns" */
    return vile_op(f, n, attributeregion, "Set normal attribute");
}

int
operattrrev(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = VAREV | VOWN_OPERS;
    return vile_op(f, n, attributeregion, "Set reverse attribute");
}

int
operattrul(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = VAUL | VOWN_OPERS;
    return vile_op(f, n, attributeregion, "Set underline attribute");
}

#if OPT_HYPERTEXT
static int
attributehyperregion(void)
{
    char line[NLINE];
    int status;

    line[0] = 0;
    status = mlreply_no_opts("Hypertext Command: ", line, NLINE);

    if (status != TRUE)
	return status;

    tb_scopy(&hypercmd, line);
    return attributeregion();
}

int
operattrhc(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = 0;
    return vile_op(f, n, attributehyperregion, "Set hypertext command");
}

static int
hyperspray(int (*f) (char *))
{
    L_NUM dlno;
    int doff;
    AREGION *p;
    int count = 0;

    (void) bsizes(curbp);	/* attach line numbers to each line */

    dlno = DOT.l->l_number;
    doff = DOT.o;

    for (p = curbp->b_attribs; p != 0; p = p->ar_next) {
	if (p->ar_hypercmd) {
	    int slno, elno, soff, eoff;

	    slno = p->ar_region.r_orig.l->l_number;
	    elno = p->ar_region.r_end.l->l_number;
	    soff = p->ar_region.r_orig.o;
	    eoff = p->ar_region.r_end.o;

	    if (((slno == dlno && doff >= soff) || dlno > slno)
		&& ((elno == dlno && doff < eoff) || dlno < elno)) {
		f(p->ar_hypercmd);
		count++;
		/* As originally written, there was no break below.
		   This means that we'd loop over all of the
		   attributes in case there were multiple
		   overlapping attributes with attached hypertext
		   commands.  As cool as this may sound, it is
		   actually of very dubious utility, and an
		   action which removes some or all of the
		   attributes very quickly gets this loop into
		   trouble.  So, if this functionality is really
		   desired, we'll have to either make a copy of
		   the attributes or somehow otherwise determine
		   that they were modified or deleted.  */
		break;
	    }
	}
    }
    return count;
}

static int
doexechypercmd(char *cmd)
{
    return docmd(cmd, TRUE, FALSE, 1);
}

/*ARGSUSED*/
int
exechypercmd(int f GCC_UNUSED, int n GCC_UNUSED)
{
    int count;
    count = hyperspray(doexechypercmd);
    return count != 0;
}

static int
doshowhypercmd(char *cmd)
{
    mlforce("%s", cmd);
    return 1;
}

/*ARGSUSED*/
int
showhypercmd(int f GCC_UNUSED, int n GCC_UNUSED)
{
    int count;
    count = hyperspray(doshowhypercmd);
    return count != 0;
}
#endif

int
operattrcaseq(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = 0;
    return vile_op(f, n, attribute_cntl_a_sequences,
		   "Attribute ^A sequences");
}

int
attribute_cntl_a_seqs_in_region(REGION * rp, REGIONSHAPE shape)
{
    haveregion = rp;
    DOT = rp->r_orig;
    MK = rp->r_end;
    if (shape == FULLLINE)
	MK.l = lback(MK.l);
    regionshape = shape;	/* Not that the following actually cares */
    return attribute_cntl_a_sequences();
}

/*
 * Setup for iteration over region to attribute, ensure that DOT < MK.
 */
LINEPTR
setup_region(void)
{
    LINEPTR pastline;		/* pointer to line just past EOP */

    if (!sameline(MK, DOT)) {
	REGION region;
	if (getregion(&region) != TRUE)
	    return 0;
	if (sameline(region.r_orig, MK))
	    swapmark();
    }
    pastline = MK.l;
    if (pastline != win_head(curwp))
	pastline = lforw(pastline);
    DOT.o = 0;
    regionshape = EXACT;

    return pastline;
}

/*
 * Parse a cntl_a sequence, returning the number of characters processed.
 * Set videoattribute and hypercmd as side-effects.
 */
int
decode_attribute(char *text, int length, int offset, int *countp)
{
    int c;			/* current char during scan */
    int count = 0;
    int found;
#if OPT_HYPERTEXT
    int save_offset;
#endif

    while (text[offset] == CONTROL_A) {
	found = FALSE;
	count = 0;
	offset++;
	while (offset < length) {
	    c = text[offset];
	    if (isDigit(c)) {
		count = count * 10 + c - '0';
		offset++;
	    } else
		break;
	}
	if (count == 0)
	    count = 1;
	videoattribute = VOWN_CTLA;
	while (offset < length
	       && (c = text[offset]) != CONTROL_A
	       && !found) {
	    switch (c) {
	    case 'C':
		/* We have color. Get color value */
		offset++;
		c = text[offset];
		if (isDigit(c))
		    videoattribute |= VCOLORATTR(c - '0');
		else if ('A' <= c && c <= 'F')
		    videoattribute |= VCOLORATTR(c - 'A' + 10);
		else if ('a' <= c && c <= 'f')
		    videoattribute |= VCOLORATTR(c - 'a' + 10);
		else
		    offset--;	/* Invalid attribute */
		break;

	    case 'U':
		videoattribute |= VAUL;
		break;
	    case 'B':
		videoattribute |= VABOLD;
		break;
	    case 'R':
		videoattribute |= VAREV;
		break;
	    case 'I':
		videoattribute |= VAITAL;
		break;
#if OPT_HYPERTEXT
	    case 'H':
		save_offset = offset;
		offset++;
		while (offset < length
		       && text[offset] != EOS)
		    offset++;

		if (offset < length) {
		    tb_init(&hypercmd, EOS);
		    tb_bappend(&hypercmd,
			       &text[save_offset + 1],
			       offset - save_offset);
		    tb_append(&hypercmd, EOS);
		} else {	/* skip bad hypertext string */
		    offset = save_offset;
		}
		break;
#endif
	    case ':':
		found = TRUE;
		break;

	    default:
		offset--;
		found = TRUE;
		break;
	    }
	    offset++;
	}
	if (videoattribute != VOWN_CTLA && count != 0)
	    break;
    }
    *countp = count;
    return offset;
}

/*
 * DOT points to the beginning of a region, we're given the count of characters
 * to put into the region.  Set MK at the end.  This handles counts that extend
 * beyond the current line, but makes assumptions about the record separator.
 * Data from an external filter always uses newline for a separator, otherwise
 * we will run into problems with lex/flex.  Internally computed regions are
 * not the same problem.
 */
static void
set_mark_after(int count, int rslen)
{
    int offset = DOT.o;

    MK = DOT;
    while (count > 0) {
	MK.o += count;
	if (is_last_line(MK, curbp)) {
	    count = 0;
	} else if (MK.o > llength(MK.l)) {
	    if (MK.o <= (llength(MK.l) + rslen)) {
		MK.o = llength(MK.l);
		break;
	    }
	    count -= (llength(MK.l) + rslen - offset);
	    MK.l = lforw(MK.l);
	    MK.o = 0;
	} else {
	    break;
	}
	offset = 0;
    }
}

/*
 * attribute_cntl_a_sequences can take quite a while when processing a region
 * with a large number of attributes.  The reason for this is that the number
 * of marks to check for fixing (from ldelete) increases with each attribute
 * that is added.  It is not really necessary to check the attributes that
 * we are adding in attribute_cntl_a_sequences due to the order in which
 * they are added (i.e, none of them ever need to be fixed up when ldelete
 * is called from within attribute_cntl_a_sequences).
 *
 * It is still necessary to update those attributes which existed (if any)
 * prior to calling attribute_cntl_a_sequences.
 *
 * We define EFFICIENCY_HACK to be 1 if we want to enable the code which
 * will prevent ldelete from doing unnecessary work.  Note that we are
 * depending on the fact that attach_attrib() adds new attributes to the
 * beginning of the list.  It is for this reason that I consider this
 * code to be somewhat hacky.
 */
#define EFFICIENCY_HACK 1

static int
attribute_cntl_a_sequences(void)
{
    LINEPTR pastline;
    C_NUM offset;		/* offset in cur line of place to attribute */
    int count;

#if EFFICIENCY_HACK
    AREGION *orig_attribs;
    AREGION *new_attribs;
    orig_attribs = new_attribs = curbp->b_attribs;
#endif

    if ((pastline = setup_region()) == 0)
	return FALSE;

    while (DOT.l != pastline) {
	if (interrupted())
	    return FALSE;
	while (DOT.o < llength(DOT.l)) {
	    if (char_at(DOT) == CONTROL_A) {
		offset = decode_attribute(DOT.l->l_text, llength(DOT.l),
					  DOT.o, &count);
#if EFFICIENCY_HACK
		new_attribs = curbp->b_attribs;
		curbp->b_attribs = orig_attribs;
		ldelete((B_COUNT) (offset - DOT.o), FALSE);
		curbp->b_attribs = new_attribs;
#else
		ldelete((B_COUNT) (offset - DOT.o), FALSE);
#endif
		set_mark_after(count, len_record_sep(curbp));
		if (apply_attribute())
		    (void) attributeregion();
	    } else {
		DOT.o++;
	    }
	}
	DOT.l = lforw(DOT.l);
	DOT.o = 0;
    }
    return TRUE;
}

#if OPT_SHELL || OPT_FILTER
static void
discard_syntax_highlighting(void)
{
    detach_attrib(selbufp, &selregion);
    detach_attrib(startbufp, &startregion);
    free_attribs(curbp);
}
#endif

/*
 * Apply attributes from a filtering command on the current buffer.  The
 * buffer is not modified.
 */
#if OPT_SHELL
static int
attribute_from_filter(void)
{
    LINEPTR pastline;
    int skip;
    int nbytes;
    int done;
    int n;
    int result = TRUE;
    int drained = FALSE;

    if ((pastline = setup_region()) == 0) {
	result = FALSE;

#ifdef MDHILITE
    } else if (!b_val(curbp, MDHILITE)) {
	discard_syntax_highlighting();
#endif

    } else if (open_region_filter() == TRUE) {

	discard_syntax_highlighting();
	while (DOT.l != pastline) {

	    if (interrupted()) {
		result = FALSE;
		break;
	    }

	    if (ffgetline(&nbytes) > FIOSUC) {
		drained = TRUE;
		break;
	    }

	    DOT.o = 0;
	    for (n = 0; n < nbytes; n++) {
		if (fflinebuf[n] == CONTROL_A) {
		    done = decode_attribute(fflinebuf, nbytes, n, &skip);
		    if (done) {
			n = (done - 1);
			set_mark_after(skip, 1);
			if (apply_attribute())
			    (void) attributeregion();
		    }
		} else {
		    DOT.o += 1;
		}
	    }
	    DOT.l = lforw(DOT.l);
	}

	/* some pipes will hang if they're not drained */
	if (!drained) {
	    while (ffgetline(&nbytes) <= FIOSUC) {
		;
	    }
	}

	(void) ffclose();	/* Ignore errors.       */
	attach_attrib(selbufp, &selregion);
	attach_attrib(startbufp, &startregion);
#if OPT_HILITEMATCH
	if (curbp->b_highlight & HILITE_ON) {
	    curbp->b_highlight |= HILITE_DIRTY;
	    attrib_matches();
	}
#endif
    }
    return result;
}

int
operattrfilter(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = 0;
    return vile_op(f, n, attribute_from_filter,
		   "Attribute ^A sequences from filter");
}
#endif /*  OPT_SHELL */

#if OPT_FILTER
static int
attribute_directly(void)
{
    int code = FALSE;

#if OPT_MAJORMODE
    if (valid_buffer(curbp)) {
	discard_syntax_highlighting();
	if (b_val(curbp, MDHILITE)) {
	    char *filtername = 0;
	    TBUFF *token = 0;

	    if (clexec || isnamedcmd)
		filtername = mac_tokval(&token);

	    if (filtername == 0
		&& curbp->majr != 0)
		filtername = curbp->majr->name;

	    if (filtername != 0
		&& flt_start(filtername)) {
		TRACE(("attribute_directly(%s) using %s\n",
		       curbp->b_bname,
		       filtername));
		flt_finish();
		code = TRUE;
	    }
	    tb_free(&token);
	}
	attach_attrib(selbufp, &selregion);
	attach_attrib(startbufp, &startregion);
    }
#endif
    return code;
}

int
operattrdirect(int f, int n)
{
    opcmd = OPOTHER;
    videoattribute = 0;
    return vile_op(f, n, attribute_directly,
		   "Attribute directly with internal filter");
}
#endif

#if OPT_LINE_ATTRS

#define INIT_LINE_ATTR_TBL() \
    if (!line_attr_tbl[0].in_use) init_line_attr_tbl()

static void
init_line_attr_tbl(void)
{
    /* Slot 0 indicates no more line attributes */
    line_attr_tbl[0].in_use = TRUE;
    line_attr_tbl[0].vattr = 0;
    /* Slot 1 indicates a normal attribute */
    line_attr_tbl[1].in_use = TRUE;
    line_attr_tbl[1].vattr = 0;
}

/* Find an index in line_attr_tbl[] containing the specified attribute.
   Add the attribute to the table if not found.  Return -1 if table is
   full. (Kevin's note: I don't think the table full condition will
   be a real problem.  But if it is, it should be possible to garbage
   collect the table.) */
static int
find_line_attr_idx(VIDEO_ATTR vattr)
{
    int hash = 0;
    int start;
    UINT i;
    VIDEO_ATTR v;

    INIT_LINE_ATTR_TBL();

    if (vattr == 0)
	return 1;		/* Normal attributes get mapped to index 1 */

    v = vattr;
    for (i = 0; i < sizeof(VIDEO_ATTR); i++) {
	hash ^= v & 0xff;
	v >>= 8;
    }

    start = hash;
    while (line_attr_tbl[hash].in_use) {

	if (line_attr_tbl[hash].vattr == vattr)
	    return hash;

	hash++;
	if (hash == start)
	    return -1;

	if (hash >= N_chars)
	    hash = 2;		/* No point starting at 0, since we know
				   that 0 and 1 must be in use. */
    }

    line_attr_tbl[hash].vattr = vattr;
    line_attr_tbl[hash].in_use = TRUE;

    return hash;
}

/* Attempt to shift a portion of a line either left or right for
   inserts or deletes.  The idea is to preserve the line attributes
   as much as possible until autocolor gets around to recoloring the
   line */
/* ARGSUSED */
void
lattr_shift(BUFFER *bp GCC_UNUSED, LINEPTR lp, int doto, int shift)
{
    UCHAR *lap;
    if (!lp->l_attrs)
	return;
    lap = lp->l_attrs;
    if (shift > 0) {
	int f, t, len;
	len = strlen((char *) lap);
	t = len - 1;
	if (t <= 0)
	    return;
	for (f = t; f >= doto && f > t - shift; f--)
	    if (lap[f] != 1) {
		int newlen;

		beginDisplay();
		newlen = len + shift - (t - f);
		lap = castrealloc(UCHAR, lap, newlen + 1);
		lp->l_attrs = lap;
		lap[newlen] = 0;
		t = newlen - 1;
		f = t - shift;
		endofDisplay();
		break;
	    }
	while (f > doto) {
	    lap[t--] = lap[f--];
	}
    } else if (shift < 0) {
	int f, t;
	int saw_attr = 0;
	shift = -shift;
	/* Move t to doto, but don't run off end */
	for (t = 0; t < doto && lap[t]; t++)
	    saw_attr |= (lap[t] != 1);
	if (lap[t] == 0)
	    return;
	/* Position f, but don't run off end */
	for (f = t; f < doto + shift && lap[f]; f++) ;
	/* Shift via copying, but observe what it is we shift */
	while (lap[f]) {
	    saw_attr |= (lap[f] != 1);
	    lap[t++] = lap[f++];
	}
	/* Try to get rid of the line attributes entirely */
	if (!saw_attr) {
	    FreeAndNull(lp->l_attrs);
	    return;
	}
	/* Normal out the stuff at the end. */
	while (t < f)
	    lap[t++] = 1;
    }
}

#endif /* OPT_LINE_ATTRS */

static void
free_line_attribs(BUFFER *bp)
{
#if OPT_LINE_ATTRS
    LINE *lp;
    int do_update = 0;
    for_each_line(lp, bp) {
	do_update |= (lp->l_attrs != 0);
	FreeAndNull(lp->l_attrs);
    }
    if (do_update) {
	WINDOW *wp;
	for_each_visible_window(wp) {
	    if (wp->w_bufp == bp)
		wp->w_flag |= WFHARD;
	}
    }
#endif /* OPT_LINE_ATTRS */
}

static int
add_line_attrib(BUFFER *bp, REGION * rp, REGIONSHAPE rs, VIDEO_ATTR vattr,
		TBUFF * hypercmdp)
{
#if OPT_LINE_ATTRS
    LINEPTR lp;
    WINDOW *wp;
    int vidx;
    int i;
    int overlap = FALSE;
    int last;

    if (rp->r_orig.l != rp->r_end.l	/* must be confined to one line */
	|| rs != EXACT		/* must be an exact region */
	|| (hypercmdp && tb_length(hypercmdp) != 0)
    /* can't be a hypertext command */
	|| vattr == 0		/* can't be normal */
	|| (vattr & VASEL) != 0)	/* can't be a selection */
	return FALSE;

    beginDisplay();

    lp = rp->r_orig.l;
    last = rp->r_end.o;

    if (lp->l_attrs != NULL) {
	int len = strlen((char *) (lp->l_attrs));
	/* Make sure the line attribute is long enough */
	if (len < rp->r_end.o) {
	    last = rp->r_end.o;
	    lp->l_attrs = castrealloc(UCHAR, lp->l_attrs, last + 1);
	    if (lp->l_attrs != NULL) {
		for (i = len; i < rp->r_end.o; i++)
		    lp->l_attrs[i] = 1;
		lp->l_attrs[i] = 0;
	    }
	}
	/* See if attributed region we're about to add overlaps an existing
	   line based one */
	if (lp->l_attrs != NULL) {
	    for (i = rp->r_orig.o; i < last; i++) {
		if (lp->l_attrs[i] != 1) {
		    overlap = TRUE;
		    break;
		}
	    }
	}
    } else {
	/* Must allocate and initialize memory for the line attributes */
	lp->l_attrs = castalloc(UCHAR, llength(lp) + 1);
	lp->l_attrs[llength(lp)] = 0;
	for (i = llength(lp) - 1; i >= 0; i--)
	    lp->l_attrs[i] = 1;
	if (last > llength(lp))
	    last = llength(lp);
    }
    endofDisplay();

    if (lp->l_attrs != NULL && !overlap) {
	if ((vidx = find_line_attr_idx(vattr)) >= 0) {
	    for (i = rp->r_orig.o; i < last; i++)
		lp->l_attrs[i] = (UCHAR) vidx;

	    for_each_visible_window(wp) {
		if (wp->w_bufp == bp)
		    wp->w_flag |= WFHARD;
	    }
	    return TRUE;
	}
    }
#endif /* OPT_LINE_ATTRS */
    return FALSE;
}

static void
purge_line_attribs(BUFFER *bp, REGION * rp, REGIONSHAPE rs, int owner)
{
#if OPT_LINE_ATTRS
    LINEPTR ls = rp->r_orig.l;
    LINEPTR le = rp->r_end.l;
    int os = rp->r_orig.o;
    int oe = rp->r_end.o;
    LINEPTR lp;
    int i;
    int do_update = 0;

    for (lp = ls; lp != buf_head(curbp); lp = lforw(lp)) {
	if (lp->l_attrs != 0) {
	    for (i = 0; i < llength(lp); i++) {
		if (lp->l_attrs[i] == 0)
		    break;	/* at end of attrs */
		if (lp->l_attrs[i] == 1)
		    continue;	/* normal, so proceed to next one */
		if (rs != FULLLINE) {
		    if ((rs == RECTANGLE || lp == ls) && i < os)
			continue;
		    if ((rs == RECTANGLE || lp == le) && i >= oe)
			break;
		}
		if (owner == 0
		    || owner == VOWNER(line_attr_tbl[lp->l_attrs[i]].vattr)) {
		    /* If we get here, set it back to normal */
		    lp->l_attrs[i] = 1;
		    do_update = 1;
		}
	    }
	}
	if (lp == le)
	    break;
    }
    if (do_update) {
	WINDOW *wp;
	for_each_visible_window(wp) {
	    if (wp->w_bufp == bp)
		wp->w_flag |= WFHARD;
	}
    }
#endif /* OPT_LINE_ATTRS */
}

/* release selection, if any */
int
sel_clear(int f GCC_UNUSED, int n GCC_UNUSED)
{
    sel_release();
    return (TRUE);
}

#endif /* OPT_SELECTIONS */
