ftp.nice.ch/pub/next/unix/editor/elvis-2.0.N.bs.tar.gz#/elvis-2.0.N.bs/buffer.c

This is buffer.c in view mode; [Download] [Up]

/* buffer.c */
/* Copyright 1995 by Steve Kirkendall */

char id_buffer[] = "$Id: buffer.c,v 2.68 1996/09/26 01:05:21 steve Exp $";

#include "elvis.h"

#define swaplong(x,y)	{long tmp; tmp = (x); (x) = (y); (y) = tmp;}

#if USE_PROTOTYPES
static void proc(_BLKNO_ bufinfo, long nchars, long nlines, long changes, long prevloc, CHAR *name);
static void freeundo(BUFFER buffer);
static void bufdo(BUFFER buf, BOOLEAN wipe);
# ifdef DEBUG_ALLOC
static void checkundo(char *where);
static void removeundo(struct undo_s *undo);
# endif
#endif

/* This variable points to the head of a linked list of buffers */
BUFFER buffers;

/* This is the default buffer.  Its options have been inserted into the
 * list accessible via optset().  This variable should only be changed by
 * the bufoptions() function.
 */
BUFFER bufdefault;

/* This stores the message type that will be used for reporting the number
 * of lines read or written.  It is normally MSG_STATUS so that other messages
 * will be allowed to overwrite it; however, when quitting it is set to
 * MSG_INFO so messages will be queued and eventually displayed somewhere
 * else after the window is closed.  It is also set to MSG_INFO during
 * initialization so the "read..." message appears in the new window.
 */
MSGIMP bufmsgtype = MSG_INFO;

/* This buffer's contents are irrelevent.  The values of its options, though,
 * are significant because the values of its options are used as the default 
 * values of any new buffer.  This buffer is also used as the default buffer
 * during execution of the initialization scripts.
 */
BUFFER bufdefopts;

/* This array describes buffer options */
static OPTDESC bdesc[] =
{
	{"filename", "file",	optsstring,	optisstring	},
	{"bufname", "buffer",	optsstring,	optisstring	},
	{"buflines", "bl",	optnstring,	optisnumber	},
	{"bufchars", "bc",	optnstring,	optisnumber	},
	{"retain", "ret",	NULL,		NULL		},
	{"modified", "mod",	NULL,		NULL,		},
	{"edited", "samename",	NULL,		NULL,		},
	{"newfile", "new",	NULL,		NULL,		},
	{"readonly", "ro",	NULL,		NULL,		},
	{"autoindent", "ai",	NULL,		NULL,		},
	{"inputtab", "itab",	opt1string,	optisoneof,	"tab spaces filename"},
	{"autotab", "at",	NULL,		NULL,		},
	{"tabstop", "ts",	optnstring,	optisnumber,	"1:100"},
	{"ccprg", "cp",		optsstring,	optisstring	},
	{"equalprg", "ep",	optsstring,	optisstring	},
	{"keywordprg", "kp",	optsstring,	optisstring	},
	{"makeprg", "mp",	optsstring,	optisstring	},
	{"paragraphs", "para",	optsstring,	optisstring	},
	{"sections", "sect",	optsstring,	optisstring	},
	{"shiftwidth", "sw",	optnstring,	optisnumber	},
	{"undolevels", "ul",	optnstring,	optisnumber	},
	{"textwidth", "tw",	optnstring,	optisnumber	},
	{"internal", "internal",NULL,		NULL		},
	{"bufdisplay", "bd",    optsstring,	optisstring	},
	{"errlines", "errlines",optnstring,	optisnumber	},
	{"binary", "bin",	NULL,		NULL		}
};

#ifdef DEBUG_ALLOC
/* This are used for maintaining a linked list of all undo versions. */
struct undo_s *undohead, *undotail;

/* This function is called after code which is suspected of leaking memory.
 * It checks all of the undo versions, making sure that each one is still
 * accessible via some buffer.
 */
static void checkundo(where)
	char	*where;
{
	struct undo_s	*scan, *undo;
	BUFFER		buf;

	/* for each undo version... */
	for (scan = undohead; scan; scan = scan->link1)
	{
		/* make sure the buffer still exists */
		for (buf = buffers; buf != scan->buf; buf = buf->next)
		{
			if (!buf)
				msg(MSG_FATAL, "[s]$1 - buffer disappeared, undo/redo not freed", where);
		}

		/* make sure this is an undo/redo for this buffer */
		if (scan->undoredo == 'l')
		{
			if (scan != buf->undolnptr)
			{
				msg(MSG_FATAL, "[s]$1 - undolnptr version leaked", where);
			}
		}
		else
		{
			for (undo = scan->undoredo=='u' ? buf->undo : buf->redo;
			     undo != scan;
			     undo = undo->next)
			{
				if (!undo)
					msg(MSG_FATAL, "[ss]$1 - $2 version leaked", where, scan->undoredo=='u'?"undo":"redo");
			}
		}
	}
}

static void removeundo(undo)
	struct undo_s	*undo;
{
	if (undo->link1)
		undo->link1->link2 = undo->link2;
	else
		undotail = undo->link2;
	if (undo->link2)
		undo->link2->link1 = undo->link1;
	else
		undohead = undo->link1;
}
#else
# define checkundo(s)
#endif

/* This function is called during session file initialization.  It creates
 * a BUFFER struct for the buffer, and collects the undo versions.
 */
