Compare commits
2 Commits
master
...
undo-syste
| Author | SHA1 | Date | |
|---|---|---|---|
| 0273a5ebb3 | |||
| 0cfb06dff2 |
@@ -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
35
abuf.c
@@ -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
7
abuf.h
@@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
buffer.c
22
buffer.c
@@ -271,7 +271,9 @@ buffer_add_empty(void)
|
|||||||
buf->dirty = 0;
|
buf->dirty = 0;
|
||||||
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;
|
||||||
@@ -397,14 +399,16 @@ buffer_close_current(void)
|
|||||||
buffer_switch(nb);
|
buffer_switch(nb);
|
||||||
}
|
}
|
||||||
|
|
||||||
b = editor.buffers[closing];
|
b = editor.buffers[closing];
|
||||||
if (b) {
|
if (b) {
|
||||||
if (b->row) {
|
/* free undo tree resources before freeing buffer */
|
||||||
for (size_t i = 0; i < b->nrows; i++) {
|
undo_tree_free(&b->undo);
|
||||||
ab_free(&b->row[i]);
|
if (b->row) {
|
||||||
}
|
for (size_t i = 0; i < b->nrows; i++) {
|
||||||
free(b->row);
|
ab_free(&b->row[i]);
|
||||||
}
|
}
|
||||||
|
free(b->row);
|
||||||
|
}
|
||||||
|
|
||||||
if (b->filename) {
|
if (b->filename) {
|
||||||
free(b->filename);
|
free(b->filename);
|
||||||
|
|||||||
20
buffer.h
20
buffer.h
@@ -2,18 +2,20 @@
|
|||||||
#define KE_BUFFER_H
|
#define KE_BUFFER_H
|
||||||
|
|
||||||
#include "abuf.h"
|
#include "abuf.h"
|
||||||
|
#include "undo.h"
|
||||||
|
|
||||||
|
|
||||||
typedef struct buffer {
|
typedef struct buffer {
|
||||||
size_t curx, cury;
|
size_t curx, cury;
|
||||||
size_t rx;
|
size_t rx;
|
||||||
size_t nrows;
|
size_t nrows;
|
||||||
size_t rowoffs, coloffs;
|
size_t rowoffs, coloffs;
|
||||||
abuf *row;
|
abuf *row;
|
||||||
char *filename;
|
char *filename;
|
||||||
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
4
ke.1
@@ -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
|
||||||
|
|||||||
91
main.c
91
main.c
@@ -1121,18 +1121,27 @@ insertch(const int16_t c)
|
|||||||
* a row; it can just figure out where the cursor is
|
* a row; it can just figure out where the cursor is
|
||||||
* at and what to do.
|
* at and what to do.
|
||||||
*/
|
*/
|
||||||
if (ECURY == ENROWS) {
|
if (ECURY == ENROWS) {
|
||||||
erow_insert(ENROWS, "", 0);
|
erow_insert(ENROWS, "", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Inserting ends kill ring chaining. */
|
/* Inserting ends kill ring chaining. */
|
||||||
editor.kill = 0;
|
editor.kill = 0;
|
||||||
|
|
||||||
row_insert_ch(&EROW[ECURY],
|
/* Begin/append undo record for insert operations */
|
||||||
ECURX,
|
undo_tree *utree = &CURBUF->undo;
|
||||||
(int16_t) (c & 0xff));
|
undo_begin(utree, UNDO_INSERT);
|
||||||
ECURX++;
|
if (utree->pending && utree->pending->text.size == 0) {
|
||||||
EDIRTY++;
|
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,47 +1914,51 @@ move_cursor(const int16_t c, const int interactive)
|
|||||||
void
|
void
|
||||||
newline(void)
|
newline(void)
|
||||||
{
|
{
|
||||||
size_t rhs_len = 0;
|
abuf *row = NULL;
|
||||||
char *tmp = NULL;
|
size_t rhs_len = 0;
|
||||||
|
char *tmp = NULL;
|
||||||
|
|
||||||
if (ECURY >= ENROWS) {
|
/* Begin/append undo record for insert operations (newline as '\n') */
|
||||||
erow_insert(ECURY, "", 0);
|
undo_tree *utree = &CURBUF->undo;
|
||||||
ECURY++;
|
undo_begin(utree, UNDO_INSERT);
|
||||||
ECURX = 0;
|
if (utree->pending && utree->pending->text.size == 0) {
|
||||||
} else if (ECURX == 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);
|
erow_insert(ECURY, "", 0);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.kill = 0; /* BREAK THE KILL CHAIN \m/ */
|
editor.kill = 0; /* BREAK THE KILL CHAIN \m/ */
|
||||||
EDIRTY++;
|
EDIRTY++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
280
undo.c
@@ -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
11
undo.h
@@ -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
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