I have spent a lot of time working on it, and I finally have this patch for
complete undo support for BusyBox 'vi' with intermediate queuing capability.
This patch INCLUDES my previous "undo patch v3" and also fixes the following
issues with that patch:

* Cursor placement when undoing is now correct
* Undoing a 'dd' of the only line in the file no longer inserts a stray newline
* Undoing 'J' no longer randomly destroys the first char of the line

The biggest change is the "undo queuing" feature. When the undo queue feature is
compiled in, single-character operations such as typing and backspacing will be
grouped together. This greatly reduces malloc() calls while typing and makes
undo much more useful; instead of each 'u' keypress undoing one typed character,
all of the characters typed up to the queue size limit will be undone in one
shot.

The function undo_queue_commit() is called on certain keypresses to further
increase the usefulness of queuing. Changing between typing and backspacing or
moving the cursor during typing will "commit" the queue to an undo object,
thereby splitting them into logical groupings.

The queue size is configurable, the queue can be disabled, and undo support can
be disabled completely, which will compile BusyBox vi identically to how it is
built now. I have tested compilation AND as many of the possible text
manipulation functions as I could with all possible combinations of undo support
compiled in. I've tried to find bugs in the code in as many ways as possible,
and it seems to work correctly under everything I have thrown at it. I doubt
that this work on the undo feature can be improved any further.

Denys, would you mind trying this final patch out and applying it?

-Jody Bruchon


Signed-off-by: Jody Bruchon <[email protected]>
diff --git a/editors/vi.c b/editors/vi.c
index 097f309..fdabc81 100644
--- a/editors/vi.c
+++ b/editors/vi.c
@@ -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,36 @@
 //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.
+//config:config FEATURE_VI_UNDO_QUEUE
+//config:	bool "Enable undo operation queuing"
+//config:	default y
+//config:	depends on FEATURE_VI_UNDO
+//config:	help
+//config:	  The vi undo functions can use an intermediate queue to greatly lower
+//config:	  malloc() calls and overhead. When the maximum size of this queue is
+//config:	  reached, the contents of the queue are committed to the undo stack.
+//config:	  This increases the size of the undo code and allows some undo
+//config:	  operations (especially un-typing/backspacing) to be far more useful.
+//config:config FEATURE_VI_UNDO_QUEUE_MAX
+//config:	int "Maximum undo character queue size"
+//config:	default 256
+//config:	range 32 65536
+//config:	depends on FEATURE_VI_UNDO_QUEUE
+//config:	help
+//config:	  This option sets the number of bytes used at runtime for the queue.
+//config:	  Smaller values will create more undo objects and reduce the amount
+//config:	  of typed or backspaced characters that are grouped into one undo
+//config:	  operation; larger values increase the potential size of each undo
+//config:	  and will generally malloc() larger objects and less frequently.
+//config:	  Unless you want more (or less) frequent "undo points" while typing,
+//config:	  you should probably leave this unchanged.
 
 //applet:IF_VI(APPLET(vi, BB_DIR_BIN, BB_SUID_DROP))
 