static void proc(bufinfo, nchars, nlines, changes, prevloc, name)
	_BLKNO_	bufinfo;	/* block describing the buffer */
	long	nchars;		/* number of characters in buffer */
	long	nlines;		/* number of lines in buffer */
	long	changes;	/* value of "changes" counter */
	long	prevloc;	/* offset of most recent change to buffer */
	CHAR	*name;		/* name of the buffer */
{
	BUFFER		buf;
	BLKNO		tmp;
	struct undo_s	*undo, *scan, *lag;

	/* try to find a buffer by this name */
	for (buf = buffers; buf && CHARcmp(o_bufname(buf), name); buf = buf->next)
	{
	}

	/* if no buffer exists yet, then create one and make it use the old
	 * bufinfo block.
	 */
	if (!buf)
	{
		buf = bufalloc(name, bufinfo);
		buf->bufinfo = bufinfo;
		o_buflines(buf) = nlines;
		o_bufchars(buf) = nchars;
		buf->changes = changes;
		buf->changepos = prevloc;

		/* guess some values for a few other critical options */
		if (!CHARncmp(name, toCHAR("Elvis "), 6) &&
			CHARncmp(name, toCHAR("Elvis untitled"), 14))
		{
			/* probably an internal buffer */
			optpreset(o_internal(buf), True, OPT_HIDE);
			optpreset(o_modified(buf), False, OPT_HIDE);
		}
		else
		{
			/* the filename is probably the same as the buffer name */
			optpreset(o_filename(buf), CHARdup(name), OPT_FREE|OPT_HIDE);
			optpreset(o_internal(buf), False, OPT_HIDE);

			/* Mark it as readonly so the user will have to think
			 * before clobbering an existing file.
			 */
			o_readonly(buf) = True;

			/* Mark it as modified, so the user has to think
			 * before exitting and losing this session file.
			 */
			optpreset(o_modified(buf), True, OPT_HIDE);
		}
		return;
	}

	/* We found the buffer.  Is this the newest version found so far? */
	if (changes > buf->changes)
	{
		/* yes, this is the newest.  Swap this one with the version
		 * currently in the buf struct.  That will leave this version
		 * (the newest) as the current version, and the current
		 * (second newest) in the arguements and ready to be added
		 * to the undo list.
		 */
		tmp = buf->bufinfo;
		buf->bufinfo = bufinfo;
		bufinfo = tmp;
		swaplong(o_buflines(buf), nlines);
		swaplong(o_bufchars(buf), nchars);
		swaplong(buf->changes, changes);
		swaplong(buf->changepos, prevloc);
	}

	/* insert as an "undo" version */
	undo = (struct undo_s *)safealloc(1, sizeof *undo);
	undo->changes = changes;
	undo->changepos = prevloc;
	undo->buflines = nlines;
	undo->bufchars = nchars;
	undo->bufinfo = bufinfo;
	for (scan = buf->undo, lag = NULL;
	     scan && scan->changes > changes;
	     lag = scan, scan = scan->next)
	{
	} 
	undo->next = scan;
	if (lag)
	{
		lag->next = undo;
	}
	else
	{
		buf->undo = undo;
	}
#ifdef DEBUG_ALLOC
	undo->link1 = undohead;
	undohead = undo;
	undo->link2 = NULL;
	if (undo->link1)
		undo->link1->link2 = undo;
	else
		undotail = undo;
	undo->buf = buf;
	undo->undoredo = 'u';
#endif
}

/* Restart a session */
void bufinit()
{
	assert(BUFOPTQTY == QTY(bdesc));

	/* find any buffers left over from a previous edit */
	lowinit(proc);

	/* create the default options buffer, if it doesn't exist already */
	bufdefopts = bufalloc(toCHAR(DEFAULT_BUF), 0);
	o_internal(bufdefopts) = True;
	bufoptions(bufdefopts);
}

/* Create a buffer with a given name.  The buffer will initially be empty;
 * if it is meant to be associated with a particular file, then the file must
 * be copied into the buffer in a separate operation.  If there is already
 * a buffer with the desired name, it returns a pointer to the old buffer
 * instead of creating a new one.
 */
BUFFER bufalloc(name, bufinfo)
	CHAR	*name;	/* name of the buffer */
	_BLKNO_	bufinfo;/* block number describing the buffer (0 to create) */
{
	BUFFER	buffer;
	BUFFER	scan, lag;	/* used for inserting new buffer */
	char	unique[255];	/* name of untitled buffer */
	int	i = 1;		/* for generating a name for untitled buffer */

	/* if no name was specified, generate a unique untitled name */
	if (!name)
	{
		do
		{
			sprintf(unique, UNTITLED_BUF, i++);
		} while (buffind(toCHAR(unique)));
		name = toCHAR(unique);
	}

	/* see if there's already a buffer with that name */
	buffer = buffind(name);
	if (buffer)
	{
		return buffer;
	}

	/* allocate buffer struct */
	buffer = (BUFFER)safekept(1, sizeof(*buffer));

	/* create a low-level buffer */
	if (bufinfo)
	{
		buffer->bufinfo = bufinfo;
	}
	else
	{
		buffer->bufinfo = lowalloc(tochar8(name));
	}

	/* initialize the buffer options */
	optpreset(o_readonly(buffer), o_defaultreadonly, OPT_HIDE);
	if (bufdefopts)
	{
		/* copy all options except the following: filename bufname
		 * buflines bufchars modified edited newfile internal autotab
		 */
		optpreset(buffer->retain, bufdefopts->retain, OPT_HIDE);
		buffer->autoindent = bufdefopts->autoindent;
		buffer->inputtab = bufdefopts->inputtab;
		buffer->tabstop = bufdefopts->tabstop;
		buffer->shiftwidth = bufdefopts->shiftwidth;
		buffer->undolevels = bufdefopts->undolevels;
		buffer->textwidth = bufdefopts->textwidth;
		buffer->autotab = bufdefopts->autotab;

		/* Strings are tricky, because we may need to allocate a
		 * duplicate of the value.
		 */
		buffer->cc = bufdefopts->cc;
		if (buffer->cc.flags & OPT_FREE)
			o_cc(buffer) = CHARdup(o_cc(bufdefopts));
		buffer->cc.flags |= OPT_HIDE;
		buffer->equalprg = bufdefopts->equalprg;
		if (buffer->equalprg.flags & OPT_FREE)
			o_equalprg(buffer) = CHARdup(o_equalprg(bufdefopts));
		buffer->keywordprg = bufdefopts->keywordprg;
		if (buffer->keywordprg.flags & OPT_FREE)
			o_keywordprg(buffer) = CHARdup(o_keywordprg(bufdefopts));
		buffer->make = bufdefopts->make;
		if (buffer->make.flags & OPT_FREE)
			o_make(buffer) = CHARdup(o_make(bufdefopts));
		buffer->make.flags |= OPT_HIDE;
		buffer->paragraphs = bufdefopts->paragraphs;
		if (buffer->paragraphs.flags & OPT_FREE)
			o_paragraphs(buffer) = CHARdup(o_paragraphs(bufdefopts));
		buffer->sections = bufdefopts->sections;
		if (buffer->sections.flags & OPT_FREE)
			o_sections(buffer) = CHARdup(o_sections(bufdefopts));
		buffer->bufdisplay = bufdefopts->bufdisplay;
		if (buffer->bufdisplay.flags & OPT_FREE)
			o_bufdisplay(buffer) = CHARdup(o_bufdisplay(bufdefopts));
	}
	else /* no default options -- set them explicitly */
	{
		o_inputtab(buffer) = 't';
		o_autotab(buffer) = True;
		o_tabstop(buffer) = 8;
#ifdef OSCCPRG
		o_cc(buffer) = toCHAR(OSCCPRG);
#else
		o_cc(buffer) = toCHAR("cc ($1?$1:$2)");
#endif
		o_equalprg(buffer) = toCHAR("fmt");
		o_keywordprg(buffer) = toCHAR("ref");
#ifdef OSMAKEPRG
		o_make(buffer) = toCHAR(OSMAKEPRG);
#else
		o_make(buffer) = toCHAR("make $1");
#endif
		o_paragraphs(buffer) = toCHAR("PPppIPLPQPP");
		o_sections(buffer) = toCHAR("NHSHSSSEse");
		o_shiftwidth(buffer) = 8;
		o_undolevels(buffer) = 0;
		o_bufdisplay(buffer) = toCHAR("normal");
		optflags(o_textwidth(buffer)) = OPT_REDRAW;
	}

	/* set the name of this buffer, and limit access to some options */
	optpreset(o_bufname(buffer), CHARkdup(name), OPT_HIDE|OPT_LOCK|OPT_FREE);
	optflags(o_buflines(buffer)) = OPT_HIDE|OPT_LOCK;
	optflags(o_bufchars(buffer)) = OPT_HIDE|OPT_LOCK;

	/* Add the buffer to the linked list of buffers.  Keep it sorted. */
	for (lag = (BUFFER)0, scan = buffers;
	     scan && CHARcmp(o_bufname(scan), o_bufname(buffer)) < 0;
	     lag = scan, scan = scan->next)
	{
	}
	buffer->next = scan;
	if (lag)
	{
		lag->next = buffer;
	}
	else
	{
		buffers = buffer;
	}

	/* return the new buffer */
	return buffer;
}

