/* Copyright (c) 1990,1991,1992 Chris and John Downey */
#ifndef lint
static char *sccsid = "@(#)misccmds.c	2.4 (Chris & John Downey) 11/6/92";
#endif

/***

* program name:
    xvi
* function:
    PD version of UNIX "vi" editor, with extensions.
* module name:
    misccmds.c
* module function:
    Miscellaneous functions.

    This module will probably get hacked later and split
    up more sensibly.
* history:
    STEVIE - ST Editor for VI Enthusiasts, Version 3.10
    Originally by Tim Thompson (twitch!tjt)
    Extensive modifications by Tony Andrews (onecom!wldrdg!tony)
    Heavily modified by Chris & John Downey

***/

#include "xvi.h"

/*
 * Add a blank line above or below the current line.
 * Returns TRUE for success, FALSE for failure to get memory.
 *
 * The single boolean parameter tells us whether to split the
 * current line at the cursor position, or just to open a new
 * line leaving the current one intact.
 */
bool_t
openfwd(split_line)
bool_t	split_line;
{
    Line		*l;		/* pointer to newly allocated line */
    register Posn	*oldposn;
    register Line	*oldline;
    register char	*otext;

    oldposn = curwin->w_cursor;
    oldline = oldposn->p_line;
    otext = oldline->l_text;

    /*
     * First find space for new line.
     *
     * By asking for as much space as the prior line had we make sure
     * that we'll have enough space for any auto-indenting.
     */
    l = newline(strlen(otext) + 1);
    if (l == NULL)
	return(FALSE);

    /*
     * Link the new line into the list.
     */
    repllines(curwin, oldline->l_next, 0L, l);

    /*
     * Do auto-indent.
     */
    if (Pb(P_autoindent)) {
	*l->l_text = '\0';
	indentchars = set_indent(l, get_indent(oldline));
    } else {
	indentchars = 0;
    }

    /*
     * If we're in insert mode, we need to move the remainder of the
     * current line onto the new line. Otherwise the new line is left
     * blank.
     */
    if (split_line) {
	char	*s;

	s = otext + oldposn->p_index;

	replchars(curwin, l, indentchars, 0, s);
	replchars(curwin, oldline, oldposn->p_index, strlen(s), "");
    }

    /*
     * Move cursor to the new line.
     */
    move_cursor(curwin, l, indentchars);
    move_window_to_cursor(curwin);
    cursupdate(curwin);
    update_buffer(curbuf);

    return(TRUE);
}

/*
 * Add a blank line above the current line.
 * Returns TRUE for success, FALSE for failure to get memory.
 */
bool_t
openbwd()
{
    Line		*l;
    register Line	*oldline;
    register char	*otext;

    oldline = curwin->w_cursor->p_line;
    otext = oldline->l_text;

    /*
     * First find space for new line.
     */
    l = newline(strlen(otext) + 1);
    if (l == NULL)
	return(FALSE);

    /*
     * Link the new line into the list.
     */
    repllines(curwin, oldline, 0L, l);

    /*
     * Do auto-indent.
     */
    if (Pb(P_autoindent)) {
	*l->l_text = '\0';
	indentchars = set_indent(l, get_indent(oldline));
    } else {
	indentchars = 0;
    }

    /*
     * Ensure the cursor is pointing at the right line.
     */
    move_cursor(curwin, l, indentchars);
    move_window_to_cursor(curwin);
    cursupdate(curwin);
    update_buffer(curbuf);

    return(TRUE);
}

/*
 * Count the number of lines between the two given lines.
 * If the two given lines are the same, the return value
 * is 1, not 0; i.e. the count is inclusive.
 *
 * Note that this function has been changed to give the
 * correct number of lines, even if they are ordered wrongly.
 * This change is backwards-compatible with the old version.
 */
long
cntllines(pbegin, pend)
Line		*pbegin;
register Line	*pend;
{
    register Line	*lp;
    register long	lnum;
    bool_t		swapped = FALSE;

    /*
     * Ensure correct ordering.
     */
    if (later(pbegin, pend)) {
	lp = pbegin;
	pbegin = pend;
	pend = lp;
	swapped = TRUE;
    }

    for (lnum = 1, lp = pbegin; lp != pend; lp = lp->l_next) {
	lnum++;
    }

    if (swapped)
	lnum = - lnum;

    return(lnum);
}