@@ -347,6 +376,42 @@ struct globals {
 	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
+// undo_push() operations
+#define UNDO_INS 0
+#define UNDO_DEL 1
+#define UNDO_INS_CHAIN 2
+#define UNDO_DEL_CHAIN 3
+// UNDO_*_QUEUED must be equal to UNDO_xxx ORed with UNDO_QUEUED_FLAG
+#define UNDO_QUEUED_FLAG 4
+#define UNDO_INS_QUEUED 4
+#define UNDO_DEL_QUEUED 5
+#define UNDO_USE_SPOS 32
+#define UNDO_EMPTY 64
+// Pass-through flags for functions that can be undone
+#define NO_UNDO 0
+#define ALLOW_UNDO 1
+#define ALLOW_UNDO_CHAIN 2
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+#define ALLOW_UNDO_QUEUED 3
+	char undo_queue_state;
+	int undo_q;
+	char *undo_queue_spos;	// Start position of queued operation
+	char undo_queue[CONFIG_FEATURE_VI_UNDO_QUEUE_MAX];
+#else
+// If undo queuing disabled, don't invoke the missing queue logic
+#define ALLOW_UNDO_QUEUED 1
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+
+	struct undo_object {
+		struct undo_object *prev;	// Linking back avoids list traversal (LIFO)
+		int u_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 /* ENABLE_FEATURE_VI_UNDO */
+
 };
 #define G (*ptr_to_globals)
 #define text           (G.text          )
@@ -408,6 +473,16 @@ struct globals {
 #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)
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+#define undo_queue_state (G.undo_queue_state)
+#define undo_q (G.undo_q)
+#define undo_queue (G.undo_queue)
+#define undo_queue_spos (G.undo_queue_spos)
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+#endif /* ENABLE_FEATURE_VI_UNDO */
+
 #define INIT_G() do { \
 	SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
 	last_file_modified = -1; \
@@ -437,10 +512,13 @@ static void dot_next(void);	// move dot to next line B-o-l
 static void dot_prev(void);	// move dot to prev line B-o-l
 static void dot_scroll(int, int);	// move the screen up or down
 static void dot_skip_over_ws(void);	// move dot pat WS
-static void dot_delete(void);	// delete the char at 'dot'
 static char *bound_dot(char *);	// make sure  text[0] <= P < "end"
 static char *new_screen(int, int);	// malloc virtual screen memory
+#if ENABLE_FEATURE_VI_UNDO
+static char *char_insert(char *, char, int);	// insert the char c at 'p'
+#else
 static char *char_insert(char *, char);	// insert the char c at 'p'
+#endif
 // might reallocate text[]! use p += stupid_insert(p, ...),
 // and be careful to not use pointers into potentially freed text[]!
 static uintptr_t stupid_insert(char *, char);	// stupidly insert the char c at 'p'
@@ -448,11 +526,19 @@ static int find_range(char **, char **, char);	// return pointers for an object
 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
+#if ENABLE_FEATURE_VI_UNDO
+static char *text_hole_delete(char *, char *, int);	// at "p", delete a 'size' byte hole
+#else
+static char *text_hole_delete(char *, char *);
+#endif
 // 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
+#if ENABLE_FEATURE_VI_UNDO
+static char *yank_delete(char *, char *, int, int, int);	// yank text[] into register then delete
+#else
 static char *yank_delete(char *, char *, int, int);	// yank text[] into register then delete
+#endif
 static void show_help(void);	// display some help info
 static void rawmode(void);	// set "raw" mode on tty
 static void cookmode(void);	// return to "cooked" mode on tty
@@ -514,7 +600,11 @@ static void showmatching(char *);	// show the matching pair ()  []  {}
 #if ENABLE_FEATURE_VI_YANKMARK || (ENABLE_FEATURE_VI_COLON && ENABLE_FEATURE_VI_SEARCH) || ENABLE_FEATURE_VI_CRASHME
 // might reallocate text[]! use p += string_insert(p, ...),
 // and be careful to not use pointers into potentially freed text[]!
+#if ENABLE_FEATURE_VI_UNDO
+static uintptr_t string_insert(char *, const char *, int);	// insert the string at 'p'
+#else
 static uintptr_t string_insert(char *, const char *);	// insert the string at 'p'
+#endif	/* ENABLE_FEATURE_VI_UNDO */
 #endif
 #if ENABLE_FEATURE_VI_YANKMARK
 static char *text_yank(char *, char *, int);	// save copy of "p" into a register
@@ -526,7 +616,13 @@ static void crash_dummy();
 static void crash_test();
 static int crashme = 0;
 #endif
-
+#if ENABLE_FEATURE_VI_UNDO
+static char undo_push(char *, unsigned int, unsigned char);	// Push an operation on the undo stack
+static char undo_pop(void);	// Undo the last operation
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+static char undo_queue_commit(void);	// Flush any queued objects to the undo stack
+#endif
+#endif	/* ENABLE_FEATURE_VI_UNDO */
 
 static void write1(const char *out)
 {
@@ -540,6 +636,13 @@ int vi_main(int argc, char **argv)
 
 	INIT_G();
 
+#if ENABLE_FEATURE_VI_UNDO
+	undo_stack_tail = NULL;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+	undo_queue_state = UNDO_EMPTY;
+	undo_q = 0;
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+#endif /* ENABLE_FEATURE_VI_UNDO */
 #if ENABLE_FEATURE_VI_USE_SIGNALS || ENABLE_FEATURE_VI_CRASHME
 	my_pid = getpid();
 #endif
@@ -631,7 +734,11 @@ static int init_text_buffer(char *fn)
 	}
 	if (size < 0) {
 		// file dont exist. Start empty buf with dummy line
+#if ENABLE_FEATURE_VI_UNDO
+		char_insert(text, '\n', NO_UNDO);
+#else
 		char_insert(text, '\n');
+#endif
 		rc = 0;
 	} else {
 		rc = file_insert(fn, text, 1);
@@ -756,7 +863,11 @@ static void edit_file(char *fn)
 				crash_dummy();	// generate a random command
 			} else {
 				crashme = 0;
-				string_insert(text, "\n\n#####  Ran out of text to work on.  #####\n\n"); // insert the string
+#if ENABLE_FEATURE_VI_UNDO
+				string_insert(text, "\n\n#####  Ran out of text to work on.  #####\n\n", NO_UNDO); // insert the string
+#else
+				string_insert(text, "\n\n#####  Ran out of text to work on.  #####\n\n");
+#endif
 				dot = text;
 				refresh(FALSE);
 			}
@@ -1015,7 +1126,11 @@ static void colon(char *buf)
 			q = begin_line(dot);	// assume .,. for the range
 			r = end_line(dot);
 		}
-		dot = yank_delete(q, r, 1, YANKDEL);	// save, then delete lines
+#if ENABLE_FEATURE_VI_UNDO
+		dot = yank_delete(q, r, 1, YANKDEL, ALLOW_UNDO);	// save, then delete lines
+#else
+		dot = yank_delete(q, r, 1, YANKDEL);
+#endif
 		dot_skip_over_ws();
 	} else if (strncmp(cmd, "edit", i) == 0) {	// Edit a file
 		// don't edit, if the current file has been modified
@@ -1175,7 +1290,7 @@ static void colon(char *buf)
 			// if the insert is before "dot" then we need to update
 			if (q <= dot)
 				dot += ch;
-			/*file_modified++; - done by file_insert */
+			// file_modified++;
 		}
 	} else if (strncmp(cmd, "rewind", i) == 0) {	// rewind cmd line args
 		if (file_modified && !useforce) {
@@ -1235,6 +1350,9 @@ static void colon(char *buf)
 		char *F, *R, *flags;
 		size_t len_F, len_R;
 		int gflag;		// global replace flag
+#if ENABLE_FEATURE_VI_UNDO
+		int first_item = 0;
+#endif
 
 		// F points to the "find" pattern
 		// R points to the "replace" pattern
@@ -1269,9 +1387,20 @@ static void colon(char *buf)
 			if (found) {
 				uintptr_t bias;
 				// we found the "find" pattern - delete it
+				// For undo support, the first item should not be chained
+#if ENABLE_FEATURE_VI_UNDO
+				if (first_item == 0) {
+					text_hole_delete(found, found + len_F - 1, ALLOW_UNDO);
+					first_item = 1;
+				} else {
+					text_hole_delete(found, found + len_F - 1, ALLOW_UNDO_CHAIN);
+				}
+				// insert the "replace" patern
+				bias = string_insert(found, R, ALLOW_UNDO_CHAIN);	// insert the string
+#else
 				text_hole_delete(found, found + len_F - 1);
-				// inset the "replace" patern
 				bias = string_insert(found, R);	// insert the string
+#endif
 				found += bias;
 				ls += bias;
 				/*q += bias; - recalculated anyway */
@@ -1572,23 +1701,35 @@ static char *find_line(int li)	// find begining of line #li
 //----- Dot Movement Routines ----------------------------------
 static void dot_left(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	if (dot > text && dot[-1] != '\n')
 		dot--;
 }
 
 static void dot_right(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	if (dot < end - 1 && *dot != '\n')
 		dot++;
 }
 
 static void dot_begin(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	dot = begin_line(dot);	// return pointer to first char cur line
 }
 
 static void dot_end(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	dot = end_line(dot);	// return pointer to last char cur line
 }
 
@@ -1614,11 +1755,17 @@ static char *move_to_col(char *p, int l)
 
 static void dot_next(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	dot = next_line(dot);
 }
 
 static void dot_prev(void)
 {
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	dot = prev_line(dot);
 }
 
@@ -1626,6 +1773,9 @@ static void dot_scroll(int cnt, int dir)
 {
 	char *q;
 
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 	for (; cnt > 0; cnt--) {
 		if (dir < 0) {
 			// scroll Backwards
@@ -1653,11 +1803,6 @@ static void dot_skip_over_ws(void)
 		dot++;
 }
 
-static void dot_delete(void)	// delete the char at 'dot'
-{
-	text_hole_delete(dot, dot);
-}
-
 static char *bound_dot(char *p) // make sure  text[0] <= P < "end"
 {
 	if (p >= end && end > text) {
@@ -1804,17 +1949,40 @@ static char *char_search(char *p, const char *pat, int dir, int range)
 
 #endif /* FEATURE_VI_SEARCH */
 
+#if ENABLE_FEATURE_VI_UNDO
+static char *char_insert(char *p, char c, int undo) // insert the char c at 'p'
+#else
 static char *char_insert(char *p, char c) // insert the char c at 'p'
+#endif
 {
 	if (c == 22) {		// Is this an ctrl-V?
 		p += stupid_insert(p, '^');	// use ^ to indicate literal next
 		refresh(FALSE);	// show the ^
 		c = get_one_char();
 		*p = c;
-		p++;
+#if ENABLE_FEATURE_VI_UNDO
+		switch (undo) {
+			case ALLOW_UNDO:
+				undo_push(p, 1, UNDO_INS);
+				break;
+			case ALLOW_UNDO_CHAIN:
+				undo_push(p, 1, UNDO_INS_CHAIN);
+				break;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+			case ALLOW_UNDO_QUEUED:
+				undo_push(p, 1, UNDO_INS_QUEUED);
+				break;
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+		}
+#else
 		file_modified++;
+#endif /* ENABLE_FEATURE_VI_UNDO */
+		p++;
 	} else if (c == 27) {	// Is this an ESC?
 		cmd_mode = 0;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 		cmdcnt = 0;
 		end_cmd_q();	// stop adding to q
 		last_status_cksum = 0;	// force status update
@@ -1825,7 +1993,11 @@ static char *char_insert(char *p, char c) // insert the char c at 'p'
 		//     123456789
 		if ((p[-1] != '\n') && (dot>text)) {
 			p--;
-			p = text_hole_delete(p, p);	// shrink buffer 1 char
+#if ENABLE_FEATURE_VI_UNDO
+			p = text_hole_delete(p, p, ALLOW_UNDO_QUEUED);	// shrink buffer 1 char
+#else
+			p = text_hole_delete(p, p);
+#endif
 		}
 	} else {
 #if ENABLE_FEATURE_VI_SETOPTS
@@ -1838,6 +2010,26 @@ static char *char_insert(char *p, char c) // insert the char c at 'p'
 #if ENABLE_FEATURE_VI_SETOPTS
 		sp = p;			// remember addr of insert
 #endif
+#if ENABLE_FEATURE_VI_UNDO
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		if (c == '\n') undo_queue_commit();
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+		switch (undo) {
+			case ALLOW_UNDO:
+				undo_push(p, 1, UNDO_INS);
+				break;
+			case ALLOW_UNDO_CHAIN:
+				undo_push(p, 1, UNDO_INS_CHAIN);
+				break;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+			case ALLOW_UNDO_QUEUED:
+				undo_push(p, 1, UNDO_INS_QUEUED);
+				break;
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+		}
+#else
+		file_modified++;
+#endif /* ENABLE_FEATURE_VI_UNDO */
 		p += 1 + stupid_insert(p, c);	// insert the char
 #if ENABLE_FEATURE_VI_SETOPTS
 		if (showmatch && strchr(")]}", *sp) != NULL) {
@@ -1853,6 +2045,9 @@ static char *char_insert(char *p, char c) // insert the char c at 'p'
 				bias = text_hole_make(p, len);
 				p += bias;
 				q += bias;
+#if ENABLE_FEATURE_VI_UNDO
+				undo_push(p, len, UNDO_INS);
+#endif
 				memcpy(p, q, len);
 				p += len;
 			}
@@ -1870,7 +2065,6 @@ static uintptr_t stupid_insert(char *p, char c) // stupidly insert the char c at
 	bias = text_hole_make(p, 1);
 	p += bias;
 	*p = c;
-	//file_modified++; - done by text_hole_make()
 	return bias;
 }
 
@@ -2051,6 +2245,194 @@ static void showmatching(char *p)
 }
 #endif /* FEATURE_VI_SETOPTS */
 
+#if ENABLE_FEATURE_VI_UNDO
+// Undo functions and hooks added by Jody Bruchon ([email protected])
+static char undo_push(char *src, unsigned int length, unsigned char u_type)	// Add to the undo stack
+{
+	struct undo_object *undo_temp;
+	// "u_type" values
+	// UNDO_INS: insertion, undo will remove from buffer
+	// UNDO_DEL: deleted text, undo will restore to buffer
+	// UNDO_{INS,DEL}_CHAIN: Same as above but also calls undo_pop() when complete
+	// The CHAIN operations are for handling multiple operations that the user
+	// performs with a single action, i.e. REPLACE mode or find-and-replace commands
+	// UNDO_{INS,DEL}_QUEUED: If queuing feature is enabled, allow use of the queue
+	// for the INS/DEL operation. The raw values should be equal to the values of
+	// UNDO_{INS,DEL} ORed with UNDO_QUEUED_FLAG
+
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+	// This undo queuing functionality groups multiple character typing or backspaces
+	// into a single large undo object. This greatly reduces calls to malloc() for
+	// single-character operations while typing and has the side benefit of letting
+	// an undo operation remove chunks of text rather than a single character.
+	switch (u_type) {
+		case UNDO_EMPTY:	// Just in case this ever happens...
+			return 0;
+			break;
+		case UNDO_DEL_QUEUED:
+			if (length != 1) return 1;	// Only queue single characters
+			switch (undo_queue_state) {
+				case UNDO_EMPTY:
+					undo_queue_state = UNDO_DEL;
+				case UNDO_DEL:
+					undo_queue_spos = src;
+					undo_q++;
+					undo_queue[(CONFIG_FEATURE_VI_UNDO_QUEUE_MAX - undo_q)] = *src;
+					// If queue is full, dump it into an object
+					if (undo_q == CONFIG_FEATURE_VI_UNDO_QUEUE_MAX) undo_queue_commit();
+					return 0;
+					break;
+				case UNDO_INS:
+					// Switch from storing inserted text to deleted text
+					undo_queue_commit();
+					undo_push(src, length, UNDO_DEL_QUEUED);
+					return 0;
+					break;
+			}
+			break;
+		case UNDO_INS_QUEUED:
+			if (length != 1) return 1;
+			switch (undo_queue_state) {
+				case UNDO_EMPTY:
+					undo_queue_state = UNDO_INS;
+					undo_queue_spos = src;
+				case UNDO_INS:
+					undo_q++;	// Don't need to save any data for insertions
+					if (undo_q == CONFIG_FEATURE_VI_UNDO_QUEUE_MAX) undo_queue_commit();
+					return 0;
+					break;
+				case UNDO_DEL:
+					// Switch from storing deleted text to inserted text
+					undo_queue_commit();
+					undo_push(src, length, UNDO_INS_QUEUED);
+					return 0;
+					break;
+			}
+			break;
+	}
+#else
+	// If undo queuing is disabled, ignore the queuing flag entirely
+	u_type = u_type & ~UNDO_QUEUED_FLAG;
+#endif
+
+	// Allocate a new undo object and use it as the stack tail
+	undo_temp = undo_stack_tail;
+	undo_stack_tail = (struct undo_object *) malloc(sizeof(struct undo_object));
+	if (undo_stack_tail == NULL) {
+		undo_stack_tail = undo_temp;
+		status_line_bold("Undo: out of memory");
+		return 1;
+	}
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+	if ((u_type & UNDO_USE_SPOS) != 0) {
+		undo_stack_tail->start = undo_queue_spos - text;	// use start position from queue
+	}	else undo_stack_tail->start = src - text;	// use offset from start of text buffer
+	u_type = (u_type & ~UNDO_USE_SPOS);
+#else
+	undo_stack_tail->start = src - text;
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+	// For UNDO_DEL objects, copy the deleted text somewhere
+	switch (u_type) {
+		case UNDO_DEL:
+		case UNDO_DEL_CHAIN:
+			if ((src + length) == end) length--;
+			// If this deletion empties text[], strip the newline. When the buffer becomes
+			// zero-length, a newline is added back, which requires this to compensate.
+			undo_stack_tail->undo_text = (char *) malloc(length);
+			if (undo_stack_tail->undo_text == NULL) {
+				undo_stack_tail = undo_temp;
+				status_line_bold("Undo: out of memory");
+				return 1;
+			}
+			memcpy(undo_stack_tail->undo_text, src, length);
+			break;
+	}
+	undo_stack_tail->prev = undo_temp;
+	undo_stack_tail->length = length;
+	undo_stack_tail->u_type = u_type;
+	file_modified++;
+	return 0;
+}
+
+static char undo_pop(void)	// Undo the last operation
+{
+	int repeat = 0;
+	char *u_start, *u_end;
+	struct undo_object *undo_temp;
+
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+	// Commit pending undo queue before popping (should be unnecessary)
+	undo_queue_commit();
+#endif
+
+	// Check for an empty undo stack
+	if (undo_stack_tail != NULL) {
+		switch (undo_stack_tail->u_type) {
+			case UNDO_DEL:
+			case UNDO_DEL_CHAIN:
+				// 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);
+				status_line("Undo [%d] restored %d chars at position %d",
+					file_modified,undo_stack_tail->length,undo_stack_tail->start);
+				break;
+			case UNDO_INS:
+			case UNDO_INS_CHAIN:
+				// 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, NO_UNDO);
+				status_line("Undo [%d] deleted %d chars at position %d",
+					file_modified,undo_stack_tail->length,undo_stack_tail->start);
+				break;
+		}
+		// For chained operations, continue popping all the way down the chain.
+		// If this is the end of a chain, lower modification count and refresh display
+		switch (undo_stack_tail->u_type) {
+			case UNDO_DEL:
+			case UNDO_INS:
+				dot = (text + undo_stack_tail->start);
+				refresh(FALSE);
+				break;
+			case UNDO_DEL_CHAIN:
+			case UNDO_INS_CHAIN:
+				repeat = 1;
+				break;
+		}
+		// Deallocate the undo object we just processed
+		undo_temp = undo_stack_tail->prev;
+		free(undo_stack_tail);
+		undo_stack_tail = undo_temp;
+		file_modified--;
+		if (repeat == 1) {
+			undo_pop();	// Follow the undo chain if one exists
+			return 0;
+		}
+	} else {
+		status_line("Already at oldest change");
+		return 1;
+	}
+	return 0;
+}
+
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+static char undo_queue_commit(void)	// Flush any queued objects to the undo stack
+{
+	// Pushes the queue object onto the undo stack
+	if (undo_q > 0) {
+		// Deleted character undo events grow from the end
+		undo_push((undo_queue + CONFIG_FEATURE_VI_UNDO_QUEUE_MAX - undo_q), undo_q, (undo_queue_state | UNDO_USE_SPOS));
+		undo_queue_state = UNDO_EMPTY;
+		undo_q = 0;
+	}
+	return 0;
+}
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+
+#endif /* ENABLE_FEATURE_VI_UNDO */
+
 // 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[]!
@@ -2082,12 +2464,16 @@ static uintptr_t text_hole_make(char *p, int size)	// at "p", make a 'size' byte
 	}
 	memmove(p + size, p, end - size - p);
 	memset(p, ' ', size);	// clear new hole
-	file_modified++;
 	return bias;
 }
 
 //  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
+#if ENABLE_FEATURE_VI_UNDO
+static char *text_hole_delete(char *p, char *q, int undo) // delete "p" through "q", inclusive
+#else
+static char *text_hole_delete(char *p, char *q)
+#endif
 {
 	char *src, *dest;
 	int cnt, hole_size;
@@ -2102,10 +2488,29 @@ static char *text_hole_delete(char *p, char *q) // delete "p" through "q", inclu
 	}
 	hole_size = q - p + 1;
 	cnt = end - src;
+#if ENABLE_FEATURE_VI_UNDO
+	switch (undo) {
+		case NO_UNDO:
+			break;
+		case ALLOW_UNDO:
+			undo_push(p, hole_size, UNDO_DEL);
+			break;
+		case ALLOW_UNDO_CHAIN:
+			undo_push(p, hole_size, UNDO_DEL_CHAIN);
+			break;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		case ALLOW_UNDO_QUEUED:
+			undo_push(p, hole_size, UNDO_DEL_QUEUED);
+			break;
+#endif /* ENABLE_FEATURE_VI_UNDO_QUEUE */
+	}
+	file_modified--;
+#endif
 	if (src < text || src > end)
 		goto thd0;
 	if (dest < text || dest >= end)
 		goto thd0;
+	file_modified++;
 	if (src >= end)
 		goto thd_atend;	// just delete the end of the buffer
 	memmove(dest, src, cnt);
@@ -2115,7 +2520,6 @@ static char *text_hole_delete(char *p, char *q) // delete "p" through "q", inclu
 		dest = end - 1;	// make sure dest in below end-1
 	if (end <= text)
 		dest = end = text;	// keep pointers valid
-	file_modified++;
  thd0:
 	return dest;
 }
@@ -2123,7 +2527,11 @@ static char *text_hole_delete(char *p, char *q) // delete "p" through "q", inclu
 // copy text into register, then delete text.
 // if dist <= 0, do not include, or go past, a NewLine
 //
+#if ENABLE_FEATURE_VI_UNDO
+static char *yank_delete(char *start, char *stop, int dist, int yf, int undo)
+#else
 static char *yank_delete(char *start, char *stop, int dist, int yf)
+#endif
 {
 	char *p;
 
@@ -2152,7 +2560,11 @@ static char *yank_delete(char *start, char *stop, int dist, int yf)
 	text_yank(start, stop, YDreg);
 #endif
 	if (yf == YANKDEL) {
+#if ENABLE_FEATURE_VI_UNDO
+		p = text_hole_delete(start, stop, undo);
+#else
 		p = text_hole_delete(start, stop);
+#endif
 	}					// delete lines
 	return p;
 }
@@ -2218,12 +2630,26 @@ static void end_cmd_q(void)
  || ENABLE_FEATURE_VI_CRASHME
 // might reallocate text[]! use p += string_insert(p, ...),
 // and be careful to not use pointers into potentially freed text[]!
-static uintptr_t string_insert(char *p, const char *s) // insert the string at 'p'
+#if ENABLE_FEATURE_VI_UNDO
+static uintptr_t string_insert(char *p, const char *s, int undo) // insert the string at 'p'
+#else
+static uintptr_t string_insert(char *p, const char *s)
+#endif
 {
 	uintptr_t bias;
 	int i;
 
 	i = strlen(s);
+#if ENABLE_FEATURE_VI_UNDO
+	switch (undo) {
+		case ALLOW_UNDO:
+			undo_push(p, i, UNDO_INS);
+			break;
+		case ALLOW_UNDO_CHAIN:
+			undo_push(p, i, UNDO_INS_CHAIN);
+			break;
+	}
+#endif
 	bias = text_hole_make(p, i);
 	p += bias;
 	memcpy(p, s, i);
@@ -2516,14 +2942,22 @@ static int file_insert(const char *fn, char *p, int update_ro_status)
 	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
+#if ENABLE_FEATURE_VI_UNDO
+		p = text_hole_delete(p, p + size - 1, NO_UNDO);	// un-do buffer insert
+#else
+		p = text_hole_delete(p, p + size - 1);
+#endif
 	} 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
+#if ENABLE_FEATURE_VI_UNDO
+		p = text_hole_delete(p + cnt, p + size - 1, NO_UNDO);	// un-do buffer insert
+#else
+		p = text_hole_delete(p + cnt, p + size - 1);
+#endif
 		status_line_bold("can't read '%s'", fn);
 	}
-	if (cnt >= size)
-		file_modified++;
+//	if (cnt >= size)
+//		file_modified++;
 	close(fd);
  fi0:
 #if ENABLE_FEATURE_VI_READONLY
@@ -3048,11 +3482,19 @@ static void do_cmd(int c)
 		if (*dot == '\n') {
 			// don't Replace past E-o-l
 			cmd_mode = 1;	// convert to insert
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+			undo_queue_commit();
+#endif
 		} else {
 			if (1 <= c || Isprint(c)) {
 				if (c != 27)
-					dot = yank_delete(dot, dot, 0, YANKDEL);	// delete char
-				dot = char_insert(dot, c);	// insert new char
+#if ENABLE_FEATURE_VI_UNDO
+					dot = yank_delete(dot, dot, 0, YANKDEL, ALLOW_UNDO);	// delete char
+					dot = char_insert(dot, c, ALLOW_UNDO_CHAIN);	// insert new char
+#else
+					dot = yank_delete(dot, dot, 0, YANKDEL);
+					dot = char_insert(dot, c);
+#endif
 			}
 			goto dc1;
 		}
@@ -3062,7 +3504,11 @@ static void do_cmd(int c)
 		if (c == KEYCODE_INSERT) goto dc5;
 		// insert the char c at "dot"
 		if (1 <= c || Isprint(c)) {
+#if ENABLE_FEATURE_VI_UNDO
+			dot = char_insert(dot, c, ALLOW_UNDO_QUEUED);
+#else
 			dot = char_insert(dot, c);
+#endif
 		}
 		goto dc1;
 	}
@@ -3108,7 +3554,6 @@ static void do_cmd(int c)
 		//case ']':	// ]-
 		//case '_':	// _-
 		//case '`':	// `-
-		//case 'u':	// u- FIXME- there is no undo
 		//case 'v':	// v-
 	default:			// unrecognized command
 		buf[0] = c;
@@ -3177,6 +3622,9 @@ static void do_cmd(int c)
 		if (cmd_mode == 0)
 			indicate_error(c);
 		cmd_mode = 0;	// stop insrting
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 		end_cmd_q();
 		last_status_cksum = 0;	// force status update
 		break;
@@ -3251,15 +3699,29 @@ static void do_cmd(int c)
 			if (c == 'p')
 				dot_right();	// move to right, can move to NL
 		}
+#if ENABLE_FEATURE_VI_UNDO
+		string_insert(dot, p, ALLOW_UNDO);	// insert the string
+#else
 		string_insert(dot, p);	// insert the string
+#endif
 		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
+#if ENABLE_FEATURE_VI_UNDO
+			p = text_hole_delete(p, q, NO_UNDO);	// delete cur line
+			p += string_insert(p, reg[Ureg], NO_UNDO);	// insert orig line
+#else
+			p = text_hole_delete(p, q);
 			p += string_insert(p, reg[Ureg]);	// insert orig line
+#endif
 			dot = p;
 			dot_skip_over_ws();
 		}