/* Locate a buffer with a particular name.  (Note that this uses the buffer
 * name, not the filename.)  Returns a pointer to the buffer, or NULL for
 * unknown buffers.
 */
BUFFER buffind(name)
	CHAR	*name;	/* name of the buffer to find */
{
	BUFFER buffer;

	/* scan through buffers, looking for a match */
	for (buffer = buffers; buffer && CHARcmp(name, o_bufname(buffer)); buffer = buffer->next)
	{
	}
	return buffer;
}

/* Read a text file or filter output into a specific place in the buffer */
BOOLEAN bufread(mark, rfile)
	MARK	mark;	/* where to insert the new next */
	char	*rfile;	/* file to read, or !cmd for filter */
{
	BUFFER	buf;		/* the buffer we're reading into */
	long	offset;		/* offset of mark before inserting text */
	long	origlines;	/* original number of lines in file */
	CHAR	chunk[4096];	/* I/O buffer */
	int	nread;		/* number of bytes in chunk[] */
	BOOLEAN	newbuf;		/* is this a new buffer? */

	/* initialize some vars */
	buf = markbuffer(mark);
	newbuf = (BOOLEAN)(o_bufchars(buf) == 0 && rfile[0] != '!' && (o_verbose || !o_internal(buf)));
	origlines = o_buflines(buf);

	/* open the file/filter */
	if (!ioopen(rfile, 'r', False, False, o_binary(markbuffer(mark))))
	{
		msg(MSG_ERROR, "[s]error opening $1", rfile);
		return False;
	}

	/* read the text */
	if (newbuf)
		msg(MSG_STATUS, "[s]reading $1", rfile);
	while ((nread = ioread(chunk, QTY(chunk))) > 0)
	{
		if (guipoll(False))
		{
			ioclose();
			return False;
		}
		offset = markoffset(mark);
		bufreplace(mark, mark, chunk, nread);
		marksetoffset(mark, offset + nread);
	}
	ioclose();
	if (newbuf)
		msg(bufmsgtype, "[sdd]read $1, $2 lines, $3 chars", rfile,
					o_buflines(buf), o_bufchars(buf));
	else if (!o_internal(buf))
		msg(bufmsgtype, "[d]read $1 lines", o_buflines(buf) - origlines);

	return True;
}

/* Create a buffer for a given file, and then load the file.  Return a pointer
 * to the buffer.  If the file can't be read for some reason, then complain and
 * leave the buffer empty, but still return the empty buffer.
 *
 * If the buffer already exists and contains text, then the "reload" option
 * can be used to force it to discard that text and reload the buffer; when
 * "reload" is False, it would leave the buffer unchanged instead.
 */
