2 Commits

Author SHA1 Message Date
0273a5ebb3 add undo docs 2026-04-02 16:03:45 -07:00
0cfb06dff2 junie-undo 2025-11-29 11:55:55 -08:00
11 changed files with 467 additions and 76 deletions

View File

@@ -2,9 +2,9 @@ cmake_minimum_required(VERSION 3.15)
project(ke C) # Specify C language explicitly project(ke C) # Specify C language explicitly
set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD 99)
set(KE_VERSION "2.1.4") set(KE_VERSION "2.1.0")
set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g") set(CMAKE_C_FLAGS "-Wall -Wextra -pedantic -Wshadow -Werror -std=c99 -g -Werror=stringop-truncation")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_DEFAULT_SOURCE -D_XOPEN_SOURCE")
# Optionally enable AddressSanitizer (ASan) # Optionally enable AddressSanitizer (ASan)
@@ -28,6 +28,8 @@ add_executable(ke
core.c core.c
core.h core.h
main.c main.c
undo.h
undo.c
) )
target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}") target_compile_definitions(ke PRIVATE KE_VERSION="ke version ${KE_VERSION}")
install(TARGETS ke RUNTIME DESTINATION bin) install(TARGETS ke RUNTIME DESTINATION bin)
@@ -35,3 +37,4 @@ install(FILES ke.1 TYPE MAN)
install(TARGETS ke RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(TARGETS ke RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
install(FILES ke.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES ke.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)

35
abuf.c
View File

@@ -1,6 +1,7 @@
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdint-gcc.h>
#include "abuf.h" #include "abuf.h"
#include "core.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 void
ab_resize(abuf *buf, size_t cap) 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 void
ab_prependch(abuf *buf, const char c) 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 void
ab_free(abuf *buf) ab_free(abuf *buf)
{ {

7
abuf.h
View File

@@ -19,11 +19,14 @@ typedef struct abuf {
void ab_init(abuf *buf); void ab_init(abuf *buf);
void ab_init_cap(abuf *buf, size_t cap); 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_resize(abuf *buf, size_t cap);
void ab_appendch(abuf *buf, char c); void ab_appendch(abuf *buf, char c);
void ab_append(abuf *buf, const char *s, size_t len); void ab_append(abuf *buf, const char *s, size_t len);
void ab_prependch(abuf *buf, const char c); void ab_append_ab(abuf *buf, abuf *other);
void ab_prepend(abuf *buf, const char *s, const size_t len); 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); void ab_free(abuf *buf);

View File

@@ -272,6 +272,8 @@ buffer_add_empty(void)
buf->mark_set = 0; buf->mark_set = 0;
buf->mark_curx = 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; editor.buffers[editor.bufcount] = buf;
idx = (int)editor.bufcount; idx = (int)editor.bufcount;
@@ -399,6 +401,8 @@ buffer_close_current(void)
b = editor.buffers[closing]; b = editor.buffers[closing];
if (b) { if (b) {
/* free undo tree resources before freeing buffer */
undo_tree_free(&b->undo);
if (b->row) { if (b->row) {
for (size_t i = 0; i < b->nrows; i++) { for (size_t i = 0; i < b->nrows; i++) {
ab_free(&b->row[i]); ab_free(&b->row[i]);

View File

@@ -2,6 +2,7 @@
#define KE_BUFFER_H #define KE_BUFFER_H
#include "abuf.h" #include "abuf.h"
#include "undo.h"
typedef struct buffer { typedef struct buffer {
@@ -14,6 +15,7 @@ typedef struct buffer {
int dirty; int dirty;
int mark_set; int mark_set;
size_t mark_curx, mark_cury; size_t mark_curx, mark_cury;
undo_tree undo;
} buffer; } buffer;
/* Access current buffer and convenient aliases for file-specific fields */ /* Access current buffer and convenient aliases for file-specific fields */

4
ke.1
View File

@@ -65,9 +65,9 @@ Reload the current buffer from disk.
.It C-k s .It C-k s
Save the file, prompting for a filename if needed. Also C-k C-s. Save the file, prompting for a filename if needed. Also C-k C-s.
.It C-k u .It C-k u
Undo changes (not implemented; marking this k-command as taken). Undo changes.
.It C-k U .It C-k U
Redo changes (not implemented; marking this k-command as taken). Redo changes.
.It C-k x .It C-k x
save the file and exit. Also C-k C-x. save the file and exit. Also C-k C-x.
.It C-k y .It C-k y

55
main.c
View File

@@ -1128,6 +1128,15 @@ insertch(const int16_t c)
/* Inserting ends kill ring chaining. */ /* Inserting ends kill ring chaining. */
editor.kill = 0; 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], row_insert_ch(&EROW[ECURY],
ECURX, ECURX,
(int16_t) (c & 0xff)); (int16_t) (c & 0xff));
@@ -1905,9 +1914,19 @@ move_cursor(const int16_t c, const int interactive)
void void
newline(void) newline(void)
{ {
abuf *row = NULL;
size_t rhs_len = 0; size_t rhs_len = 0;
char *tmp = NULL; 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) { if (ECURY >= ENROWS) {
erow_insert(ECURY, "", 0); erow_insert(ECURY, "", 0);
ECURY++; ECURY++;
@@ -1917,30 +1936,24 @@ newline(void)
ECURY++; ECURY++;
ECURX = 0; ECURX = 0;
} else { } else {
/* row = &EROW[ECURY];
* IMPORTANT: Do not keep a pointer to EROW[ECURY] across erow_insert(), rhs_len = row->size - (size_t) ECURX;
* as erow_insert() may realloc the rows array and invalidate it.
*/
rhs_len = EROW[ECURY].size - (size_t)ECURX;
if (rhs_len > 0) { if (rhs_len > 0) {
tmp = malloc(rhs_len); tmp = malloc(rhs_len);
assert(tmp != NULL); assert(tmp != NULL);
memcpy(tmp, &EROW[ECURY].b[ECURX], rhs_len); memcpy(tmp, &row->b[ECURX], rhs_len);
} }
/* Insert the right-hand side as a new row first (may realloc rows). */ row->size = ECURX;
erow_insert(ECURY + 1, tmp ? tmp : "", (int)rhs_len); if (row->cap <= row->size) {
ab_resize(row, row->size + 1);
}
row->b[row->size] = '\0';
erow_insert(ECURY + 1, tmp ? tmp : "", (int) rhs_len);
if (tmp) { if (tmp) {
free(tmp); free(tmp);
} }
/* Now safely shrink the original row (re-fetch by index). */
EROW[ECURY].size = ECURX;
if (EROW[ECURY].cap <= EROW[ECURY].size) {
ab_resize(&EROW[ECURY], EROW[ECURY].size + 1);
}
EROW[ECURY].b[EROW[ECURY].size] = '\0';
ECURY++; ECURY++;
ECURX = 0; ECURX = 0;
} }
@@ -2182,14 +2195,16 @@ process_kcommand(const int16_t c)
case 'u': case 'u':
reps = uarg_get(); reps = uarg_get();
while (reps--) {} while (reps--) {
editor_set_status("Undo not implemented."); editor_undo(CURBUF);
}
break; break;
case 'U': case 'U':
reps = uarg_get(); reps = uarg_get();
while (reps--) {} while (reps--) {
editor_set_status("Redo not implemented."); editor_redo(CURBUF);
}
break; break;
case 'y': case 'y':
reps = uarg_get(); reps = uarg_get();

280
undo.c
View File

@@ -1,6 +1,9 @@
#include <assert.h> #include <assert.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include "abuf.h" #include "abuf.h"
#include "editor.h"
#include "buffer.h"
#include "undo.h" #include "undo.h"
@@ -27,8 +30,6 @@ undo_node_new(undo_kind kind)
void void
undo_node_free(undo_node *node) undo_node_free(undo_node *node)
{ {
undo_node *next = NULL;
if (node == NULL) { if (node == NULL) {
return; return;
} }
@@ -100,15 +101,276 @@ undo_begin(undo_tree *tree, undo_kind kind)
void void
undo_prepend(undo_tree *tree, abuf *buf) 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
void undo_prependch(undo_tree *tree, char c); undo_append(undo_tree *tree, abuf *buf)
void undo_appendch(undo_tree *tree, char c); {
void undo_commit(undo_tree *tree); assert(tree != NULL);
void undo_apply(struct editor *editor); assert(tree->pending != NULL);
void editor_undo(undo_tree *tree);
void editor_redo(undo_tree *tree); 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
View File

@@ -1,13 +1,16 @@
#include <stddef.h> #include <stddef.h>
#include "abuf.h" #include "abuf.h"
#include "editor.h" #include "buffer.h"
#ifndef KE_UNDO_H #ifndef KE_UNDO_H
#define KE_UNDO_H #define KE_UNDO_H
struct buffer;
typedef enum undo_kind { typedef enum undo_kind {
UNDO_INSERT = 1 << 0, UNDO_INSERT = 1 << 0,
UNDO_UNKNOWN = 1 << 1, 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_prependch(undo_tree *tree, char c);
void undo_appendch(undo_tree *tree, char c); void undo_appendch(undo_tree *tree, char c);
void undo_commit(undo_tree *tree); void undo_commit(undo_tree *tree);
void undo_apply(struct editor *editor); void undo_apply(struct buffer *buf, int direction);
void editor_undo(undo_tree *tree); void editor_undo(struct buffer *buf);
void editor_redo(undo_tree *tree); void editor_redo(struct buffer *buf);
#endif #endif

64
undo.md Normal file
View 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`