@@ -3485,7 +3947,11 @@ static void do_cmd(int c)
 		cnt = count_lines(text, dot);	// remember what line we are on
 		c1 = get_one_char();	// get the type of thing to delete
 		find_range(&p, &q, c1);
-		yank_delete(p, q, 1, YANKONLY);	// save copy before change
+#if ENABLE_FEATURE_VI_UNDO
+		yank_delete(p, q, 1, YANKONLY, NO_UNDO);	// save copy before change
+#else
+		yank_delete(p, q, 1, YANKONLY);
+#endif
 		p = begin_line(p);
 		q = end_line(q);
 		i = count_lines(p, q);	// # of lines we are shifting
@@ -3494,16 +3960,28 @@ static void do_cmd(int c)
 				// shift left- remove tab or 8 spaces
 				if (*p == '\t') {
 					// shrink buffer 1 char
+#if ENABLE_FEATURE_VI_UNDO
+					text_hole_delete(p, p, NO_UNDO);
+#else
 					text_hole_delete(p, p);
+#endif
 				} else if (*p == ' ') {
 					// we should be calculating columns, not just SPACE
 					for (j = 0; *p == ' ' && j < tabstop; j++) {
+#if ENABLE_FEATURE_VI_UNDO
+						text_hole_delete(p, p, NO_UNDO);
+#else
 						text_hole_delete(p, p);
+#endif
 					}
 				}
 			} else if (c == '>') {
 				// shift right -- add tab or 8 spaces
+#if ENABLE_FEATURE_VI_UNDO
+				char_insert(p, '\t', ALLOW_UNDO);
+#else
 				char_insert(p, '\t');
+#endif
 			}
 		}
 		dot = find_line(cnt);	// what line were we on
