377 lines
7.2 KiB
C
377 lines
7.2 KiB
C
#include <assert.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "abuf.h"
|
|
#include "editor.h"
|
|
#include "buffer.h"
|
|
#include "undo.h"
|
|
|
|
|
|
undo_node *
|
|
undo_node_new(undo_kind kind)
|
|
{
|
|
undo_node *node = NULL;
|
|
|
|
node = (undo_node *)malloc(sizeof(undo_node));
|
|
assert(node != NULL);
|
|
|
|
node->kind = kind;
|
|
node->row = node->col = 0;
|
|
|
|
ab_init(&node->text);
|
|
|
|
node->next = NULL;
|
|
node->parent = NULL;
|
|
|
|
return node;
|
|
}
|
|
|
|
|
|
void
|
|
undo_node_free(undo_node *node)
|
|
{
|
|
if (node == NULL) {
|
|
return;
|
|
}
|
|
|
|
ab_free(&node->text);
|
|
}
|
|
|
|
|
|
void
|
|
undo_node_free_all(undo_node *node)
|
|
{
|
|
undo_node *next = NULL;
|
|
|
|
if (node == NULL) {
|
|
return;
|
|
}
|
|
|
|
while (node != NULL) {
|
|
next = node->next;
|
|
undo_node_free(node);
|
|
free(node);
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
undo_tree_init(undo_tree *tree)
|
|
{
|
|
assert(tree != NULL);
|
|
|
|
tree->root = NULL;
|
|
tree->current = NULL;
|
|
tree->pending = NULL;
|
|
}
|
|
|
|
|
|
void
|
|
undo_tree_free(undo_tree *tree)
|
|
{
|
|
assert(tree != NULL);
|
|
|
|
undo_node_free(tree->pending);
|
|
undo_node_free_all(tree->root);
|
|
undo_tree_init(tree);
|
|
}
|
|
|
|
|
|
void
|
|
undo_begin(undo_tree *tree, undo_kind kind)
|
|
{
|
|
undo_node *pending = NULL;
|
|
|
|
if (tree->pending != NULL) {
|
|
if (tree->pending->kind == kind) {
|
|
/* don't initiate a new undo sequence if it's the same kind */
|
|
return;
|
|
}
|
|
undo_commit(tree);
|
|
}
|
|
|
|
pending = undo_node_new(kind);
|
|
assert(pending != NULL);
|
|
|
|
tree->pending = pending;
|
|
}
|
|
|
|
|
|
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)
|
|
{
|
|
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);
|
|
}
|
|
|