BUFFER bufload(bufname, filename, reload)
	CHAR	*bufname;	/* name of buffer, or NULL to derive from filename */
	char	*filename;	/* name of file to load into a buffer */
	BOOLEAN	reload;		/* load from file even if previously loaded? */
{
	BUFFER	buf;
	MARKBUF	top;
	MARKBUF	end;
	BUFFER	initbuf;	/* buffer containing the initialization script */
	int	i;


	/* Create a buffer, whose name defaults to the same as this file */
	buf = bufalloc(bufname ? bufname : toCHAR(filename), 0);

	/* Does the buffer already contain text? */
	if (o_bufchars(buf) > 0)
	{
		/* If we aren't supposed to reload, then just return the
		 * buffer as-is.
		 */
		if (!reload)
			return buf;

		/* Save the text as an "undo" version, and then delete it */
		if (windefault && markbuffer(windefault->cursor) == buf)
			bufwilldo(windefault->cursor);
		else
			bufwilldo(marktmp(top, buf, 0));
		bufreplace(marktmp(top, buf, 0), marktmp(end, buf, o_bufchars(buf)), (CHAR *)0, 0);
	}

	/* Set the buffer's options */
	optpreset(o_filename(buf), CHARkdup(filename), OPT_HIDE|OPT_LOCK|OPT_FREE);
	optpreset(o_internal(buf), (bufname ? True : False), OPT_HIDE);
	optpreset(o_edited(buf), True, OPT_HIDE);
	o_readonly(buf) = False;
	optpreset(o_newfile(buf), False, OPT_HIDE);
	switch (dirperm(filename))
	{
	  case DIR_INVALID:
	  case DIR_BADPATH:
	  case DIR_NOTFILE:
	  case DIR_UNREADABLE:
	  case DIR_READONLY:
		o_readonly(buf) = True;
		break;

	  case DIR_NEW:
		o_newfile(buf) = True;
		break;

	  case DIR_READWRITE:
		/* nothing needed */
		break;
	}

	/* Execute the "before read"  script, if it exists.  If the script
	 * fails, then don't load the newly-created buffer.
	 */
	if (!o_internal(buf))
	{
		initbuf = buffind(toCHAR(BEFOREREAD_BUF));
		if (initbuf)
		{
			/* make the buffer available to :set */
			bufoptions(buf);

			/* execute the script */
			if (experform(windefault, marktmp(top, initbuf, 0),
				marktmp(end, initbuf, o_bufchars(initbuf))) != RESULT_COMPLETE)
			{
				return buf;
			}
		}
	}

	/* read the file's contents into the buffer */
	if (o_newfile(buf))
	{
		msg(bufmsgtype, "[sdd]$1 [NEW FILE]", filename);
	}
	else if (!bufread(marktmp(top, buf, 0), filename))
	{
		o_edited(buf) = False;
		return buf;
	}

	/* set other options to describe the file */
	o_modified(buf) = False;
	optpreset(o_errlines(buf), o_buflines(buf), OPT_HIDE);

	/* Restore the marks to their previous offsets.  Otherwise any marks
	 * which refer to this buffer will be set to the end of the file.
	 * Restoring them isn't perfect, but it beats setting them all to EOF!
	 * (Note: New buffers won't have an "undo" version.)
	 */
	if (buf->undo)
	{
		for (i = 0; i < QTY(buf->undo->offset); i++)
		{
			/* ignore unset marks, or marks into other buffers*/
			if (buf->undo->offset[i] < 0)
				continue;
			assert(markbuffer(namedmark[i]) == buf);

			/* marks past the new end should be freed */
			if (buf->undo->offset[i] >= o_bufchars(buf))
			{
				markfree(namedmark[i]);
				continue;
			}

			/* other marks should have their offsets restored
			 * to what they were before the buffer was loaded
			 */
			marksetoffset(namedmark[i], buf->undo->offset[i]);
		}
	}

	/* execute the file initialization script, if it exists */
	if (!o_internal(buf))
	{
		initbuf = buffind(toCHAR(AFTERREAD_BUF));
		if (initbuf)
		{
			/* make the buffer available to :set */
			bufoptions(buf);

			/* Execute the script's contents. */
			(void)experform(windefault, marktmp(top, initbuf, 0),
				marktmp(end, initbuf, o_bufchars(initbuf)));
		}
	}

	return buf;
}


/* This function searches through a path for a file to load.  It then loads
 * that file into a buffer and returns the buffer.  If the file couldn't be
 * located, it returns NULL instead.  If a buffer already exists with the given
 * name, then it returns that buffer without attempting to load anything.
 */
BUFFER bufpath(path, filename, bufname)
	CHAR	*path;		/* path to search through */
	char	*filename;	/* file to search for */
	CHAR	*bufname;	/* name of buffer to store the file */
{
	char	*pathname;	/* full pathname of the loaded file */
	char	pathdup[256];	/* local copy of pathname */
	BUFFER	buf;

	/* if the buffer already exists, return it immediately */
	buf = buffind(bufname);
	if (buf)
	{
		return buf;
	}

	/* try to find the file */
	pathname = iopath(tochar8(path), filename, False);
	if (!pathname)
	{
		return (BUFFER)0;
	}

	/* we need a local copy of the pathname, because bufload() will also
	 * call iopath() to find the "elvis.brf" and "elvis.arf" files, and
	 * iopath() only has a single static buffer that it uses for returning
	 * the found pathname.  We don't want our pathname clobbered.
	 */
	strcpy(pathdup, pathname);

	/* load the file */
	buf = bufload(bufname, pathdup, True);
	return buf;
}


/* This function deletes the oldest undo versions of a given buffer */
static void freeundo(buffer)
	BUFFER	buffer;	/* buffer to be cleaned */
{
	struct undo_s *undo, *other, *tail;
	int	      i;

	checkundo("before freundo()");

	/* locate the most recent doomed version */
	i = o_undolevels(buffer);
	if (i < 1) i++;
	for (other = NULL, undo = buffer->undo;
	     i > 0 && undo;
	     i--, other = undo, undo = undo->next)
	{
	}

	/* if none are doomed, return now */
	if (!undo)
	{
		return;
	}

	/* Remove the most recent doomed version (and all following versions)
	 * from the linked list of undo versions.
	 */
	tail = other;
	if (tail)
	{
		tail->next = NULL;
	}
	else
	{
		buffer->undo = NULL;
	}

	/* delete each doomed version */
	for (; undo; undo = other)
	{
		other = undo->next;
		/* free the lowbuf and the (struct undo_s) structure */
		lowfree(undo->bufinfo);
#ifdef DEBUG_ALLOC
		removeundo(undo);
#endif
		safefree(undo);
	}
	checkundo("after freeundo");
}



/* Free a buffer which was created via bufalloc(). */
void buffree(buffer)
	BUFFER	buffer;	/* buffer to be destroyed */
{
	BUFFER	scan, lag;
	struct undo_s *undo;

	assert(buffer != bufdefopts);
	checkundo("before buffree");

	/* if any window is editing this buffer, then fail */
	if (wincount(buffer) > 0)
	{
		return;
	}

	/* transfer any marks to the dummy "bufdefopts" buffer */
	while (buffer->marks)
	{
		marksetoffset(buffer->marks, 0L);
		marksetbuffer(buffer->marks, bufdefopts);
	}

	/* free any undo/redo versions of this buffer */
	while (buffer->undo)
	{
		undo = buffer->undo;
		buffer->undo = undo->next;
		lowfree(undo->bufinfo);
#ifdef DEBUG_ALLOC
		removeundo(undo);
#endif
		safefree(undo);
	}
	while (buffer->redo)
	{
		undo = buffer->redo;
		buffer->redo = undo->next;
		lowfree(undo->bufinfo);
#ifdef DEBUG_ALLOC
		removeundo(undo);
#endif
		safefree(undo);
	}
	if (buffer->undolnptr)
	{
		lowfree(buffer->undolnptr->bufinfo);
#ifdef DEBUG_ALLOC
		removeundo(buffer->undolnptr);
#endif
		safefree(buffer->undolnptr);
		buffer->undolnptr = NULL;
	}
	assert(buffer->undo == NULL && buffer->redo == NULL);

	/* locate the buffer in the linked list */
	for (lag = NULL, scan = buffers; scan != buffer; lag = scan, scan = scan->next)
	{
		assert(scan->next);
	}

	/* remove it from the linked list */
	if (lag)
	{
		lag->next = scan->next;
	}
	else
	{
		buffers = scan->next;
	}

	/* free the values of any string options which have been set */
	optfree(QTY(bdesc), &buffer->filename);

	/* free any marks in this buffer */
	while (buffer->marks)
	{
		markfree(buffer->marks);
	}

	/* free the low-level block */
	lowfree(buffer->bufinfo);

	/* free the buffer struct itself */
	safefree(buffer);
	checkundo("after buffree");
}