@@ -3538,7 +4016,11 @@ static void do_cmd(int c)
 		save_dot = dot;
 		dot = dollar_line(dot);	// move to before NL
 		// copy text into a register and delete
-		dot = yank_delete(save_dot, dot, 0, YANKDEL);	// delete to e-o-l
+#if ENABLE_FEATURE_VI_UNDO
+		dot = yank_delete(save_dot, dot, 0, YANKDEL, ALLOW_UNDO);	// delete to e-o-l
+#else
+		dot = yank_delete(save_dot, dot, 0, YANKDEL);
+#endif
 		if (c == 'C')
 			goto dc_i;	// start inserting
 #if ENABLE_FEATURE_VI_DOT_CMD
@@ -3583,15 +4065,28 @@ static void do_cmd(int c)
 	case KEYCODE_INSERT:	// Cursor Key Insert
  dc_i:
 		cmd_mode = 1;	// start inserting
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();	// commit queue when cmd_mode changes
+#endif
 		break;
 	case 'J':			// J- join current and next lines together
 		do {
 			dot_end();		// move to NL
 			if (dot < end - 1) {	// make sure not last char in text[]
+#if ENABLE_FEATURE_VI_UNDO
+				undo_push(dot, 1, UNDO_DEL);
 				*dot++ = ' ';	// replace NL with space
+				undo_push((dot - 1), 1, UNDO_INS_CHAIN);
+#else
+				*dot++ = ' ';
 				file_modified++;
+#endif
 				while (isblank(*dot)) {	// delete leading WS
-					dot_delete();
+#if ENABLE_FEATURE_VI_UNDO
+					text_hole_delete(dot, dot, ALLOW_UNDO_CHAIN);
+#else
+					text_hole_delete(dot, dot);
+#endif
 				}
 			}
 		} while (--cmdcnt > 0);
