[PATCH] 'u' (Undo) command support for vi

Jody Bruchon jody at jodybruchon.com
Tue Mar 18 22:15:48 UTC 2014


As promised earlier, I have written undo support for BusyBox 'vi'.

This code is in a "works for me" state but needs testing and refinement. 
I have not tested it on any other platforms than my computer. I have not 
tested it under all possible configurations or scenarios, and there is 
room for future improvement. Adding an option for a maximum number of 
undo operations in order to limit memory usage on severely limited 
systems would be nice. At some point, the code also needs to ensure 
there is consistency in the "file_modified" counter since an undo_pop() 
decrements it until no undo operations remain.

Note that unlike Vim's undo, my undo code was written to plug into 
existing insert/delete/swap functions in BusyBox vi, and therefore does 
not group together the typing of or backspacing of multiple characters 
into one undo operation. I plan to implement this later, but it was far 
more important to me to get this code to the mailing list ASAP.

The following works in my testing:
* Typing characters, then undoing the typing
* Backspacing characters, then undoing
* Deleting (DEL key) characters, then undoing
* Using buffer commands such as 'dd', 'd100d', or 'p', then undoing
* Switching to REPLACE mode, overwriting text, then undoing
* Comparing a file and its counterpart that was modified with these 
operations, then undone, then saved.

I hope that this is helpful!

-Jody Bruchon
-------------- next part --------------
diff -Naurw a/editors/vi.c b/editors/vi.c
--- a/editors/vi.c	2014-01-09 13:15:44.000000000 -0500
+++ b/editors/vi.c	2014-03-18 17:58:10.243293500 -0400
@@ -17,7 +17,6 @@
  *      it would be easier to change the mark when add/delete lines
  *	More intelligence in refresh()
  *	":r !cmd"  and  "!cmd"  to filter text through an external command
- *	A true "undo" facility
  *	An "ex" line oriented mode- maybe using "cmdedit"
  */
 
@@ -136,6 +135,13 @@
 //config:	  cursor position using "ESC [ 6 n" escape sequence, then read stdin.
 //config:
 //config:	  This is not clean but helps a lot on serial lines and such.
+//config:config FEATURE_VI_UNDO
+//config:	bool "Support undo command 'u'"
+//config:	default y
+//config:	depends on VI
+//config:	help
+//config:	  Support the 'u' command to undo insertion, deletion, and replacement
+//config:	  of text.
 
 //applet:IF_VI(APPLET(vi, BB_DIR_BIN, BB_SUID_DROP))
 
@@ -347,6 +353,16 @@
 	char get_input_line__buf[MAX_INPUT_LEN]; /* former static */
 
 	char scr_out_buf[MAX_SCR_COLS + MAX_TABSTOP * 2];
+#if ENABLE_FEATURE_VI_UNDO
+	struct undo_object {
+		struct undo_object *prev;	// Linking back avoids list traversal (LIFO)
+		int type;		// 0=deleted, 1=inserted, 2=swapped
+		int start;		// Offset where the data should be restored/deleted
+		int length;		// total data size
+		char *undo_text;	// ptr to text that will be inserted
+	} *undo_stack_tail;
+#endif
+
 };
 #define G (*ptr_to_globals)
 #define text           (G.text          )
@@ -408,6 +424,10 @@
 #define last_modifying_cmd  (G.last_modifying_cmd )
 #define get_input_line__buf (G.get_input_line__buf)
 
+#if ENABLE_FEATURE_VI_UNDO
+#define undo_stack_tail (G.undo_stack_tail)
+#endif
+
 #define INIT_G() do { \
 	SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
 	last_file_modified = -1; \
@@ -448,7 +468,7 @@
 static int st_test(char *, int, int, char *);	// helper for skip_thing()
 static char *skip_thing(char *, int, int, int);	// skip some object
 static char *find_pair(char *, char);	// find matching pair ()  []  {}
-static char *text_hole_delete(char *, char *);	// at "p", delete a 'size' byte hole
+static char *text_hole_delete(char *, char *, int);	// at "p", delete a 'size' byte hole
 // might reallocate text[]! use p += text_hole_make(p, ...),
 // and be careful to not use pointers into potentially freed text[]!
 static uintptr_t text_hole_make(char *, int);	// at "p", make a 'size' byte hole
@@ -526,7 +546,10 @@
 static void crash_test();
 static int crashme = 0;
 #endif
