diff --git a/CMakeLists.txt b/CMakeLists.txt index 1430108..ffd0c0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,8 @@ add_executable(ke core.c core.h main.c + undo.h + undo.c ) target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}") install(TARGETS ke RUNTIME DESTINATION bin) diff --git a/abuf.c b/abuf.c index a1f0dd5..3732b25 100644 --- a/abuf.c +++ b/abuf.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "abuf.h" #include "core.h" @@ -36,6 +37,16 @@ ab_init_cap(abuf *buf, const size_t cap) } +void +ab_init_str(abuf *buf, const char *s) +{ + size_t len = kstrnlen(s, SIZE_MAX); + + ab_init_cap(buf, len); + ab_append(buf, s, len); +} + + void ab_resize(abuf *buf, size_t cap) { @@ -71,6 +82,18 @@ ab_append(abuf *buf, const char *s, size_t len) } +void +ab_append_ab(abuf *buf, abuf *other) +{ + assert(buf != NULL && other != NULL); + if (other->size == 0) { + return; + } + + ab_append(buf, other->b, other->size); +} + + void ab_prependch(abuf *buf, const char c) { @@ -97,6 +120,18 @@ ab_prepend(abuf *buf, const char *s, const size_t len) } +void +ab_prepend_ab(abuf *buf, abuf *other) +{ + assert(buf != NULL && other != NULL); + if (other->size == 0) { + return; + } + + ab_prepend(buf, other->b, other->size); +} + + void ab_free(abuf *buf) { diff --git a/abuf.h b/abuf.h index 90d87f3..b793c92 100644 --- a/abuf.h +++ b/abuf.h @@ -19,11 +19,14 @@ typedef struct abuf { void ab_init(abuf *buf); void ab_init_cap(abuf *buf, size_t cap); +void ab_init_str(abuf *buf, const char *s); void ab_resize(abuf *buf, size_t cap); void ab_appendch(abuf *buf, char c); void ab_append(abuf *buf, const char *s, size_t len); -void ab_prependch(abuf *buf, const char c); -void ab_prepend(abuf *buf, const char *s, const size_t len); +void ab_append_ab(abuf *buf, abuf *other); +void ab_prependch(abuf *buf, char c); +void ab_prepend(abuf *buf, const char *s, size_t len); +void ab_prepend_ab(abuf *buf, abuf *other); void ab_free(abuf *buf); diff --git a/buffer.c b/buffer.c index 7f17f14..e5226ba 100644 --- a/buffer.c +++ b/buffer.c @@ -271,7 +271,9 @@ buffer_add_empty(void) buf->dirty = 0; buf->mark_set = 0; buf->mark_curx = 0; - buf->mark_cury = 0; + buf->mark_cury = 0; + /* initialize undo tree for this buffer */ + undo_tree_init(&buf->undo); editor.buffers[editor.bufcount] = buf; idx = (int)editor.bufcount; @@ -397,14 +399,16 @@ buffer_close_current(void) buffer_switch(nb); } - b = editor.buffers[closing]; - if (b) { - if (b->row) { - for (size_t i = 0; i < b->nrows; i++) { - ab_free(&b->row[i]); - } - free(b->row); - } + b = editor.buffers[closing]; + if (b) { + /* free undo tree resources before freeing buffer */ + undo_tree_free(&b->undo); + if (b->row) { + for (size_t i = 0; i < b->nrows; i++) { + ab_free(&b->row[i]); + } + free(b->row); + } if (b->filename) { free(b->filename); diff --git a/buffer.h b/buffer.h index 1289801..d6f57d3 100644 --- a/buffer.h +++ b/buffer.h @@ -2,18 +2,20 @@ #define KE_BUFFER_H #include "abuf.h" +#include "undo.h" typedef struct buffer { - size_t curx, cury; - size_t rx; - size_t nrows; - size_t rowoffs, coloffs; - abuf *row; - char *filename; - int dirty; - int mark_set; - size_t mark_curx, mark_cury; + size_t curx, cury; + size_t rx; + size_t nrows; + size_t rowoffs, coloffs; + abuf *row; + char *filename; + int dirty; + int mark_set; + size_t mark_curx, mark_cury; + undo_tree undo; } buffer; /* Access current buffer and convenient aliases for file-specific fields */ diff --git a/ke.1 b/ke.1 index 38774ac..183bd76 100644 --- a/ke.1 +++ b/ke.1 @@ -65,9 +65,9 @@ Reload the current buffer from disk. .It C-k s Save the file, prompting for a filename if needed. Also C-k C-s. .It C-k u -Undo changes (not implemented; marking this k-command as taken). +Undo changes. .It C-k U -Redo changes (not implemented; marking this k-command as taken). +Redo changes. .It C-k x save the file and exit. Also C-k C-x. .It C-k y diff --git a/main.c b/main.c index f101cf1..1f1af8c 100644 --- a/main.c +++ b/main.c @@ -1121,18 +1121,27 @@ insertch(const int16_t c) * a row; it can just figure out where the cursor is * at and what to do. */ - if (ECURY == ENROWS) { - erow_insert(ENROWS, "", 0); - } + if (ECURY == ENROWS) { + erow_insert(ENROWS, "", 0); + } - /* Inserting ends kill ring chaining. */ - editor.kill = 0; + /* Inserting ends kill ring chaining. */ + editor.kill = 0; - row_insert_ch(&EROW[ECURY], - ECURX, - (int16_t) (c & 0xff)); - ECURX++; - EDIRTY++; + /* Begin/append undo record for insert operations */ + undo_tree *utree = &CURBUF->undo; + undo_begin(utree, UNDO_INSERT); + if (utree->pending && utree->pending->text.size == 0) { + utree->pending->row = ECURY; + utree->pending->col = ECURX; + } + undo_appendch(utree, (char)(c & 0xff)); + + row_insert_ch(&EROW[ECURY], + ECURX, + (int16_t) (c & 0xff)); + ECURX++; + EDIRTY++; } @@ -1905,15 +1914,24 @@ move_cursor(const int16_t c, const int interactive) void newline(void) { - abuf *row = NULL; - size_t rhs_len = 0; - char *tmp = NULL; + abuf *row = NULL; + size_t rhs_len = 0; + char *tmp = NULL; - if (ECURY >= ENROWS) { - erow_insert(ECURY, "", 0); - ECURY++; - ECURX = 0; - } else if (ECURX == 0) { + /* Begin/append undo record for insert operations (newline as '\n') */ + undo_tree *utree = &CURBUF->undo; + undo_begin(utree, UNDO_INSERT); + if (utree->pending && utree->pending->text.size == 0) { + utree->pending->row = ECURY; + utree->pending->col = ECURX; + } + undo_appendch(utree, '\n'); + + if (ECURY >= ENROWS) { + erow_insert(ECURY, "", 0); + ECURY++; + ECURX = 0; + } else if (ECURX == 0) { erow_insert(ECURY, "", 0); ECURY++; ECURX = 0; @@ -2177,14 +2195,16 @@ process_kcommand(const int16_t c) case 'u': reps = uarg_get(); - while (reps--) {} - editor_set_status("Undo not implemented."); + while (reps--) { + editor_undo(CURBUF); + } break; case 'U': reps = uarg_get(); - while (reps--) {} - editor_set_status("Redo not implemented."); + while (reps--) { + editor_redo(CURBUF); + } break; case 'y': reps = uarg_get(); diff --git a/undo.c b/undo.c index 50551c8..5865b7b 100644 --- a/undo.c +++ b/undo.c @@ -1,6 +1,9 @@ #include #include +#include #include "abuf.h" +#include "editor.h" +#include "buffer.h" #include "undo.h" @@ -27,8 +30,6 @@ undo_node_new(undo_kind kind) void undo_node_free(undo_node *node) { - undo_node *next = NULL; - if (node == NULL) { return; } @@ -100,15 +101,276 @@ undo_begin(undo_tree *tree, undo_kind kind) void undo_prepend(undo_tree *tree, abuf *buf) { + assert(tree != NULL); + assert(tree->pending != NULL); + ab_prepend_ab(&tree->pending->text, buf); } -void undo_append(undo_tree *tree, abuf *buf); -void undo_prependch(undo_tree *tree, char c); -void undo_appendch(undo_tree *tree, char c); -void undo_commit(undo_tree *tree); -void undo_apply(struct editor *editor); -void editor_undo(undo_tree *tree); -void editor_redo(undo_tree *tree); +void +undo_append(undo_tree *tree, abuf *buf) +{ + assert(tree != NULL); + assert(tree->pending != NULL); + + ab_append_ab(&tree->pending->text, buf); +} + + +void +undo_prependch(undo_tree *tree, char c) +{ + assert(tree != NULL); + assert(tree->pending != NULL); + + ab_prependch(&tree->pending->text, c); +} + + +void +undo_appendch(undo_tree *tree, char c) +{ + assert(tree != NULL); + assert(tree->pending != NULL); + + ab_appendch(&tree->pending->text, c); +} + + +void +undo_commit(undo_tree *tree) +{ + assert(tree != NULL); + + if (tree->pending == NULL) { + return; + } + + if (tree->root == NULL) { + tree->root = tree->pending; + tree->current = tree->pending; + tree->pending = NULL; + return; + } + + undo_node_free_all(tree->current->next); + tree->current->next = tree->pending; + tree->pending->parent = tree->current; + tree->current = tree->pending; + tree->pending = NULL; +} + + +/* --- Helper functions for applying undo/redo operations --- */ +static void +row_insert_at(buffer *b, size_t at, const char *s, size_t len) +{ + abuf *newrows = realloc(b->row, sizeof(abuf) * (b->nrows + 1)); + assert(newrows != NULL); + b->row = newrows; + + if (at < b->nrows) { + memmove(&b->row[at + 1], &b->row[at], sizeof(abuf) * (b->nrows - at)); + } + ab_init(&b->row[at]); + if (len > 0) { + ab_append(&b->row[at], s, len); + } + b->nrows++; + b->dirty++; +} + +static void +ensure_row_exists(buffer *b, size_t r) +{ + while (r >= b->nrows) { + row_insert_at(b, b->nrows, "", 0); + } +} + +static void +row_insert_ch_at(abuf *row, size_t col, char ch) +{ + if (col > row->size) { + col = row->size; + } + ab_resize(row, row->size + 2); + memmove(&row->b[col + 1], &row->b[col], row->size - col + 1); + row->b[col] = ch; + row->size++; + row->b[row->size] = '\0'; +} + +static void +row_delete_ch_at(abuf *row, size_t col) +{ + if (col >= row->size) { + return; + } + memmove(&row->b[col], &row->b[col + 1], row->size - col); + row->size--; + row->b[row->size] = '\0'; +} + +static void +split_row_at(buffer *b, size_t r, size_t c) +{ + ensure_row_exists(b, r); + abuf *row = &b->row[r]; + if (c > row->size) c = row->size; + size_t rhs_len = row->size - c; + char *rhs = NULL; + if (rhs_len > 0) { + rhs = malloc(rhs_len); + assert(rhs != NULL); + memcpy(rhs, &row->b[c], rhs_len); + } + row->size = c; + if (row->cap <= row->size) { + ab_resize(row, row->size + 1); + } + row->b[row->size] = '\0'; + + row_insert_at(b, r + 1, rhs ? rhs : "", rhs_len); + if (rhs) free(rhs); + b->dirty++; +} + +static void +join_with_next_row(buffer *b, size_t r, size_t c) +{ + if (r >= b->nrows) return; + abuf *row = &b->row[r]; + if (c > row->size) c = row->size; + if (r + 1 >= b->nrows) return; + abuf *next = &b->row[r + 1]; + + /* Make room in current row at end if needed (we append next->b after position c). */ + size_t tail_len = row->size - c; + size_t add_len = next->size; + ab_resize(row, row->size + add_len + 1); + /* Move tail to make room for next content */ + memmove(&row->b[c + add_len], &row->b[c], tail_len); + memcpy(&row->b[c], next->b, add_len); + row->size += add_len; + row->b[row->size] = '\0'; + + /* Delete next row */ + ab_free(next); + if (b->nrows - (r + 2) > 0) { + memmove(&b->row[r + 1], &b->row[r + 2], sizeof(abuf) * (b->nrows - (r + 2))); + } + b->nrows--; + b->dirty++; +} + +static void +buffer_insert_text(buffer *b, size_t row, size_t col, const char *s, size_t len) +{ + ensure_row_exists(b, row); + size_t r = row; + size_t c = col; + if (c > b->row[r].size) c = b->row[r].size; + for (size_t i = 0; i < len; i++) { + char ch = s[i]; + if (ch == '\n') { + split_row_at(b, r, c); + r++; + c = 0; + } else { + row_insert_ch_at(&b->row[r], c, ch); + c++; + } + b->dirty++; + } +} + +static void +buffer_delete_text(buffer *b, size_t row, size_t col, const char *s, size_t len) +{ + if (row >= b->nrows) return; + size_t r = row; + size_t c = col; + if (c > b->row[r].size) c = b->row[r].size; + for (size_t i = 0; i < len; i++) { + char ch = s[i]; + if (ch == '\n') { + /* delete newline at (r,c): join row r with next */ + join_with_next_row(b, r, c); + } else { + row_delete_ch_at(&b->row[r], c); + /* c stays the same because character at c is removed */ + } + b->dirty++; + if (r >= b->nrows) break; /* safety */ + if (c > b->row[r].size) c = b->row[r].size; + } +} + +void +undo_apply(struct buffer *buf, int direction) { + undo_tree *tree = &buf->undo; + undo_node *node = NULL; + undo_commit(tree); + + if (tree->root == NULL) { + return; + } + + if (direction < 0) { + /* UNDO: apply inverse of current node, then move back */ + node = tree->current; + if (node == NULL) { + return; + } + switch (node->kind) { + /* support insert first */ + case UNDO_INSERT: + buffer_delete_text(buf, node->row, node->col, + node->text.b, node->text.size); + break; + default: + /* unknown type: do nothing */ + break; + } + + tree->current = node->parent; /* move back in history */ + } else if (direction > 0) { + /* REDO: move forward then apply node */ + undo_node *next = (tree->current == NULL) + ? tree->root + : tree->current->next; + if (next == NULL) { + return; /* nothing to redo */ + } + + switch (next->kind) { + /* support insert first */ + case UNDO_INSERT: + buffer_insert_text(buf, next->row, next->col, + next->text.b, next->text.size); + break; + default: + /* unknown type: do nothing */ + break; + } + + tree->current = next; /* move forward in history */ + } +} + + +void +editor_undo(struct buffer *buf) +{ + undo_apply(buf, -1); +} + + +void +editor_redo(struct buffer *buf) +{ + undo_apply(buf, 1); +} diff --git a/undo.h b/undo.h index f33e5f5..9d2f056 100644 --- a/undo.h +++ b/undo.h @@ -1,13 +1,16 @@ #include #include "abuf.h" -#include "editor.h" +#include "buffer.h" #ifndef KE_UNDO_H #define KE_UNDO_H +struct buffer; + + typedef enum undo_kind { UNDO_INSERT = 1 << 0, UNDO_UNKNOWN = 1 << 1, @@ -42,9 +45,9 @@ void undo_append(undo_tree *tree, abuf *buf); void undo_prependch(undo_tree *tree, char c); void undo_appendch(undo_tree *tree, char c); void undo_commit(undo_tree *tree); -void undo_apply(struct editor *editor); -void editor_undo(undo_tree *tree); -void editor_redo(undo_tree *tree); +void undo_apply(struct buffer *buf, int direction); +void editor_undo(struct buffer *buf); +void editor_redo(struct buffer *buf); #endif