/* Copyright 1996 Acorn Computers Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Title: c.txtundo * Purpose: Undo facilities for text objects * Author: W. Stoye * Status: system-independent * History: * 13-Oct-87: started * 02-Mar-88: WRS: implicit txtundo_init on any change to txt object. * 08-Mar-88: WRS: txtundo_commit added. * 14-Mar-88: WRS: undoing of selection added. */ #define BOOL int #define TRUE 1 #define FALSE 0 #include <stdlib.h> #include <string.h> #include <stdarg.h> #include "h.flex" #include "h.trace" #include "h.txt" #include "h.EditIntern.txtundo" #include "h.EditIntern.txt1" #include "h.txtscrap" #include "h.werr" typedef struct txtundo__state { char *buf; int size; int head; /* in [0..size-1] */ int tail; /* in [0..size-1], head==tail ? empty. */ int ptr; /* current pointer for undo/redo */ BOOL withinundo; } txtundo__state; /* head==tail -> empty head -> first free for insertion tail -> last interesting byte ptr -> last thing ungot, so Undo starts with DEC(ptr) */ #define INITIAL_BUF_SIZE 20 /* -------- Debugging info -------- */ /* Can be omitted on "production" versions of things. */ #if TRACE void txtundo__safewrch(char c) { if (c == '*') tracef0(" *"); else if (c >= 32 && c < 127) tracef1(":%c", c); else tracef1("%.2x", c); } void txtundo__tab(int by) { int i; for (i=1; i<=by; i++) tracef0(" "); } void txtundo_pr(txtundo u) { int i; tracef("Undo buffer:\n"); if (u->size > 300) tracef(">>>>size=%i head=%i tail=%i ptr=%i\n", u->size, u->head, u->tail, u->ptr); else { /* zap the gap chars to make things clearer */ if (u->tail > u->head) { for (i=u->head; i<=u->tail-1; i++) u->buf[i] = '*'; } else if (u->tail <= u->head) { for (i=u->head; i<=u->size-1; i++) u->buf[i] = '*'; for (i=0; i<u->tail; i++) u->buf[i] = '*'; }; tracef(" ["); for (i=0; i<u->size; i++) txtundo__safewrch(u->buf[i]); tracef("]\n"); txtundo__tab(2*u->head); tracef("head^^ (%i)\n", u->head); txtundo__tab(2*u->tail); tracef("tail^^ (%i)\n", u->tail); txtundo__tab(2*u->ptr); tracef(" ptr^^ (%i)\n", u->ptr); }; } #endif /* -------- Creation, Deletion -------- */ txtundo txtundo_new() { txtundo u; u = malloc(sizeof(txtundo__state)); if (u == 0) return 0; if (flex_alloc((void**) &(u->buf), INITIAL_BUF_SIZE) == 0) { free(u); return 0; }; u->head = 0; u->tail = 0; u->size = INITIAL_BUF_SIZE; u->ptr = 0; u->withinundo = FALSE; return u; } void txtundo_dispose(txtundo *u) { /* The extra * is for M2-compatibility: it will change. */ flex_free((void**) &((*u)->buf)); free(*u); } void txtundo_setbufsize(txt t, int nbytes) { txtundo u = t->undostate; int by; if (nbytes <= u->size) { by = 0; } else { by = nbytes - u->size; }; if (flex_midextend((void**) &(u->buf), u->head, by)) { if (u->tail >= u->head) {u->tail += by;}; if (u->ptr >= u->head) {u->ptr += by;}; u->head += by; u->size += by; } else { /* he can't have the store, but it doesn't matter. */ tracef0("Undo setbufsize didn't work.\n"); }; } void txtundo_prevent_undo(txt t) { /* very easy, just empty the buffer. */ txtundo u = t->undostate; u->head = 0; u->tail = 0; u->ptr = 0; /* 20-Dec-88 WRS: otherwise, a following ReDo will get confused... */ } /* -------- Put operations. -------- */ void txtundo_putcode(txtundo u, char code) { u->buf[u->head++] = code; if (u->head == u->size) u->head = 0; if (u->head == u->tail) { u->tail++; if (u->tail == u->size) u->tail = 0; }; if (! u->withinundo) { /* If this update is generated outside an undo context then cancel any current chain of undos. */ u->ptr = u->head; }; } static int min(int a, int b) {return a < b ? a : b;} void txtundo_putbytes(txtundo u, char* bytes, int n) { int blocksize; if (n >= u->size) { /* We'll never be able to undo this, so forget it. */ tracef1("Undo %i chars impossible, clearing.\n", n); u->head = 0; u->tail = 0; } else { while (1) { if (n == 0) break; blocksize = min(n, u->size - u->head); tracef1("Undo putbytes %i.\n", blocksize); (void) memcpy(/*to*/ &u->buf[u->head], /*from*/ bytes, blocksize); n -= blocksize; bytes += blocksize; if (u->tail >= u->head && u->tail <= u->head + blocksize) { u->tail = u->head + blocksize + 1; if (u->tail == u->size) u->tail = 0; if (u->tail == u->size + 1) u->tail = 1; }; u->head += blocksize; if (u->head == u->size) { u->head = 0; if (u->tail == 0) u->tail = 1; }; }; }; } void txtundo_putnumber(txtundo u, int n) { /* Chars will be picked up LS byte first, each byte contributing 7 bits, with all but the last one having top bit set. */ tracef1("undo putnumber %i.\n", n); txtundo_putcode(u, n % 128); while (1) { n = n / 128; if (n == 0) break; txtundo_putcode(u, 128 + n % 128); }; } static char txtundo__charbefore(txtundo u, int p) { if (p == 0) { return u->buf[u->size - 1]; } else { return u->buf[p-1]; }; } void txtundo_separate_major_edits(txt t) { txtundo u = t->undostate; if (u->head != u->tail && txtundo__charbefore(u, u->head) == 's') { /* already separated */ tracef0("undo sepmajoredits, already separated.\n"); } else { tracef0("undo sepmajoredits.\n"); txtundo_putcode(u, 's'); }; } /* -------- Extract operations. -------- */ static int txtundo__count_ptr_to_tail(txtundo u) /* e.g. bytes still extractable. Extraction happens below the poitner, e.g. starts at head. ptr==tail->nothing left. */ { if (u->ptr >= u->tail) { return u->ptr - u->tail; } else { return u->ptr + (u->size - u->tail); }; } static int txtundo__count_ptr_to_head(txtundo u) /* How much could be inserted with ptr remaining valid? Add 1 to this because the buffer always clears one char ahead. */ { if (u->head < u->ptr) { return u->ptr - u->head; } else { return u->ptr + (u->size - u->head); }; } static int txtundo__extractch(txtundo u, char *c /*out*/) { if (u->ptr == u->tail) return 0; if (u->ptr == 0) u->ptr = u->size; *c = u->buf[--u->ptr]; tracef2("Undo extract char %i from %i.\n", *c, u->ptr); return 1; } static int txtundo__extractnum(txtundo u, int *c /*out*/) { char ch; *c = 0; while (1) { if (txtundo__extractch(u, &ch)) { if (ch < 128) { *c = *c * 128 + ch; return 1; } else { *c = *c * 128 + ch - 128; }; } else { return 0; }; }; } static void txtundo__extractchars(txtundo u, int n, txt t) /* There definitely are enough. Insert them into the Text. */ { int blocksize; int saven = n; int savehead = u->head; tracef1("Undo extractchars n=%i.\n", n); if (n == 0 || u->ptr == u->tail) return; while (n > 0) { if (u->ptr == 0) u->ptr = u->size; blocksize = u->ptr; if (u->tail < u->ptr) blocksize = u->ptr - u->tail; if (blocksize > n) blocksize = n; u->ptr -= blocksize; n -= blocksize; tracef2("undo reinserts %i chars from %i.\n", blocksize, u->ptr); txt_replacechars(t, 0, &(u->buf[u->ptr]), blocksize); /* This will recursively call back and adjust u-> */ u->head = savehead; }; txtundo_putnumber(u, saven); txtundo_putcode(u, 'd'); } /* The savehead/saven business is in case we go round twice, to prevent this one simple edit from becoming two and so gently swallowing more of the undeletion buffer. the head=savehead is in the loop to prevent event to two delete operations from piling up. */ /* -------- Undo operations. -------- */ void txtundo_init(txt t) { txtundo u = t->undostate; u->ptr = u->head; } void txtundo_commit(txt t) { txtundo u = t->undostate; u->head = u->ptr; } txtundo_result txtundo_undo(txt t) { txtundo u = t->undostate; char code; int n; int saveptr = u->ptr; txtundo_result result; if (txtundo__extractch(u, &code) == 0) return txtundo_RANOUT; u->withinundo = TRUE; switch (code) { case 's': result = txtundo_MAJOR; break; case 'm': if (txtundo__extractnum(u, &n) && txtundo__count_ptr_to_head(u) >= 5) { txt_setdot(t, n); result = txtundo_MINOR; } else { u->ptr = saveptr; result = txtundo_RANOUT; }; break; case 'd': if (txtundo__extractnum(u, &n) && txtundo__count_ptr_to_head(u) >= 5+n) { txt_delete(t, n); result = txtundo_MINOR; } else { u->ptr = saveptr; result = txtundo_RANOUT; }; break; case 'i': if (txtundo__extractnum(u, &n) && txtundo__count_ptr_to_tail(u) >= n && txtundo__count_ptr_to_head(u) >= 5+2*n) /* e.g. can extract them, and insertion will still be reversable. */ { txtundo__extractchars(u, n, t); result = txtundo_MINOR; } else { u->ptr = saveptr; result = txtundo_RANOUT; }; break; case 'l': { int n1; if (txtundo__extractnum(u, &n) && txtundo__extractnum(u, &n1)) { txtscrap_setselect(t, n, n1); result = txtundo_MINOR; } else { u->ptr = saveptr; result = txtundo_RANOUT; }; }; break; default: tracef1("Undo found a %i char.\n", code); u->ptr = saveptr; result = txtundo_RANOUT; break; }; u->withinundo = FALSE; return result; } /* -------- Redo operations. -------- */ static int txtundo__skipop(txtundo u, int p) /* Skip past a complete recorded operation. */ { int saveptr; char code; int result; int n; saveptr = u->ptr; u->ptr = p; if (txtundo__extractch(u, &code) == 0) { tracef0("Undo bug 1.\n"); }; switch (code) { case 's': break; case 'd': if (txtundo__extractnum(u, &n) == 0) tracef0("Undo bug 2.\n"); break; case 'm': if (txtundo__extractnum(u, &n) == 0) tracef0("Undo bug 3.\n"); break; case 'i': if (txtundo__extractnum(u, &n) == 0) tracef0("Undo bug 4.\n"); if (u->ptr < n) { u->ptr = u->size + u->ptr - n; } else { u->ptr -= n; }; break; case 'l': if (txtundo__extractnum(u, &n) == 0) tracef0("undo bug 5.\n"); if (txtundo__extractnum(u, &n) == 0) tracef0("undo bug 6.\n"); break; default: tracef0("Undo bug 5.\n"); } result = u->ptr; u->ptr = saveptr; return result; } static int txtundo__skip_to_previous(txtundo u, int p, int *onebeforethat /*out*/) { int p1; int p2; p1 = u->head; p2 = u->head; *onebeforethat = u->head; while (p1 != p) { *onebeforethat = p2; p2 = p1; p1 = txtundo__skipop(u, p1); } return p2; } txtundo_result txtundo_redo(txt t) { txtundo u = t->undostate; int prev; int onebeforethat; txtundo_result res; if (u->ptr == u->head) return txtundo_RANOUT; u->withinundo = TRUE; u->ptr = txtundo__skip_to_previous(u, u->ptr, &onebeforethat); tracef2("Redo prev=%i before that=%i.\n", u->ptr, onebeforethat); prev = u->ptr; if (txtundo__charbefore(u, u->ptr) == 's') { /* undo a separator: no problem, done. */ } else { u->ptr = u->head; res = txtundo_undo(t); u->head = u->ptr; u->ptr = prev; }; if (onebeforethat == u->head) { res = txtundo_RANOUT; } else if (txtundo__charbefore(u, onebeforethat) == 's') { res = txtundo_MAJOR; } else { res = txtundo_MINOR; }; u->withinundo = FALSE; return res; } /* end */