Compare commits
2 Commits
v2.1.4
...
undo-syste
| Author | SHA1 | Date | |
|---|---|---|---|
| 0273a5ebb3 | |||
| 0cfb06dff2 |
@@ -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)
|
||||
|
||||
35
abuf.c
35
abuf.c
@@ -1,6 +1,7 @@
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint-gcc.h>
|
||||
|
||||
#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)
|
||||
{
|
||||
|
||||
7
abuf.h
7
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);
|
||||
|
||||
|
||||
|
||||
4
buffer.c
4
buffer.c
@@ -272,6 +272,8 @@ buffer_add_empty(void)
|
||||
buf->mark_set = 0;
|
||||
buf->mark_curx = 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;
|
||||
@@ -399,6 +401,8 @@ buffer_close_current(void)
|
||||
|
||||
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]);
|
||||
|
||||
2
buffer.h
2
buffer.h
@@ -2,6 +2,7 @@
|
||||
#define KE_BUFFER_H
|
||||
|
||||
#include "abuf.h"
|
||||
#include "undo.h"
|
||||
|
||||
|
||||
typedef struct buffer {
|
||||
@@ -14,6 +15,7 @@ typedef struct buffer {
|
||||
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 */
|
||||
|
||||
4
ke.1
4
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
|
||||
|
||||
28
main.c
28
main.c
@@ -1128,6 +1128,15 @@ insertch(const int16_t c)
|
||||
/* Inserting ends kill ring chaining. */
|
||||
editor.kill = 0;
|
||||
|
||||
/* 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));
|
||||
@@ -1909,6 +1918,15 @@ newline(void)
|
||||
size_t rhs_len = 0;
|
||||
char *tmp = NULL;
|
||||
|
||||
/* 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++;
|
||||
@@ -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();
|
||||
|
||||
280
undo.c
280
undo.c
@@ -1,6 +1,9 @@
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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);
|
||||
}
|
||||
|
||||
|
||||
11
undo.h
11
undo.h
@@ -1,13 +1,16 @@
|
||||
#include <stddef.h>
|
||||
|
||||
#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
|
||||
|
||||
64
undo.md
Normal file
64
undo.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Towards an Undo System
|
||||
|
||||
date
|
||||
2025-11-27 20:14
|
||||
|
||||
tags
|
||||
ked, hacks, text-editors
|
||||
|
||||
I've been thinking about building an undo system for
|
||||
[ke](https://git.wntrmute.dev/kyle/ke). The first pass is going to be a
|
||||
linear system. Let's start with the basic definitions for an undo
|
||||
system:
|
||||
|
||||
``` c
|
||||
typedef enum undo_kind {
|
||||
UNDO_INSERT = 1 << 0,
|
||||
UNDO_UNKNOWN = 1 << 1,
|
||||
/* more types to follow */
|
||||
} undo_kind;
|
||||
|
||||
|
||||
typedef struct undo_node {
|
||||
undo_kind op;
|
||||
size_t row, col;
|
||||
abuf text;
|
||||
struct undo_node *next;
|
||||
struct undo_node *parent;
|
||||
} undo_node;
|
||||
|
||||
|
||||
typedef struct undo_tree {
|
||||
/* the start of the undo sequence */
|
||||
undo_node *root;
|
||||
/* where we are currently at */
|
||||
undo_node *current;
|
||||
/* the current undo operations being built */
|
||||
undo_node *pending;
|
||||
} undo_tree;
|
||||
```
|
||||
|
||||
The root is anchored at the last time the file was saved; when saving,
|
||||
the tree is freed. Current points to the end of the history, and pending
|
||||
is the sequence being built.
|
||||
|
||||
The lifecycle looks something like:
|
||||
|
||||
- `undo_tree_new` and `undo_tree_free` --- called in `init_editor` and
|
||||
`deathknell`, respectively. The tree is initialized with all nodes set
|
||||
to `NULL`.
|
||||
- Once the user starts doing undoable things, `undo_begin(undo_kind)`
|
||||
gets called, calling `undo_node_new(undo_kind)` if needed to set up
|
||||
`tree->pending`. It may need to call `undo_commit` (below) if needed.
|
||||
- Until an `undo_commit` is called, some form of `undo_append` or
|
||||
`undo_prepend` is called.
|
||||
- Finally, at some point `undo_commit` is called. This needs to do a few
|
||||
things:
|
||||
1. If `tree->current->next` is not `NULL`, it must be freed.
|
||||
2. Set `tree->current->next` as pending, set `tree->current` to
|
||||
`tree->current->next`.
|
||||
3. Set `tree->pending` to `NULL`.
|
||||
|
||||
Once the e
|
||||
|
||||
- `undo_node_new(undo_kind)` and `undo_node_free`
|
||||
Reference in New Issue
Block a user