junie-undo
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user