/* Free a buffer, if possible without losing anything important.  Return
 * True if freed, False if retained.  If "force" is True, it tries harder.
 */
BOOLEAN bufunload(buf, force, save)
	BUFFER	buf;	/* buffer to be unloaded */
	BOOLEAN	force;	/* if True, try harder */
	BOOLEAN	save;	/* if True, maybe save even if noautowrite */
{
	MARKBUF	top, bottom;

	/* if "internal" then retain it for now */
	if (o_internal(buf))
	{
		return False;
	}

	/* if being used by some window, then retain it */
	if (wincount(buf) > 0)
	{
		return False;
	}

	/* If supposed to retain, then keep it unless "force" is True.
	 * If this is a temporary session, then no buffers will be retained,
	 * so we should fail in that situation too.
	 */
	if (o_retain(buf) && !force && !o_tempsession)
	{
		return False;
	}

	/* if not modified, then discard it */
	if (!o_modified(buf))
	{
		buffree(buf);
		return True;
	}

	/* if readonly, or no known filename then free it if "force" or
	 * retain it if not "force"
	 */
	if (o_readonly(buf) || o_filename(buf) == NULL)
	{
		if (force)
		{
			buffree(buf);
		}
		return force;
	}

	/* Try to save the buffer to a file */
	if (save && o_filename(buf)
	 && bufwrite(marktmp(top, buf, 0), marktmp(bottom, buf, o_bufchars(buf)), tochar8(o_filename(buf)), force))
	{
		free(buf);
		return True;
	}
	return False;
}


/* Return True if "buf" can be deleted without loosing data, or False if it
 * can't -- in which case it also emits a message describing why.  NOTE THAT
 * YOU ALSO NEED TO MAKE SURE THE BUFFER ISN'T BEING EDITED IN A WINDOW.
 *
 * This function has side-effects.  If the buffer has been modified, it may
 * try to write the buffer out to a file.
 */
BOOLEAN bufsave(buf, force, mustwr)
	BUFFER	buf;	/* the buffer to write */
	BOOLEAN	force;	/* passed to bufwrite() if writing is necessary */
	BOOLEAN	mustwr;	/* write to file even if buffer isn't modified */
{
	MARKBUF	top, bottom;

	/* Can never "save" or delete the internal buffers */
	if (o_internal(buf))
	{
		msg(MSG_ERROR, "[s]$1 is used internally by elvis", o_bufname(buf));
		return False;
	}

	/* If writing wasn't explicitly demanded, and isn't needed, then
	 * return True without doing anything else.
	 */
	if (!mustwr && !o_modified(buf))
	{
		return True;
	}

	/* We know that we need to write this buffer.  If it has no filename
	 * then we can't write it.
	 */
	if (!o_filename(buf))
	{
		msg(MSG_ERROR, "no file name");
		return False;
	}

	/* try to write the buffer out to its file */
	return bufwrite(marktmp(top, buf, 0),
			marktmp(bottom, buf, o_bufchars(buf)),
			tochar8(o_filename(buf)), force);
}



/* Write a buffer, or part of a buffer, out to a file.  Return True if
 * successful, or False if error.
 */
