--- /dev/null
+/* $OpenBSD: undo.c,v 1.50 2010/06/30 19:12:54 oga Exp $ */
+/*
+ * This file is in the public domain
+ */
+
+#include "def.h"
+#include "kbd.h"
+
+#define MAX_FREE_RECORDS 32
+
+/*
+ * Local variables
+ */
+static struct undoq undo_free;
+static int undo_free_num;
+static int boundary_flag = TRUE;
+static int undo_enable_flag = TRUE;
+
+/*
+ * Local functions
+ */
+static int find_dot(struct line *, int);
+static int find_lo(int, struct line **, int *, int *);
+static struct undo_rec *new_undo_record(void);
+static int drop_oldest_undo_record(void);
+
+/*
+ * find_dot, find_lo()
+ *
+ * Find an absolute dot in the buffer from a line/offset pair, and vice-versa.
+ *
+ * Since lines can be deleted while they are referenced by undo record, we
+ * need to have an absolute dot to have something reliable.
+ */
+static int
+find_dot(struct line *lp, int off)
+{
+ int count = 0;
+ struct line *p;
+
+ for (p = curbp->b_headp; p != lp; p = lforw(p)) {
+ if (count != 0) {
+ if (p == curbp->b_headp) {
+ ewprintf("Error: Undo stuff called with a"
+ "nonexistent line");
+ return (FALSE);
+ }
+ }
+ count += llength(p) + 1;
+ }
+ count += off;
+
+ return (count);
+}
+
+static int
+find_lo(int pos, struct line **olp, int *offset, int *lnum)
+{
+ struct line *p;
+ int lineno;
+
+ p = curbp->b_headp;
+ lineno = 0;
+ while (pos > llength(p)) {
+ pos -= llength(p) + 1;
+ if ((p = lforw(p)) == curbp->b_headp) {
+ *olp = NULL;
+ *offset = 0;
+ return (FALSE);
+ }
+ lineno++;
+ }
+ *olp = p;
+ *offset = pos;
+ *lnum = lineno;
+
+ return (TRUE);
+}
+
+static struct undo_rec *
+new_undo_record(void)
+{
+ struct undo_rec *rec;
+
+ rec = TAILQ_FIRST(&undo_free);
+ if (rec != NULL) {
+ /* Remove it from the free-list */
+ TAILQ_REMOVE(&undo_free, rec, next);
+ undo_free_num--;
+ } else {
+ if ((rec = malloc(sizeof(*rec))) == NULL)
+ panic("Out of memory in undo code (record)");
+ }
+ memset(rec, 0, sizeof(struct undo_rec));
+
+ return (rec);
+}
+
+void
+free_undo_record(struct undo_rec *rec)
+{
+ static int initialised = 0;
+
+ /*
+ * On the first run, do initialisation of the free list.
+ */
+ if (initialised == 0) {
+ TAILQ_INIT(&undo_free);
+ initialised = 1;
+ }
+ if (rec->content != NULL) {
+ free(rec->content);
+ rec->content = NULL;
+ }
+ if (undo_free_num >= MAX_FREE_RECORDS) {
+ free(rec);
+ return;
+ }
+ undo_free_num++;
+
+ TAILQ_INSERT_HEAD(&undo_free, rec, next);
+}
+
+/*
+ * Drop the oldest undo record in our list. Return 1 if we could remove it,
+ * 0 if the undo list was empty.
+ */
+static int
+drop_oldest_undo_record(void)
+{
+ struct undo_rec *rec;
+
+ rec = TAILQ_LAST(&curbp->b_undo, undoq);
+ if (rec != NULL) {
+ undo_free_num--;
+ TAILQ_REMOVE(&curbp->b_undo, rec, next);
+ free_undo_record(rec);
+ return (1);
+ }
+ return (0);
+}
+
+static int
+lastrectype(void)
+{
+ struct undo_rec *rec;
+
+ if ((rec = TAILQ_FIRST(&curbp->b_undo)) != NULL)
+ return (rec->type);
+ return (0);
+}
+
+/*
+ * Returns TRUE if undo is enabled, FALSE otherwise.
+ */
+int
+undo_enabled(void)
+{
+ return (undo_enable_flag);
+}
+
+/*
+ * undo_enable: toggle undo_enable.
+ * Returns the previous value of the flag.
+ */
+int
+undo_enable(int f, int n)
+{
+ int pon = undo_enable_flag;
+
+ if (f & (FFARG | FFRAND))
+ undo_enable_flag = n > 0;
+ else
+ undo_enable_flag = !undo_enable_flag;
+
+ if (!(f & FFRAND))
+ ewprintf("Undo %sabled", undo_enable_flag ? "en" : "dis");
+
+ return (pon);
+}
+
+/*
+ * If undo is enabled, then:
+ * Toggle undo boundary recording.
+ * If called with an argument, (n > 0) => enable. Otherwise disable.
+ * In either case, add an undo boundary
+ * If undo is disabled, this function has no effect.
+ */
+int
+undo_boundary_enable(int f, int n)
+{
+ int bon = boundary_flag;
+
+ if (!undo_enable_flag)
+ return (FALSE);
+
+ undo_add_boundary(FFRAND, 1);
+
+ if (f & (FFARG | FFRAND))
+ boundary_flag = n > 0;
+ else
+ boundary_flag = !boundary_flag;
+
+ if (!(f & FFRAND))
+ ewprintf("Undo boundaries %sabled",
+ boundary_flag ? "en" : "dis");
+
+ return (bon);
+}
+
+/*
+ * Record an undo boundary, unless boundary_flag == FALSE.
+ * Does nothing if previous undo entry is already a boundary or 'modified' flag.
+ */
+int
+undo_add_boundary(int f, int n)
+{
+ struct undo_rec *rec;
+ int last;
+
+ if (boundary_flag == FALSE)
+ return (FALSE);
+
+ last = lastrectype();
+ if (last == BOUNDARY || last == MODIFIED)
+ return (TRUE);
+
+ rec = new_undo_record();
+ rec->type = BOUNDARY;
+
+ TAILQ_INSERT_HEAD(&curbp->b_undo, rec, next);
+
+ return (TRUE);
+}
+
+/*
+ * Record an undo "modified" boundary
+ */
+void
+undo_add_modified(void)
+{
+ struct undo_rec *rec;
+
+ rec = new_undo_record();
+ rec->type = MODIFIED;
+
+ TAILQ_INSERT_HEAD(&curbp->b_undo, rec, next);
+
+ return;
+}
+
+int
+undo_add_insert(struct line *lp, int offset, int size)
+{
+ struct region reg;
+ struct undo_rec *rec;
+ int pos;
+
+ if (!undo_enable_flag)
+ return (TRUE);
+ reg.r_linep = lp;
+ reg.r_offset = offset;
+ reg.r_size = size;
+
+ pos = find_dot(lp, offset);
+
+ /*
+ * We try to reuse the last undo record to `compress' things.
+ */
+ rec = TAILQ_FIRST(&curbp->b_undo);
+ if (rec != NULL && rec->type == INSERT) {
+ if (rec->pos + rec->region.r_size == pos) {
+ rec->region.r_size += reg.r_size;
+ return (TRUE);
+ }
+ }
+
+ /*
+ * We couldn't reuse the last undo record, so prepare a new one.
+ */
+ rec = new_undo_record();
+ rec->pos = pos;
+ rec->type = INSERT;
+ memmove(&rec->region, ®, sizeof(struct region));
+ rec->content = NULL;
+
+ undo_add_boundary(FFRAND, 1);
+
+ TAILQ_INSERT_HEAD(&curbp->b_undo, rec, next);
+
+ return (TRUE);
+}
+
+/*
+ * This of course must be done _before_ the actual deletion is done.
+ */
+int
+undo_add_delete(struct line *lp, int offset, int size, int isreg)
+{
+ struct region reg;
+ struct undo_rec *rec;
+ int pos;
+
+ if (!undo_enable_flag)
+ return (TRUE);
+
+ reg.r_linep = lp;
+ reg.r_offset = offset;
+ reg.r_size = size;
+
+ pos = find_dot(lp, offset);
+
+ if (offset == llength(lp)) /* if it's a newline... */
+ undo_add_boundary(FFRAND, 1);
+ else if ((rec = TAILQ_FIRST(&curbp->b_undo)) != NULL) {
+ /*
+ * Separate this command from the previous one if we're not
+ * just before the previous record...
+ */
+ if (!isreg && rec->type == DELETE) {
+ if (rec->pos - rec->region.r_size != pos)
+ undo_add_boundary(FFRAND, 1);
+ }
+ }
+ rec = new_undo_record();
+ rec->pos = pos;
+ if (isreg)
+ rec->type = DELREG;
+ else
+ rec->type = DELETE;
+ memmove(&rec->region, ®, sizeof(struct region));
+ do {
+ rec->content = malloc(reg.r_size + 1);
+ } while ((rec->content == NULL) && drop_oldest_undo_record());
+
+ if (rec->content == NULL)
+ panic("Out of memory");
+
+ region_get_data(®, rec->content, reg.r_size);
+
+ if (isreg || lastrectype() != DELETE)
+ undo_add_boundary(FFRAND, 1);
+
+ TAILQ_INSERT_HEAD(&curbp->b_undo, rec, next);
+
+ return (TRUE);
+}
+
+/*
+ * This of course must be called before the change takes place.
+ */
+int
+undo_add_change(struct line *lp, int offset, int size)
+{
+ if (!undo_enable_flag)
+ return (TRUE);
+ undo_add_boundary(FFRAND, 1);
+ boundary_flag = FALSE;
+ undo_add_delete(lp, offset, size, 0);
+ undo_add_insert(lp, offset, size);
+ boundary_flag = TRUE;
+ undo_add_boundary(FFRAND, 1);
+
+ return (TRUE);
+}
+
+/*
+ * Show the undo records for the current buffer in a new buffer.
+ */
+/* ARGSUSED */
+int
+undo_dump(int f, int n)
+{
+ struct undo_rec *rec;
+ struct buffer *bp;
+ struct mgwin *wp;
+ char buf[4096], tmp[1024];
+ int num;
+
+ /*
+ * Prepare the buffer for insertion.
+ */
+ if ((bp = bfind("*undo*", TRUE)) == NULL)
+ return (FALSE);
+ bp->b_flag |= BFREADONLY;
+ bclear(bp);
+ popbuf(bp, WNONE);
+
+ for (wp = wheadp; wp != NULL; wp = wp->w_wndp) {
+ if (wp->w_bufp == bp) {
+ wp->w_dotp = bp->b_headp;
+ wp->w_doto = 0;
+ }
+ }
+
+ num = 0;
+ TAILQ_FOREACH(rec, &curbp->b_undo, next) {
+ num++;
+ snprintf(buf, sizeof(buf),
+ "%d:\t %s at %d ", num,
+ (rec->type == DELETE) ? "DELETE":
+ (rec->type == DELREG) ? "DELREGION":
+ (rec->type == INSERT) ? "INSERT":
+ (rec->type == BOUNDARY) ? "----" :
+ (rec->type == MODIFIED) ? "MODIFIED": "UNKNOWN",
+ rec->pos);
+
+ if (rec->content) {
+ (void)strlcat(buf, "\"", sizeof(buf));
+ snprintf(tmp, sizeof(tmp), "%.*s", rec->region.r_size,
+ rec->content);
+ (void)strlcat(buf, tmp, sizeof(buf));
+ (void)strlcat(buf, "\"", sizeof(buf));
+ }
+ snprintf(tmp, sizeof(tmp), " [%d]", rec->region.r_size);
+ if (strlcat(buf, tmp, sizeof(buf)) >= sizeof(buf)) {
+ ewprintf("Undo record too large. Aborted.");
+ return (FALSE);
+ }
+ addlinef(bp, "%s", buf);
+ }
+ return (TRUE);
+}
+
+/*
+ * After the user did action1, then action2, then action3:
+ *
+ * [action3] <--- Undoptr
+ * [action2]
+ * [action1]
+ * ------
+ * [undo]
+ *
+ * After undo:
+ *
+ * [undo of action3]
+ * [action2] <--- Undoptr
+ * [action1]
+ * ------
+ * [undo]
+ *
+ * After another undo:
+ *
+ *
+ * [undo of action2]
+ * [undo of action3]
+ * [action1] <--- Undoptr
+ * ------
+ * [undo]
+ *
+ * Note that the "undo of actionX" have no special meaning. Only when
+ * we undo a deletion, the insertion will be recorded just as if it
+ * was typed on the keyboard. Resulting in the inverse operation being
+ * saved in the list.
+ *
+ * If undoptr reaches the bottom of the list, or if we moved between
+ * two undo actions, we make it point back at the topmost record. This is
+ * how we handle redoing.
+ */
+/* ARGSUSED */
+int
+undo(int f, int n)
+{
+ struct undo_rec *ptr, *nptr;
+ int done, rval;
+ struct line *lp;
+ int offset, save, dot;
+ static int nulled = FALSE;
+ int lineno;
+
+ if (n < 0)
+ return (FALSE);
+
+ dot = find_dot(curwp->w_dotp, curwp->w_doto);
+
+ ptr = curbp->b_undoptr;
+
+ /* first invocation, make ptr point back to the top of the list */
+ if ((ptr == NULL && nulled == TRUE) || rptcount == 0) {
+ ptr = TAILQ_FIRST(&curbp->b_undo);
+ nulled = TRUE;
+ }
+
+ rval = TRUE;
+ while (n--) {
+ /* if we have a spurious boundary, free it and move on.... */
+ while (ptr && ptr->type == BOUNDARY) {
+ nptr = TAILQ_NEXT(ptr, next);
+ TAILQ_REMOVE(&curbp->b_undo, ptr, next);
+ free_undo_record(ptr);
+ ptr = nptr;
+ }
+ /*
+ * Ptr is NULL, but on the next run, it will point to the
+ * top again, redoing all stuff done in the buffer since
+ * its creation.
+ */
+ if (ptr == NULL) {
+ ewprintf("No further undo information");
+ rval = FALSE;
+ nulled = TRUE;
+ break;
+ }
+ nulled = FALSE;
+
+ /*
+ * Loop while we don't get a boundary specifying we've
+ * finished the current action...
+ */
+
+ undo_add_boundary(FFRAND, 1);
+
+ save = boundary_flag;
+ boundary_flag = FALSE;
+
+ done = 0;
+ do {
+ /*
+ * Move to where this has to apply
+ *
+ * Boundaries (and the modified flag) are put as
+ * position 0 (to save lookup time in find_dot)
+ * so we must not move there...
+ */
+ if (ptr->type != BOUNDARY && ptr->type != MODIFIED) {
+ if (find_lo(ptr->pos, &lp,
+ &offset, &lineno) == FALSE) {
+ ewprintf("Internal error in Undo!");
+ rval = FALSE;
+ break;
+ }
+ curwp->w_dotp = lp;
+ curwp->w_doto = offset;
+ curwp->w_markline = curwp->w_dotline;
+ curwp->w_dotline = lineno;
+ }
+
+ /*
+ * Do operation^-1
+ */
+ switch (ptr->type) {
+ case INSERT:
+ ldelete(ptr->region.r_size, KNONE);
+ break;
+ case DELETE:
+ lp = curwp->w_dotp;
+ offset = curwp->w_doto;
+ region_put_data(ptr->content,
+ ptr->region.r_size);
+ curwp->w_dotp = lp;
+ curwp->w_doto = offset;
+ break;
+ case DELREG:
+ region_put_data(ptr->content,
+ ptr->region.r_size);
+ break;
+ case BOUNDARY:
+ done = 1;
+ break;
+ case MODIFIED:
+ curbp->b_flag &= ~BFCHG;
+ break;
+ default:
+ break;
+ }
+
+ /* And move to next record */
+ ptr = TAILQ_NEXT(ptr, next);
+ } while (ptr != NULL && !done);
+
+ boundary_flag = save;
+ undo_add_boundary(FFRAND, 1);
+
+ ewprintf("Undo!");
+ }
+ /*
+ * Record where we are. (we have to save our new position at the end
+ * since we change the dot when undoing....)
+ */
+ curbp->b_undoptr = ptr;
+
+ curbp->b_undopos = find_dot(curwp->w_dotp, curwp->w_doto);
+
+ return (rval);
+}