@@ -3620,10 +4115,18 @@ static void do_cmd(int c)
 			dot_prev();
 	case 'o':			// o- open a empty line below; Yes, I know it is in the middle of the "if (..."
 			dot_end();
+#if ENABLE_FEATURE_VI_UNDO
+			dot = char_insert(dot, '\n', ALLOW_UNDO);
+#else
 			dot = char_insert(dot, '\n');
+#endif
 		} else {
 			dot_begin();	// 0
-			dot = char_insert(dot, '\n');	// i\n ESC
+#if ENABLE_FEATURE_VI_UNDO
+			dot = char_insert(dot, '\n', ALLOW_UNDO);	// i\n ESC
+#else
+			dot = char_insert(dot, '\n');
+#endif
 			dot_prev();	// -
 		}
 		goto dc_i;
@@ -3631,6 +4134,9 @@ static void do_cmd(int c)
 	case 'R':			// R- continuous Replace char
  dc5:
 		cmd_mode = 2;
+#if ENABLE_FEATURE_VI_UNDO_QUEUE
+		undo_queue_commit();
+#endif
 		break;
 	case KEYCODE_DELETE:
 		c = 'x';
@@ -3645,7 +4151,11 @@ static void do_cmd(int c)
 			if (dot[dir] != '\n') {
 				if (c == 'X')
 					dot--;	// delete prev char
-				dot = yank_delete(dot, dot, 0, YANKDEL);	// delete char
+#if ENABLE_FEATURE_VI_UNDO
+				dot = yank_delete(dot, dot, 0, YANKDEL, ALLOW_UNDO);	// delete char
+#else
+				dot = yank_delete(dot, dot, 0, YANKDEL);
+#endif
 			}
 		} while (--cmdcnt > 0);
 		end_cmd_q();	// stop adding to q