BOOLEAN bufwrite(from, to, wfile, force)
	MARK	from;	/* start of text to write */
	MARK	to;	/* end of text to write */
	char	*wfile;	/* output file, ">>file" to append, "!cmd" to filter */
	BOOLEAN	force;	/* write even if file already exists */
{
	BUFFER	buf = markbuffer(from);
	BUFFER	initbuf;	/* one of the file initialization buffers */
	MARKBUF	top;		/* the endpoints of initbuf */
	MARKBUF	next;		/* used for determining append location */
	CHAR	*cp;		/* used for scanning through file */
	BOOLEAN	append;		/* If True, we're appending */
	BOOLEAN	filter;		/* If True, we're writing to a filter */
	BOOLEAN	wholebuf;	/* if True, we're writing the whole buffer */
	BOOLEAN	samefile;	/* If True, we're writing the buffer to its original file */
	int	bytes;

	assert(from && to && wfile);
	assert(markbuffer(from) == markbuffer(to));
	assert(markoffset(from) >= 0);
	assert(markoffset(from) <= markoffset(to));
	assert(markoffset(to) <= o_bufchars(buf));

	/* Determine some characteristics of this write */
	append = (BOOLEAN)(wfile[0] == '>' && wfile[1] == '>');
	filter = (BOOLEAN)(wfile[0] == '!');
	wholebuf = (BOOLEAN)(markoffset(from) == 0 && markoffset(to) == o_bufchars(buf));
	samefile = (BOOLEAN)(o_filename(buf) && !strcmp(wfile, tochar8(o_filename(buf))));

	/* if we're appending, skip the initial ">>" */
	if (append)
	{
		for (wfile += 2; isspace(*wfile); wfile++)
		{
		}
	}

	/* If writing to the same file, as it is a readonly file, then fail
	 * unless we're forcing a write.
	 */
	if (!filter && wholebuf && samefile && o_readonly(buf) && !force)
	{
		msg(MSG_ERROR, "[s]$1 readonly" , wfile);
		return False;
	}

	/* If this is supposed to be a new file, or we're writing to
	 * a name other than what was originally loaded, and we aren't
	 * forcing a write, then make sure the file doesn't already
	 * exist.
	 */
	if ((o_newfile(buf) || !o_edited(buf) || !samefile || !wholebuf)
	  && !force
	  && !append
	  && !filter
	  && dirperm(wfile) != DIR_NEW)
	{
		msg(MSG_ERROR, "[s]$1 exists", wfile);
		return False;
	}

	/* If we're writing the whole file back over itself, then execute the
	 * "before write" script if it exists.  If this fails and "force" isn't
	 * true, then fail.
	 */
	if (wholebuf && samefile && !filter && !append)
	{
		initbuf = buffind(toCHAR(BEFOREWRITE_BUF));
		if (initbuf)
		{
			/* make the buffer be the default buffer */
			bufoptions(buf);

			/* execute the script */
			if (experform(windefault, marktmp(top, initbuf, 0),
				marktmp(next, initbuf, o_bufchars(initbuf))) != RESULT_COMPLETE
			    && !force)
			{
				return False;
			}
		}
	}

	/* Try to write the file */
	if (ioopen(wfile, append ? 'a' : 'w', False, True, o_binary(markbuffer(from))))
	{
		if (wholebuf && !filter)
		{
			msg(MSG_STATUS, "[s]writing $1", wfile);
		}
		next = *from;
		scanalloc(&cp, &next);
		assert(cp);
		bytes = 1;
		do
		{
			/* check for ^C */
			if (guipoll(False))
			{
				ioclose();
				scanfree(&cp);
				return False;
			}

			bytes = scanright(&cp);
			if (markoffset(&next) + bytes > markoffset(to))
			{
				bytes = (int)(markoffset(to) - markoffset(&next));
			}
			if (iowrite(cp, bytes) < bytes)
			{
				msg(MSG_ERROR, (wfile[0] == '!') ? "broken pipe" : "disk full");
				ioclose();
				scanfree(&cp);
				return False;
			}
			markaddoffset(&next, bytes);
			scanseek(&cp, &next);
		} while (cp != NULL && markoffset(&next) < markoffset(to));
		ioclose();
		scanfree(&cp);

		if (!filter)
		{
			if (append)
				msg(bufmsgtype, "[ds]appended $1 lines to $2",
					markline(to) - markline(from), wfile);
			else
				msg(bufmsgtype, "[sdd]wrote $1, $2 lines, $3 chars",
					wfile, markline(to) - markline(from),
					markoffset(to) - markoffset(from));
		}
	}
	else
	{
		/* We had an error, and already wrote the error message */
		return False;
	}

	/* Execute the "after write" script. */
	if (wholebuf && samefile && !filter && !append)
	{
		initbuf = buffind(toCHAR(AFTERWRITE_BUF));
		if (initbuf)
		{
			/* make the buffer be the default buffer */
			bufoptions(buf);

			/* execute the script */
			(void)experform(windefault, marktmp(top, initbuf, 0),
				marktmp(next, initbuf, o_bufchars(initbuf)));
		}
	}

	/* Writing the whole file has some side-effects on options */
	if (wholebuf && !append && !filter)
	{
		/* if it had no filename before, it has one now */
		if (!o_filename(buf) && wholebuf)
		{
			o_filename(buf) = CHARdup(toCHAR(wfile));
			optflags(o_filename(buf)) |= OPT_FREE;
			buftitle(buf, toCHAR(wfile));
		}

		/* buffer is no longer modified */
		o_modified(buf) = False;
		o_newfile(buf) = False;
		
		/* if the original file was overwritten, then reset the
		 * readonly flag because apparently the file isn't readonly.
		 */
		if (!CHARcmp(o_filename(buf), toCHAR(wfile)))
		{
			o_readonly(buf) = False;
			o_edited(buf) = True;
		}
	}

	/* success! */
	return True;
}


/* Make "buf" be the default buffer.  The default buffer is the one whose
 * options are available to the :set command.
 */
void bufoptions(buf)
	BUFFER	buf;	/* the buffer to become the new default buffer */
{
	/* if same as before, then do nothing */
	if (buf == bufdefault)
	{
		return;
	}

	/* if there is a previous buffer, then delete its options */
	if (bufdefault)
	{
		optdelete(&bufdefault->filename);
	}

	/* make this buffer be the default */
	bufdefault = buf;

	/* if bufdefault is not NULL, then insert its options */
	if (buf)
	{
		optinsert("buf", QTY(bdesc), bdesc, &buf->filename);
	}
}


/* This function changes the name of a buffer.  If the buffer happens to
 * be the main buffer of one or more windows, then it will also retitle
 * those windows.
 */
void buftitle(buffer, title)
	BUFFER	buffer;	/* the buffer whose name is to be changed */
	CHAR	*title;	/* the new name of the buffer */
{
	WINDOW	win;

	/* change the name */
	safefree(o_bufname(buffer));
	o_bufname(buffer) = CHARkdup(title);

	/* change the window titles, if the gui supports that */
	if (gui->retitle)
	{
		for (win = winofbuf((WINDOW)0, buffer);
		     win;
		     win = winofbuf(win, buffer))
		{
			(*gui->retitle)(win->gw, tochar8(o_bufname(buffer)));
		}
	}
}


/* Set the buffer's flag that will eventually cause an undo version of to
 * be saved.
 */
void bufwilldo(cursor)
	MARK	cursor;	/* where to put cursor if we return to this "undo" version */
{
	markbuffer(cursor)->willdo = True;
	markbuffer(cursor)->docursor = markoffset(cursor);
}