-
+#if ENABLE_FEATURE_VI_UNDO
+static char undo_push(char *, int, int);	// Push an operation on the undo stack
+static char undo_pop(void);	// Undo the last operation
+#endif
 
 static void write1(const char *out)
 {
@@ -540,6 +563,9 @@
 
 	INIT_G();
 
+#if ENABLE_FEATURE_VI_UNDO
+	undo_stack_tail = NULL;
+#endif
 #if ENABLE_FEATURE_VI_USE_SIGNALS || ENABLE_FEATURE_VI_CRASHME
 	my_pid = getpid();
 #endif
@@ -1269,7 +1295,7 @@
 			if (found) {
 				uintptr_t bias;
 				// we found the "find" pattern - delete it
-				text_hole_delete(found, found + len_F - 1);
+				text_hole_delete(found, found + len_F - 1, 1);
 				// inset the "replace" patern
 				bias = string_insert(found, R);	// insert the string
 				found += bias;
@@ -1655,7 +1681,7 @@
 
 static void dot_delete(void)	// delete the char at 'dot'
 {
-	text_hole_delete(dot, dot);
+	text_hole_delete(dot, dot, 0);
 }
 
 static char *bound_dot(char *p) // make sure  text[0] <= P < "end"
@@ -1811,6 +1837,9 @@
 		refresh(FALSE);	// show the ^
 		c = get_one_char();
 		*p = c;
+#if ENABLE_FEATURE_VI_UNDO
+		undo_push(p, 1, 1);
+#endif
 		p++;
 		file_modified++;
 	} else if (c == 27) {	// Is this an ESC?
@@ -1825,7 +1854,7 @@
 		//     123456789
 		if ((p[-1] != '\n') && (dot>text)) {
 			p--;
-			p = text_hole_delete(p, p);	// shrink buffer 1 char
+			p = text_hole_delete(p, p, 0);	// shrink buffer 1 char
 		}
 	} else {
 #if ENABLE_FEATURE_VI_SETOPTS
@@ -1838,6 +1867,9 @@
 #if ENABLE_FEATURE_VI_SETOPTS
 		sp = p;			// remember addr of insert
 #endif
+#if ENABLE_FEATURE_VI_UNDO
+		undo_push(p, 1, 1);
+#endif
 		p += 1 + stupid_insert(p, c);	// insert the char
 #if ENABLE_FEATURE_VI_SETOPTS
 		if (showmatch && strchr(")]}", *sp) != NULL) {
@@ -1853,6 +1885,9 @@
 				bias = text_hole_make(p, len);
 				p += bias;
 				q += bias;
+#if ENABLE_FEATURE_VI_UNDO
+				undo_push(p, len, 1);
+#endif
 				memcpy(p, q, len);
 				p += len;
 			}
@@ -2051,6 +2086,76 @@
 }
 #endif /* FEATURE_VI_SETOPTS */
 
+#if ENABLE_FEATURE_VI_UNDO
+// Undo functions added by Jody Bruchon (jody at jodybruchon.com)
+static char undo_push(char *src, int length, int type)	// Add to the undo stack
+{
+	struct undo_object *undo_temp;
+	// "type" values
+	// 0: deleted text, undo will restore to buffer
+	// 1: insertion, undo will remove from buffer
+	// 2: swap undo will perform the remove and insert operations in one shot
+	// Swap undo pushing is a two-operation process (push with 0, then with 2.)
+
+	if ((type > 2) || (type < 0)) return 1;	// only types 0,1,2 are valid
+	// Allocate a new undo object and use it as the stack tail
+	if (type == 2) {
+		undo_stack_tail->type = 2;	// Previous insert operation is part of a replace
+	} else {
+		undo_temp = undo_stack_tail;
+		undo_stack_tail = (struct undo_object *) malloc(sizeof(struct undo_object));
+		undo_stack_tail->prev = undo_temp;
+		undo_stack_tail->start = src - text;	// use offset from start of text buffer
+		undo_stack_tail->length = length;
+		undo_stack_tail->type = type;
+		if (type == 0) {
+				undo_stack_tail->undo_text = (char *) malloc(length);
+				memcpy(undo_stack_tail->undo_text, src, length);
+		}
+	}
+	return 0;
+}
+
+static char undo_pop(void)	// Undo the last operation
+{
+	int repeat = 0;
+	char *u_start, *u_end;
+	struct undo_object *undo_temp;
+
+	// Check for an empty undo stack first
+	if (undo_stack_tail != NULL) {
+		switch (undo_stack_tail->type) {
+			case 0:
+				// make hole and put in text that was deleted; deallocate text
+				u_start = text + undo_stack_tail->start;
+				text_hole_make(u_start, undo_stack_tail->length);
+				memcpy(u_start, undo_stack_tail->undo_text, undo_stack_tail->length);
+				free(undo_stack_tail->undo_text);
+				break;
+			case 1:
+			case 2:
+				// delete what was inserted
+				u_start = undo_stack_tail->start + text;
+				u_end = u_start - 1 + undo_stack_tail->length;
+				text_hole_delete(u_start, u_end, 1);
+				break;
+		}
+		if (undo_stack_tail->type == 2) repeat = 1;	// handle swap undo
+		// Lower modification count and deallocate undo object
+		file_modified--;
+		undo_temp = undo_stack_tail->prev;
+		free(undo_stack_tail);
+		undo_stack_tail = undo_temp;
+		if (repeat == 1) undo_pop();	// swap undo requires two runs
+	} else {
+		status_line("Already at oldest change");
+		return 1;
+	}
+	refresh(FALSE);
+	return 0;
+}
+#endif
+    
 // open a hole in text[]
 // might reallocate text[]! use p += text_hole_make(p, ...),
 // and be careful to not use pointers into potentially freed text[]!
@@ -2087,7 +2192,8 @@
 }
 
 //  close a hole in text[]