@@ -3716,6 +4226,7 @@ static void do_cmd(int c)
 			c1 = get_one_char();	// get the type of thing to delete
 		// determine range, and whether it spans lines
 		ml = find_range(&p, &q, c1);
+		place_cursor(0,0);
 		if (c1 == 27) {	// ESC- user changed mind and wants out
 			c = c1 = 27;	// Escape- do nothing
 		} else if (strchr("wW", c1)) {
@@ -3727,13 +4238,21 @@ static void do_cmd(int c)
 					q--;
 				}
 			}
-			dot = yank_delete(p, q, ml, yf);	// delete word
+#if ENABLE_FEATURE_VI_UNDO
+			dot = yank_delete(p, q, ml, yf, ALLOW_UNDO);	// delete word
 		} else if (strchr("^0bBeEft%$ lh\b\177", c1)) {
 			// partial line copy text into a register and delete
-			dot = yank_delete(p, q, ml, yf);	// delete word
+			dot = yank_delete(p, q, ml, yf, ALLOW_UNDO);	// delete word
 		} else if (strchr("cdykjHL+-{}\r\n", c1)) {
 			// whole line copy text into a register and delete
-			dot = yank_delete(p, q, ml, yf);	// delete lines
+			dot = yank_delete(p, q, ml, yf, ALLOW_UNDO);	// delete lines
+#else
+			dot = yank_delete(p, q, ml, yf);
+		} else if (strchr("^0bBeEft%$ lh\b\177", c1)) {
+			dot = yank_delete(p, q, ml, yf);
+		} else if (strchr("cdykjHL+-{}\r\n", c1)) {
+			dot = yank_delete(p, q, ml, yf);
+#endif
 			whole = 1;
 		} else {
 			// could not recognize object
@@ -3743,7 +4262,11 @@ static void do_cmd(int c)
 		}
 		if (ml && whole) {
 			if (c == 'c') {
+#if ENABLE_FEATURE_VI_UNDO
+				dot = char_insert(dot, '\n', ALLOW_UNDO_CHAIN);
+#else
 				dot = char_insert(dot, '\n');
+#endif
 				// on the last line of file don't move to prev line
 				if (whole && dot != (end-1)) {
 					dot_prev();
@@ -3789,8 +4312,14 @@ static void do_cmd(int c)
 	case 'r':			// r- replace the current char with user input
 		c1 = get_one_char();	// get the replacement char
 		if (*dot != '\n') {
+#if ENABLE_FEATURE_VI_UNDO
+			undo_push(dot, 1, UNDO_DEL);
+			*dot = c1;
+			undo_push(dot, 1, UNDO_INS_CHAIN);
+#else
 			*dot = c1;
 			file_modified++;
+#endif
 		}
 		end_cmd_q();	// stop adding to q
 		break;
@@ -3830,6 +4359,17 @@ static void do_cmd(int c)
 		break;
 	case '~':			// ~- flip the case of letters   a-z -> A-Z
 		do {
+#if ENABLE_FEATURE_VI_UNDO
+			if (islower(*dot)) {
+				undo_push(dot, 1, UNDO_DEL);
+				*dot = toupper(*dot);
+				undo_push(dot, 1, UNDO_INS_CHAIN);
+			} else if (isupper(*dot)) {
+				undo_push(dot, 1, UNDO_DEL);
+				*dot = tolower(*dot);
+				undo_push(dot, 1, UNDO_INS_CHAIN);
+			}
+#else
 			if (islower(*dot)) {
 				*dot = toupper(*dot);
 				file_modified++;
@@ -3837,6 +4377,7 @@ static void do_cmd(int c)
 				*dot = tolower(*dot);
 				file_modified++;
 			}
+#endif
 			dot_right();
 		} while (--cmdcnt > 0);
 		end_cmd_q();	// stop adding to q
@@ -3866,7 +4407,11 @@ static void do_cmd(int c)
  dc1:
 	// if text[] just became empty, add back an empty line
 	if (end == text) {
-		char_insert(text, '\n');	// start empty buf with dummy line
+#if ENABLE_FEATURE_VI_UNDO
+		char_insert(text, '\n', NO_UNDO);	// start empty buf with dummy line
+#else
+		char_insert(text, '\n');
+#endif
 		dot = text;
 	}
 	// it is OK for dot to exactly equal to end, otherwise check dot validity
_______________________________________________
busybox mailing list
[email protected]
http://lists.busybox.net/mailman/listinfo/busybox

Reply via email to