/* Save an "undo" version of the buffer that "cursor" points to */
static void bufdo(buf, wipe)
	BUFFER	buf;	/* buffer to make an "undo" version for */
	BOOLEAN	wipe;	/* if True, then delete all "redo" versions */
{
	struct undo_s	*undo;
	int		i;
	long		linenum;

	checkundo("before bufdo");

	/* never save an undo version of an internal buffer */
	if (o_internal(buf))
		return;

	/* allocate an undo structure */
	undo = (struct undo_s *)safealloc(1, sizeof *undo);
#ifdef DEBUG_ALLOC
	undo->link1 = undohead;
	undohead = undo;
	undo->link2 = NULL;
	if (undo->link1)
		undo->link1->link2 = undo;
	else
		undotail = undo;
	undo->buf = buf;
	undo->undoredo = 'u';
#endif

	/* fill it in */
	undo->changepos = buf->changepos = buf->docursor;
	undo->buflines = o_buflines(buf);
	undo->bufchars = o_bufchars(buf);
	for (i = 0; i < QTY(namedmark); i++)
	{
		if (namedmark[i] && markbuffer(namedmark[i]) == buf)
		{
			undo->offset[i] = markoffset(namedmark[i]);
		}
		else
		{
			undo->offset[i] = -1;
		}
	}
	undo->bufinfo = lowdup(buf->bufinfo);

	/* insert it into the buffer's "undo" list */
	undo->next = buf->undo;
	buf->undo = undo;

	checkundo("in bufdo, before changing undolnptr");

	/* If this is on a different line from previous change, then store this
	 * as the current "line-undo".  This is done by recursively calling
	 * bufdo() with the same arguments to create a second identical
	 * struct undo_s structure, and then moving the second one from the
	 * undo list to the undolnptr variable.
	 */
	(void)lowoffset(undo->bufinfo, undo->changepos,
		(COUNT *)0, (COUNT *)0, (LBLKNO *)0, &linenum);
	if (linenum != buf->undoline)
	{
		/* change undoline to the expected line NOW, so we don't get
		 * stuck in infinite recursion.
		 */
		buf->undoline = linenum;

		/* Call bufdo() again to make another copy of the undo version.
		 * Increment undolevels temporarily so we don't loose the
		 * oldest one yet.
		 */
		linenum = o_undolevels(buf);
		o_undolevels(buf)++;
		if (o_undolevels(buf) < 2) o_undolevels(buf) = 2;
		bufdo(buf, False);
		o_undolevels(buf) = linenum;

		/* free the old one, if any */
		if (buf->undolnptr)
		{
			lowfree(buf->undolnptr->bufinfo);
#ifdef DEBUG_ALLOC
			removeundo(buf->undolnptr);
#endif
			safefree(buf->undolnptr);
		}

		/* Use the second undo copy as the line-undo version */
		buf->undolnptr = buf->undo;
		buf->undo = buf->undo->next;
#ifdef DEBUG_ALLOC
		buf->undolnptr->undoredo = 'l';
#endif
		assert(buf->undo == undo);
	}

	checkundo("in bufdo, after changing undolnptr");

	/* discard the redo versions, if we're supposed to */
	if (wipe)
	{
		while (buf->redo && buf->redo != buf->undolnptr)
		{
			undo = buf->redo;
			buf->redo = undo->next;
			lowfree(undo->bufinfo);
#ifdef DEBUG_ALLOC
			removeundo(undo);
#endif
			safefree(undo);
		}

		checkundo("in bufdo, after wiping the redo versions");
	}

	/* discard the oldest version[s] from the undo list */
	freeundo(buf);

	/* make sure this is written to disk */
	if (!o_internal(buf) && eventcounter > 2)
		sessync();
}

/* Recall a previous "undo" version of a given buffer.  The "back" argument
 * should be positive to undo, negative to redo, or 0 to undo all changes
 * to the current line.  Returns the cursor offset if successful, or -1 if
 * the requested undo version doesn't exist.
 */
long bufundo(cursor, back)
	MARK	cursor;	/* buffer to be undone, plus cursor offset of current version */
	long	back;	/* number of versions back (negative to redo) */
{
	struct undo_s	*undo;
	struct undo_s	*tmp;
	long		i;
	BUFFER		buffer;
	MARKBUF		from, to;
	long		delta;
	long		origulev;

	checkundo("before bufundo");

	/* locate the desired undo version */
	buffer = markbuffer(cursor);
	if (back == 0)
	{
		/* line undo */
		undo = buffer->undolnptr;
	}
	else if (o_undolevels(buffer) == 0)
	{
		/* Can only oscillate between previous version and this one,
		 * but it has the advantage that <u> and <^R> both do exactly
		 * the same thing.
		 */
		if (buffer->redo)
			undo = buffer->redo, back = -1;
		else
			undo = buffer->undo, back = 1;
	}
	else if (back > 0)
	{
		/* undo */
		for (i = back, undo = buffer->undo; undo && i > 1; i--, undo = undo->next)
		{
		}
	}
	else
	{
		/* redo */
		for (i = -back, undo = buffer->redo; undo && i > 1; i--, undo = undo->next)
		{
		}
	}

	/* if the requested version doesn't exist, then fail */
	if (!undo)
	{
		return -1;
	}

	/* save the current version as either an undo version or a redo version,
	 * so we can revert to it.  Note that we increase the number of undo
	 * levels temporarily, so the oldest undo version won't be discarded
	 * yet.
	 */
	origulev = o_undolevels(buffer);
	o_undolevels(buffer)++;
	if (o_undolevels(buffer) < 2) o_undolevels(buffer) = 2;
	bufwilldo(marktmp(from, buffer, undo->changepos));
	bufdo(buffer, False);
	if (back > 0)
	{
		/* undoing: move from "undo" to "redo" */
		while (buffer->undo != undo)
		{
			assert(buffer->undo);
			tmp = buffer->undo;
			buffer->undo = tmp->next;
			tmp->next = buffer->redo;
			buffer->redo = tmp;
#ifdef DEBUG_ALLOC
			tmp->undoredo = 'r';
#endif
		}

		/* remove the selected version from the "undo" list */
		buffer->undo = undo->next;
	}
	else if (back < 0)
	{
		/* redoing: move from "redo" to "undo" */
		while (buffer->redo != undo)
		{
			assert(buffer->redo);
			tmp = buffer->redo;
			buffer->redo = tmp->next;
			tmp->next = buffer->undo;
			buffer->undo = tmp;
#ifdef DEBUG_ALLOC
			tmp->undoredo = 'u';
#endif
		}

		/* remove the selected version from the "redo" list */
		buffer->redo = undo->next;
	}
	else
	{
		/* line-undo: Remove the selected undo version from the
		 * undolnptr pointer.
		 */
		buffer->undolnptr = (struct undo_s *)0;
		buffer->undoline = 0;
	}

	/* replace the current version with the selected undo version */
	lowfree(buffer->bufinfo);
	buffer->bufinfo = undo->bufinfo;
	delta = undo->bufchars - o_bufchars(buffer);
	buffer->changepos = undo->changepos;
	o_buflines(buffer) = undo->buflines;
	o_bufchars(buffer) = undo->bufchars;
	buffer->changes++;
	o_modified(buffer) = True;

	/* Adjust the values of any marks.  Most marks can be fixed just by
	 * calling markadjust(), but the named marks' old values are stored in
	 * the undo buffer and we can recall them exactly.
	 */
	if (delta < 0)
	{
		markadjust(marktmp(from, buffer, undo->changepos),
			marktmp(to, buffer, undo->changepos - delta),
			delta);
	}
	else
	{
		markadjust(marktmp(from, buffer, undo->changepos), &from, delta);
	}
	for (i = 0; i < QTY(namedmark); i++)
	{
		if (undo->offset[i] >= 0 && (!namedmark[i] || markbuffer(namedmark[i]) == buffer))
		{
			if (namedmark[i])
				markfree(namedmark[i]);
			namedmark[i] = markalloc(buffer, undo->offset[i]);
		}
	}
#ifdef DISPLAY_MARKUP
	dmmuadjust(marktmp(from, buffer, 0), marktmp(to, buffer, o_bufchars(buffer)), 0);
#endif

	/* We can free the undo_s structure now. */
#ifdef DEBUG_ALLOC
	removeundo(undo);
#endif
	safefree(undo);

	/* Okay to free the oldest "undo" version now */
	o_undolevels(buffer) = origulev;
	freeundo(buffer);	/* Is this really necessary? */

	/* Return the offset of the last change, so the cursor can be moved
	 * there.  Never return a point past the end of the buffer, though.
	 */
	checkundo("after bufundo");
	if (o_bufchars(buffer) == 0)
		buffer->changepos = 0;
	else if (buffer->changepos >= o_bufchars(buffer))
		buffer->changepos = o_bufchars(buffer) - 1;
	return buffer->changepos;
}

