junie-undo

This commit is contained in:
2025-11-29 11:55:55 -08:00
parent a574df2ab7
commit 0cfb06dff2
9 changed files with 388 additions and 57 deletions

280
undo.c
View File

@@ -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);
}