undo.c (9769B)
1 #include <string.h> 2 #include <stdlib.h> 3 #include <stdint.h> 4 5 #include <X11/Xlib.h> 6 #include <X11/Xutil.h> 7 #include <pango/pangoxft.h> 8 9 #include "util.h" 10 #include "memory.h" 11 #include "common.h" 12 #include "txtbuf.h" 13 #include "cache.h" 14 #include "undo.h" 15 #include "assert.h" 16 17 /* FIXME: more sanity checks in case text is 18 inserted/deleted without adding to undo stack */ 19 /* FIXME: cursor positions can be a bit weird when 20 undo is used across different insert sessions in 21 insert mode - e.g. if some text is inserted, then 22 'o' is used in normal mode to append a new line and 23 type some text, the cursor position at a certain 24 undo position will differ in insert mode depending 25 on whether it was reached with undo or redo since 26 'o' saves the position at which it was pressed, 27 not the position of the last insert */ 28 29 enum operation { 30 UNDO_INSERT, 31 UNDO_DELETE 32 }; 33 34 typedef struct { 35 txtbuf *text; 36 enum operation type; 37 ledit_mode mode; 38 ledit_range op_range; 39 ledit_range cursor_range; 40 int group; 41 int mode_group; 42 } undo_elem; 43 44 struct undo_stack { 45 size_t len, cur, cap; 46 undo_elem *stack; 47 int cur_valid; 48 int change_mode_group; 49 }; 50 51 undo_stack * 52 undo_stack_create(void) { 53 undo_stack *undo = ledit_malloc(sizeof(undo_stack)); 54 undo->len = undo->cap = 0; 55 undo->cur = 0; 56 undo->cur_valid = 0; 57 undo->stack = NULL; 58 undo->change_mode_group = 0; 59 return undo; 60 } 61 62 void 63 undo_stack_destroy(undo_stack *undo) { 64 for (size_t i = 0; i < undo->cap; i++) { 65 if (undo->stack[i].text != NULL) 66 txtbuf_destroy(undo->stack[i].text); 67 } 68 free(undo->stack); 69 free(undo); 70 } 71 72 /* FIXME: resize text buffers when they aren't needed anymore */ 73 static undo_elem * 74 push_undo_elem(undo_stack *undo) { 75 if (undo->cur_valid) 76 undo->cur++; 77 else 78 undo->cur = 0; 79 undo->cur_valid = 1; 80 undo->len = add_sz(undo->cur, 1); 81 if (undo->len > undo->cap) { 82 size_t cap = ideal_array_size(undo->cap, undo->len); 83 undo->stack = ledit_reallocarray(undo->stack, cap, sizeof(undo_elem)); 84 for (size_t i = undo->cap; i < cap; i++) { 85 undo->stack[i].text = NULL; 86 } 87 undo->cap = cap; 88 } 89 return &undo->stack[undo->cur]; 90 } 91 92 static undo_elem * 93 peek_undo_elem(undo_stack *undo) { 94 if (!undo->cur_valid) 95 return NULL; 96 return &undo->stack[undo->cur]; 97 } 98 99 void 100 undo_change_mode_group(undo_stack *undo) { 101 undo->change_mode_group = 1; 102 } 103 104 #if TEST 105 void 106 dump_undo_stack(FILE *file, undo_stack *undo) { 107 fprintf( 108 file, 109 "cur: %zu, cur_valid: %d, change_mode_group: %d, len: %zu, cap: %zu\n", 110 undo->cur, undo->cur_valid, undo->change_mode_group, undo->len, undo->cap 111 ); 112 for (size_t i = 0; i < undo->len; i++) { 113 undo_elem *e = &undo->stack[i]; 114 fprintf( 115 file, 116 "type %d, mode %d, group %d, mode_group %d, text '%.*s', " 117 "op_range (%zu,%zu;%zu,%zu), cursor_range (%zu,%zu;%zu,%zu)\n", 118 e->type, e->mode, e->group, e->mode_group, (int)e->text->len, e->text->text, 119 e->op_range.line1, e->op_range.byte1, e->op_range.line2, e->op_range.byte2, 120 e->cursor_range.line1, e->cursor_range.byte1, e->cursor_range.line2, e->cursor_range.byte2 121 ); 122 } 123 } 124 #endif 125 126 static void 127 push_undo( 128 undo_stack *undo, txtbuf *text, 129 ledit_range insert_range, /* maybe not the best name */ 130 ledit_range cursor_range, 131 int start_group, 132 enum operation type, ledit_mode mode) { 133 undo_elem *old = peek_undo_elem(undo); 134 int last_group = old == NULL ? 0 : old->group; 135 int last_mode_group = old == NULL ? 0 : old->mode_group; 136 undo_elem *e = push_undo_elem(undo); 137 e->group = start_group ? !last_group : last_group; 138 e->mode_group = undo->change_mode_group ? !last_mode_group : last_mode_group; 139 undo->change_mode_group = 0; 140 e->op_range = insert_range; 141 e->cursor_range = cursor_range; 142 e->mode = mode; 143 e->type = type; 144 if (e->text != NULL) 145 txtbuf_copy(e->text, text); 146 else 147 e->text = txtbuf_dup(text); 148 } 149 150 void 151 undo_push_insert( 152 undo_stack *undo, txtbuf *text, 153 ledit_range insert_range, ledit_range cursor_range, 154 int start_group, ledit_mode mode) { 155 push_undo( 156 undo, text, insert_range, cursor_range, 157 start_group, UNDO_INSERT, mode 158 ); 159 } 160 161 void 162 undo_push_delete( 163 undo_stack *undo, txtbuf *text, 164 ledit_range delete_range, ledit_range cursor_range, 165 int start_group, ledit_mode mode) { 166 push_undo( 167 undo, text, delete_range, cursor_range, 168 start_group, UNDO_DELETE, mode 169 ); 170 } 171 172 undo_status 173 ledit_undo(undo_stack *undo, ledit_mode mode, void *callback_data, 174 undo_insert_callback insert_cb, undo_delete_callback delete_cb, 175 size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) { 176 undo_elem *e; 177 /* skip empty elements */ 178 while (undo->cur_valid && undo->stack[undo->cur].text->len == 0) { 179 if (undo->cur == 0) 180 undo->cur_valid = 0; 181 else 182 undo->cur--; 183 } 184 if (!undo->cur_valid) 185 return UNDO_OLDEST_CHANGE; 186 int group = undo->stack[undo->cur].group; 187 int mode_group = undo->stack[undo->cur].mode_group; 188 size_t min_line = SIZE_MAX; 189 int mode_group_same = 0; 190 size_t cur_line = 0; 191 size_t cur_index = 0; 192 while (undo->cur_valid && 193 (undo->stack[undo->cur].group == group || (mode_group_same = 194 ((mode == NORMAL || 195 mode == VISUAL) && 196 undo->stack[undo->cur].mode == INSERT && 197 undo->stack[undo->cur].mode_group == mode_group)))) { 198 e = &undo->stack[undo->cur]; 199 /* if the mode group is the same, we need to update the group, 200 otherwise it can happen that some iterations are performed 201 because the mode group (but not the normal group) is the 202 same, and then the next normal group is also undone because 203 it has the same group id as the original group here */ 204 if (mode_group_same) 205 group = e->group; 206 switch (e->type) { 207 case UNDO_INSERT: 208 /* FIXME: should the paste buffer also be modified? */ 209 /* printf("delete %zu,%zu; %zu,%zu\n", e->op_range.line1, e->op_range.byte1, e->op_range.line2, e->op_range.byte2); */ 210 delete_cb( 211 callback_data, 212 e->op_range.line1, e->op_range.byte1, 213 e->op_range.line2, e->op_range.byte2 214 ); 215 break; 216 case UNDO_DELETE: 217 /* printf("delete %zu,%zu; %zu,%zu\n", e->op_range.line1, e->op_range.byte1, e->op_range.line2, e->op_range.byte2); */ 218 insert_cb( 219 callback_data, 220 e->op_range.line1, e->op_range.byte1, 221 e->text->text, e->text->len 222 ); 223 break; 224 default: 225 /* FIXME: show this as a message */ 226 fprintf(stderr, "Error with undo. This should not happen. Fix the code please.\n"); 227 break; 228 } 229 /* FIXME: make sure this is always sorted already */ 230 if (e->op_range.line1 < min_line) 231 min_line = e->op_range.line1; 232 if (undo->cur == 0) 233 undo->cur_valid = 0; 234 else 235 undo->cur--; 236 cur_line = e->cursor_range.line1; 237 cur_index = e->cursor_range.byte1; 238 } 239 /* these can't be written directly in the while loop because that causes 240 conflicts - when text is inserted/deleted on the current line of the 241 view, the cursor is updated, but if the current line was already changed 242 here before the end of the undo run, it's possible for multiple lines 243 to have a cursor highlight set in the end (at least when working with 244 multiple views) */ 245 *cur_line_ret = cur_line; 246 *cur_index_ret = cur_index; 247 *min_line_ret = min_line; 248 if (mode == NORMAL || mode == VISUAL) 249 undo_change_mode_group(undo); 250 return UNDO_NORMAL; 251 } 252 253 undo_status 254 ledit_redo(undo_stack *undo, ledit_mode mode, void *callback_data, 255 undo_insert_callback insert_cb, undo_delete_callback delete_cb, 256 size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) { 257 undo_elem *e; 258 if (undo->len == 0) 259 return UNDO_NEWEST_CHANGE; 260 /* skip elements where no text is changed */ 261 while (undo->cur < undo->len - 1 && undo->stack[undo->cur + 1].text->len == 0) { 262 undo->cur++; 263 } 264 if (undo->cur_valid && undo->cur >= undo->len - 1) 265 return UNDO_NEWEST_CHANGE; 266 if (!undo->cur_valid) { 267 undo->cur_valid = 1; 268 undo->cur = 0; 269 } else { 270 undo->cur++; 271 } 272 int group = undo->stack[undo->cur].group; 273 int mode_group = undo->stack[undo->cur].mode_group; 274 size_t min_line = SIZE_MAX; 275 int mode_group_same = 0; 276 size_t cur_line = 0; 277 size_t cur_index = 0; 278 while (undo->cur < undo->len && 279 (undo->stack[undo->cur].group == group || (mode_group_same = 280 ((mode == NORMAL || 281 mode == VISUAL) && 282 undo->stack[undo->cur].mode == INSERT && 283 undo->stack[undo->cur].mode_group == mode_group)))) { 284 e = &undo->stack[undo->cur]; 285 if (mode_group_same) 286 group = e->group; 287 switch (e->type) { 288 case UNDO_INSERT: 289 insert_cb( 290 callback_data, 291 e->op_range.line1, e->op_range.byte1, 292 e->text->text, e->text->len 293 ); 294 break; 295 case UNDO_DELETE: 296 delete_cb( 297 callback_data, 298 e->op_range.line1, e->op_range.byte1, 299 e->op_range.line2, e->op_range.byte2 300 ); 301 break; 302 default: 303 fprintf(stderr, "Error with redo. This should not happen. Fix the code please.\n"); 304 break; 305 } 306 if (e->op_range.line1 < min_line) 307 min_line = e->op_range.line1; 308 undo->cur++; 309 cur_line = e->cursor_range.line2; 310 cur_index = e->cursor_range.byte2; 311 } 312 *cur_line_ret = cur_line; 313 *cur_index_ret = cur_index; 314 /* it should theoretically never be 0 anyways, but whatever */ 315 if (undo->cur > 0) 316 undo->cur--; 317 *min_line_ret = min_line; 318 if (mode == NORMAL || mode == VISUAL) 319 undo_change_mode_group(undo); 320 return UNDO_NORMAL; 321 } 322 323 void 324 undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range) { 325 undo_elem *e = peek_undo_elem(undo); 326 if (e != NULL) 327 e->cursor_range = cur_range; 328 } 329 330 char * 331 undo_state_to_str(undo_status s) { 332 switch (s) { 333 case UNDO_NORMAL: 334 return "Performed undo/redo"; 335 case UNDO_OLDEST_CHANGE: 336 return "Already at oldest change"; 337 case UNDO_NEWEST_CHANGE: 338 return "Already at newest change"; 339 default: 340 return "This is a bug. Tell lumidify about it."; 341 } 342 }