/* This function replaces part of a buffer with new text.  In addition to
 * replacement, it can also be used to implement insertion (by having "from"
 * and "to" be identical) or deletion (by having "newlen" be zero).
 *
 * It uses markadjust() to automatically update marks.  If the buffer's
 * "willdo" flag is set, then it will automatically create an "undo" version
 * of the buffer before making the change.
 */
void bufreplace(from, to, newp, newlen)
	MARK	from;	/* starting position of old text */
	MARK	to;	/* ending position of old text */
	CHAR	*newp;	/* pointer to new text (in RAM) */
	long	newlen;	/* length of new text */
{
	long	chglines;
	long	chgchars;


	assert(markbuffer(from) == markbuffer(to) && newlen >= 0);

	/* if the destination's "willdo" flag is set, then save an "undo"
	 * version of it before doing the change
	 */
	if (markbuffer(from)->willdo)
	{
		bufdo(markbuffer(from), True);
		markbuffer(from)->willdo = False;
	}

	/* make the change to the buffer contents */
	if (markoffset(from) == markoffset(to))
	{
		/* maybe we aren't really changing anything? */
		if (newlen == 0)
		{
			return;
		}

		/* we're inserting */
		chglines = lowinsert(markbuffer(from)->bufinfo, markoffset(from), newp, newlen);
	}
	else if (newlen == 0)
	{
		/* we're deleting */
		chglines = lowdelete(markbuffer(from)->bufinfo, markoffset(from), markoffset(to));
	}
	else
	{
		/* we're replacing */
		chglines = lowreplace(markbuffer(from)->bufinfo, markoffset(from), markoffset(to), newp, newlen);
	}

	/* adjust the buffer totals */
	chgchars = newlen - (markoffset(to) - markoffset(from));
	o_buflines(markbuffer(from)) += chglines;
	o_bufchars(markbuffer(from)) += chgchars;
	o_modified(markbuffer(from)) = True;
	markbuffer(from)->changes++;

	/* adjust the marks */
#ifdef DISPLAY_MARKUP
	dmmuadjust(from, to, chgchars);
#endif
	markadjust(from, to, chgchars);
}

/* Copy part of one buffer into another.  "dst" is the destination (where
 * the pasted text will be inserted), and "from" and "to" describe the
 * portion of the source buffer to insert.
 *
 * This calls markadjust() to automatically adjust marks.  If the destination
 * buffer's "willdo" flag is set, it will save an "undo" version before
 * making the change.
 */
void bufpaste(dst, from, to)
	MARK	dst;	/* destination */
	MARK	from;	/* start of source */
	MARK	to;	/* end of source */
{
	long	chglines;
	long	chgchars;


	assert(markbuffer(from) == markbuffer(to));

	/* if the destination's "willdo" flag is set, then save an "undo"
	 * version of it before doing the paste
	 */
	if (markbuffer(dst)->willdo)
	{
		bufdo(markbuffer(dst), True);
		markbuffer(dst)->willdo = False;
	}

	/* make the change to the buffer's contents */
	chglines = lowpaste(markbuffer(dst)->bufinfo, markoffset(dst),
		markbuffer(from)->bufinfo, markoffset(from), markoffset(to));

	/* adjust the destination's counters */
	chgchars = markoffset(to) - markoffset(from);
	o_buflines(markbuffer(dst)) += chglines;
	o_bufchars(markbuffer(dst)) += chgchars;
	o_modified(markbuffer(dst)) = True;
	markbuffer(dst)->changes++;

	/* adjust marks */
	markadjust(dst, dst, chgchars);
}


/* Copy a section of some buffer into dynamically-allocated RAM, and append
 * a NUL to the end of the copy.  The calling function must call safefree()
 * on the returned memory.
 */
CHAR *bufmemory(from, to)
	MARK	from, to;	/* the section of the buffer to fetch */
{
	CHAR	*memory;	/* the allocated memory */
	CHAR	*scan, *build;	/* used for copying text from buffer to memory */
	long	i;

	assert(markbuffer(from) == markbuffer(to)
		&& markoffset(from) <= markoffset(to));

	/* allocate space for the copy */
	i = markoffset(to) - markoffset(from);
	memory = (CHAR *)safealloc((int)(i + 1), sizeof(CHAR));

	/* copy the text into it */
	for (scanalloc(&scan, from), build = memory; i > 0; scannext(&scan), i--)
	{
		assert(scan);
		*build++ = *scan;
	}
	scanfree(&scan);
	*build = '\0';

	return memory;
}

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.