/*
 * plines(lp) - return the number of physical screen lines taken by line 'lp'.
 */
long
plines(win, lp)
Xviwin	*win;
Line	*lp;
{
    register long	col;
    register char	*s;

    s = lp->l_text;

    if (*s == '\0')		/* empty line */
	return(1);

    /*
     * If list mode is on, then the '$' at the end of
     * the line takes up one extra column.
     */
    col = Pb(P_list) ? 1 : 0;

    if (Pb(P_number)) {
	col += NUM_SIZE;
    }

    for ( ; *s != '\0'; s++) {
	col += vischar(*s, (char **) NULL, (int) col);
    }

    {
	register int	row;
	register int	columns;

	columns = win->w_ncols;
	for (row = 1; col > columns; ) {
	    row++;
	    col -= columns;
	}
	return row;
    }
}

/*
 * Count the number of physical lines between the two given lines.
 *
 * This routine is like cntllines(), except that:
 *	it counts physical rather than logical lines
 *	it always returns the absolute number of physical lines
 *	it is non-inclusive
 *	if the physical line count for a group of lines is greater
 *	than or equal to rows * 2, we just return rows * 2; we assume
 *	the caller isn't interested in the exact number.
 */
long
cntplines(win, pbegin, pend)
Xviwin		*win;
Line		*pbegin;
register Line	*pend;
{
    register Line	*lp;
    register long	physlines;
    unsigned		toomuch;

    /*
     * Ensure correct ordering.
     */
    if (later(pbegin, pend)) {
	lp = pbegin;
	pbegin = pend;
	pend = lp;
    }

    toomuch = win->w_nrows * 2;
    for (physlines = 0, lp = pbegin; lp != pend; lp = lp->l_next) {
	physlines += plines(win, lp);
	if (physlines >= toomuch)
	    break;
    }

    return(physlines);
}

/*
 * gotoline(buffer, n) - return a pointer to line 'n' in the given buffer
 *
 * Returns the first line of the file if n is 0.
 * Returns the last line of the file if n is beyond the end of the file.
 */
Line *
gotoline(b, n)
Buffer			*b;
register unsigned long	n;
{
    if (n == 0) {
	return(b->b_file);
    } else {
	register Line	*lp;

	for (lp = b->b_file; --n > 0 && lp->l_next != b->b_lastline;
							lp = lp->l_next) {
	    ;
	}
	return(lp);
    }
}

int
get_indent(lp)
register Line	*lp;
{
    register char   *text;
    register int    indent;
    register int    ts = Pn(P_tabstop);	/* synonym for efficiency */

    if (lp == NULL || (text = lp->l_text) == NULL) {
	show_error(curwin, "Internal error: get_indent(NULL)");
	return 0;
    }

    for (indent = 0; *text != '\0' && (*text == ' ' || *text == '\t');
    								text++) {
	indent += *text == ' ' ? 1 : ts - indent % ts;
    }
    return indent;
}

/*
 * Set number of columns of leading whitespace on line, regardless of
 * what was there before, & return number of characters (not columns)
 * used.
 */
int
set_indent(lp, indent)
Line		*lp;
register int	indent;
{
    register char	*cp;		/* temp char pointer for loops */
    register int	ntabs;		/* number of tabs to use */
    unsigned		nold;		/* no of chars used in new version */
    static Flexbuf	newstr;		/* space for new chars */

    if (lp == NULL || lp->l_text == NULL) {
	show_error(curwin, "Internal error: set_indent(0)");
	return(0);
    }

    /*
     * Find out how many tabs we need, & how many spaces.
     */
    for (ntabs = 0; indent >= Pn(P_tabstop); ntabs++)
	indent -= Pn(P_tabstop);

    /*
     * Find out how many characters were used for initial
     * whitespace in the current (old) line.
     */
    for (cp = lp->l_text; *cp == ' ' || *cp == '\t'; cp++) {
	;
    }
    nold = cp - lp->l_text;

    /*
     * Collect the string of tabs and spaces which will form the new
     * indentation.
     */
    flexclear(&newstr);
    while (ntabs-- > 0)
	flexaddch(&newstr, '\t');
    while (indent-- > 0)
	flexaddch(&newstr, ' ');

    /*
     * Finally, replace the old with the new.
     */
    replchars(curwin, lp, 0, (int) nold, flexgetstr(&newstr));

    /*
     * This is the number of characters we've inserted.
     */
    return flexlen(&newstr);
}