-static char *text_hole_delete(char *p, char *q) // delete "p" through "q", inclusive
+//  "undo" value indicates if this operation should be undo-able (0 = yes, 1 = no)
+static char *text_hole_delete(char *p, char *q, int undo) // delete "p" through "q", inclusive
 {
 	char *src, *dest;
 	int cnt, hole_size;
@@ -2102,6 +2208,9 @@
 	}
 	hole_size = q - p + 1;
 	cnt = end - src;
+#if ENABLE_FEATURE_VI_UNDO
+	if (undo == 0) undo_push(p, hole_size, 0);	// UNDO support
+#endif
 	if (src < text || src > end)
 		goto thd0;
 	if (dest < text || dest >= end)
@@ -2152,7 +2261,7 @@
 	text_yank(start, stop, YDreg);
 #endif
 	if (yf == YANKDEL) {
-		p = text_hole_delete(start, stop);
+		p = text_hole_delete(start, stop, 0);
 	}					// delete lines
 	return p;
 }
@@ -2224,6 +2333,9 @@
 	int i;
 
 	i = strlen(s);
+#if ENABLE_FEATURE_VI_UNDO
+	undo_push(p, i, 1);
+#endif
 	bias = text_hole_make(p, i);
 	p += bias;
 	memcpy(p, s, i);
@@ -2516,10 +2628,10 @@
 	cnt = safe_read(fd, p, size);
 	if (cnt < 0) {
 		status_line_bold_errno(fn);
-		p = text_hole_delete(p, p + size - 1);	// un-do buffer insert
+		p = text_hole_delete(p, p + size - 1, 1);	// un-do buffer insert
 	} else if (cnt < size) {
 		// There was a partial read, shrink unused space text[]
-		p = text_hole_delete(p + cnt, p + size - 1);	// un-do buffer insert
+		p = text_hole_delete(p + cnt, p + size - 1, 1);	// un-do buffer insert
 		status_line_bold("can't read '%s'", fn);
 	}
 	if (cnt >= size)
@@ -3053,6 +3165,7 @@
 				if (c != 27)
 					dot = yank_delete(dot, dot, 0, YANKDEL);	// delete char
 				dot = char_insert(dot, c);	// insert new char
+					undo_push(dot, 1, 2);
 			}
 			goto dc1;
 		}
@@ -3108,7 +3221,6 @@
 		//case ']':	// ]-
 		//case '_':	// _-
 		//case '`':	// `-
-		//case 'u':	// u- FIXME- there is no undo
 		//case 'v':	// v-
 	default:			// unrecognized command
 		buf[0] = c;
@@ -3254,11 +3366,16 @@
 		string_insert(dot, p);	// insert the string
 		end_cmd_q();	// stop adding to q
 		break;
+#if ENABLE_FEATURE_VI_UNDO
+	case 'u':	// u- undo last operation
+		undo_pop();
+		break;
+#endif
 	case 'U':			// U- Undo; replace current line with original version
 		if (reg[Ureg] != NULL) {
 			p = begin_line(dot);
 			q = end_line(dot);
-			p = text_hole_delete(p, q);	// delete cur line
+			p = text_hole_delete(p, q, 1);	// delete cur line
 			p += string_insert(p, reg[Ureg]);	// insert orig line
 			dot = p;
 			dot_skip_over_ws();
@@ -3494,11 +3611,11 @@
 				// shift left- remove tab or 8 spaces
 				if (*p == '\t') {
 					// shrink buffer 1 char
-					text_hole_delete(p, p);
+					text_hole_delete(p, p, 1);
 				} else if (*p == ' ') {
 					// we should be calculating columns, not just SPACE
 					for (j = 0; *p == ' ' && j < tabstop; j++) {
-						text_hole_delete(p, p);
+						text_hole_delete(p, p, 1);
 					}
 				}
 			} else if (c == '>') {


More information about the busybox mailing list