/*
 * tabinout(inout, start, finish)
 *
 * "inout" is either '<' or '>' to indicate left or right shift.
 */
void
tabinout(inout, start, finish)
int	inout;
Line	*start;
Line	*finish;
{
    Line	*lp;
    long	nlines = 0;

    if (!start_command(curwin)) {
	return;
    }

    finish = finish->l_next;

    for (lp = start; lp != finish; lp = lp->l_next) {
	register char *p;

	/*
	 * Find out whether it's a blank line (either
	 * empty or containing only spaces & tabs).
	 * If so, just remove all whitespace from it.
	 */
	for (p = lp->l_text; *p && (*p == '\t' || *p == ' '); p++)
	    ;
	if (*p == '\0') {
	    if (p > lp->l_text) {
		replchars(curwin, lp, 0, p - lp->l_text, "");
	    }
	} else if (inout == '<') {
	    int oldindent = get_indent(lp);

	    (void) set_indent(lp, (oldindent <= Pn(P_shiftwidth) ?
				    0 : oldindent - Pn(P_shiftwidth)));
	} else {
	    (void) set_indent(lp, get_indent(lp) + Pn(P_shiftwidth));
	}

	nlines++;
    }

    end_command(curwin);

    if (nlines > Pn(P_report)) {
	show_message(curwin, "%ld lines %ced", nlines, inout);
    }
}

/*
 * Construct a vector of pointers into each word in
 * the given string. Intervening whitespace will be
 * converted to null bytes.
 *
 * Returned vector is constructed in allocated space,
 * and so must be freed after use.
 *
 * An extra NULL pointer is always allocated for safety.
 *
 * If memory cannot be allocated, or if there are no
 * words in the given string, or if it is a NULL ptr,
 * then the returned values will be 0 and NULL.
 *
 * The "whites" argument is a pointer to an array of
 * characters which are to be considered as whitespace.
 */
void
makeargv(str, argcp, argvp, whites)
char	*str;
int	*argcp;
char	***argvp;
char	*whites;
{
    int		argc;
    char	**argv;
    int		argv_size;

    *argcp = 0;
    *argvp = NULL;

    if (str == NULL)
	return;

    /*
     * Scan past initial whitespace.
     */
    while (*str != '\0' && strchr(whites, *str) != NULL) {
	if (*str == '\\' && strchr(whites, str[1]) != NULL) {
	    str++;
	}
	str++;
    }
    if (*str == '\0')
	return;

    argv = (char **) alloc(sizeof(char *) * 8);
    if (argv == NULL)
	return;
    argv_size = 8;
    argc = 0;

    do {
	if (argc >= (argv_size - 1)) {
	    argv_size += 8;
	    argv = (char **) realloc((char *) argv,
				(unsigned) argv_size * sizeof(char *));
	    if (argv == NULL)
		return;
	}

	argv[argc++] = str;

	while (*str != '\0' && strchr(whites, *str) == NULL) {
	    if (*str == '\\' && strchr(whites, str[1]) != NULL) {
		char	*p;

		/*
		 * What a hack. Copy the rest of the string
		 * down by one byte to remove the backslash.
		 * Don't forget to copy the null byte.
		 */
		for (p = str + 1; (*(p-1) = *p) != '\0'; p++)
		    ;
	    }
	    str++;
	}
	while (*str != '\0' && strchr(whites, *str) != NULL)
	    *str++ = '\0';

    } while (*str != '\0');

    argv[argc] = NULL;

    *argvp = argv;
    *argcp = argc;
}
