Working on undo system.
This commit is contained in:
17
TODO
17
TODO
@@ -1,15 +1,18 @@
|
||||
[X] goto-line
|
||||
[X] text-corruption bug
|
||||
[ ] alt-modifiers
|
||||
[ ] refresh-screen
|
||||
[ ] functions -> keymapping?
|
||||
[x] refresh-screen
|
||||
[ ] functions -> keymapping? (what did this even mean)
|
||||
[X] rendering: need to skip over control characters like we do with tabs
|
||||
[X] control-d -> delete
|
||||
[ ] control-g -> exit buf prompt
|
||||
[ ] load-file prompt on dirty buffer
|
||||
[x] control-g -> exit buf prompt
|
||||
[x] load-file prompt on dirty buffer
|
||||
[ ] multiple files
|
||||
[x] utf-8
|
||||
[x] kill ring
|
||||
[x] mark/region
|
||||
|
||||
What would it take for ke to be your daily drives?
|
||||
+ C-u
|
||||
+ Alt nav (backspace, delete, f, b, etc)
|
||||
|
||||
[ ] C-u (repeat actions)
|
||||
[x] Alt nav (backspace, delete, f, b, etc)
|
||||
[ ] undo tree (C-k u)
|
||||
|
||||
12
ke.1
12
ke.1
@@ -26,10 +26,14 @@ k-command mode can be exited with ESC or C-g.
|
||||
.Bl -tag -width xxxxxxxxxxxx -offset indent
|
||||
.It C-k BACKSPACE
|
||||
Delete from the cursor to the beginning of the line.
|
||||
.It C-k c
|
||||
Clear (flush) the kill ring.
|
||||
.It C-k SPACE
|
||||
Toggle the mark.
|
||||
.It C-k -
|
||||
If the mark is set, unindent the region.
|
||||
.It C-k =
|
||||
If the mark is set, indent the region.
|
||||
.It C-k c
|
||||
Clear (flush) the kill ring.
|
||||
.It C-k d
|
||||
Delete from the cursor to the end of the line.
|
||||
.It C-k C-d
|
||||
@@ -54,7 +58,9 @@ Immediately exit the editor.
|
||||
.It C-k C-r
|
||||
Reload the current buffer from disk.
|
||||
.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
|
||||
Undo changes. Not implemented yet, placeholder.
|
||||
.It C-k x
|
||||
save the file and exit. Also C-k C-x.
|
||||
.It C-k y
|
||||
|
||||
559
main.c
559
main.c
@@ -79,8 +79,8 @@ static FILE* debug_log = NULL;
|
||||
/* append buffer */
|
||||
struct abuf {
|
||||
char *b;
|
||||
int len;
|
||||
int cap;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
};
|
||||
|
||||
#define ABUF_INIT {NULL, 0, 0}
|
||||
@@ -98,6 +98,35 @@ struct erow {
|
||||
};
|
||||
|
||||
|
||||
typedef enum undo_flag {
|
||||
UNDO_INSERT = 1 << 0, /* insertch */
|
||||
UNDO_DELETE = 1 << 1, /* deletech */
|
||||
UNDO_PASTE = 1 << 2, /* yank */
|
||||
UNDO_NEWLINE = 1 << 3, /* newline duh */
|
||||
UNDO_DELETE_ROW = 1 << 4, /* delete_row duh */
|
||||
UNDO_INDENT = 1 << 5,
|
||||
UNDO_UNINDENT = 1 << 6,
|
||||
UNDO_KILL_REGION = 1 << 7
|
||||
} undo_flag_t;
|
||||
|
||||
|
||||
typedef struct undo_node {
|
||||
undo_flag_t type;
|
||||
int row, col;
|
||||
struct abuf text;
|
||||
struct undo_node *next;
|
||||
struct undo_node *child;
|
||||
} undo_node_t;
|
||||
|
||||
|
||||
typedef struct undo_tree {
|
||||
undo_node_t *root;
|
||||
undo_node_t *current;
|
||||
undo_node_t *saved;
|
||||
undo_node_t *pending;
|
||||
} undo_tree_t;
|
||||
|
||||
|
||||
/*
|
||||
* editor is the global editor state; it should be broken out
|
||||
* to buffers and screen state, probably.
|
||||
@@ -121,6 +150,7 @@ struct editor_t {
|
||||
int mark_set;
|
||||
int mark_curx, mark_cury;
|
||||
time_t msgtm;
|
||||
undo_tree_t *undo;
|
||||
} editor = {
|
||||
.cols = 0,
|
||||
.rows = 0,
|
||||
@@ -151,9 +181,11 @@ int next_power_of_2(int n);
|
||||
int cap_growth(int cap, int sz);
|
||||
size_t kstrnlen(const char *buf, const size_t max);
|
||||
void ab_init(struct abuf *buf);
|
||||
void ab_append(struct abuf *buf, const char *s, int len);
|
||||
void ab_appendch(struct abuf *buf, char c);
|
||||
void ab_append(struct abuf *buf, const char *s, size_t len);
|
||||
void ab_free(struct abuf *buf);
|
||||
char nibble_to_hex(char c);
|
||||
void swap_int(int *a, int *b);
|
||||
|
||||
/* editor rows */
|
||||
int erow_render_to_cursor(struct erow *row, int cx);
|
||||
@@ -163,7 +195,69 @@ void erow_update(struct erow *row);
|
||||
void erow_insert(int at, char *s, int len);
|
||||
void erow_free(struct erow *row);
|
||||
|
||||
/* kill ring, marking, etc */
|
||||
/*
|
||||
* undo ops
|
||||
*
|
||||
* notes:
|
||||
* + undo_node_free destroys an entire timeline, including children and next.
|
||||
* + undo_node_free_branch only discards next.
|
||||
* + undo_discard_redo_branches kills child and next.
|
||||
*
|
||||
* Basic invariants of the undo system:
|
||||
* + root->parent == NULL
|
||||
* + root->current is reachable from root via repeated child walk
|
||||
* + saved is NULL or reachable the same way
|
||||
* + pending is either NULL or a brand-new node not yet linked
|
||||
* + when we commit, pending becomes current->child and current moves forward
|
||||
* + when we undo, current = current->parent
|
||||
* + when we type after undo, we free current->child (redo branch) first
|
||||
*
|
||||
* Or, visually,
|
||||
*
|
||||
* root ──> N1 ──> N2 ──> N3 ──> N4* ──> N5 ──> N6 (main timeline)
|
||||
* ^ ^ ^
|
||||
* | | |
|
||||
* saved pending current
|
||||
*
|
||||
* + root : first edit ever, never has a parent
|
||||
* + current : where we are right now in history
|
||||
* + saved : points to the node that matches the on-disk file
|
||||
* + pending : temporary node being built (committed → becomes current->child)
|
||||
*
|
||||
* If I do a double undo then type something:
|
||||
*
|
||||
* root ──> N1 ──> N2 ──> N3* ──> N4 ──> N5 (old N4→N5→N6 discarded)
|
||||
* ^ ^
|
||||
* | |
|
||||
* current pending (new edit)
|
||||
* |
|
||||
* saved
|
||||
*
|
||||
* All four pointers point into the same tree → and should only be memory
|
||||
* managed via the root node.
|
||||
*/
|
||||
undo_node_t *undo_node_new(undo_flag_t type);
|
||||
void undo_node_free(undo_node_t *node);
|
||||
void undo_node_free_branch(undo_node_t *node);
|
||||
undo_tree_t *undo_tree_new(void);
|
||||
void undo_tree_free(undo_tree_t *ut);
|
||||
int undo_continue_pending(undo_flag_t type);
|
||||
void undo_begin(undo_flag_t type);
|
||||
void undo_append_char(char c);
|
||||
void undo_append(const char *data, size_t len);
|
||||
void undo_commit(void);
|
||||
void undo_discard_redo_branches(struct undo_node *from);
|
||||
undo_node_t *undo_parent_of(undo_node_t *node);
|
||||
void undo_apply(const undo_node_t *node, int direction);
|
||||
// void editor_undo(void);
|
||||
// void editor_redo(void);
|
||||
// void undo_mark_saved(void);
|
||||
// int undo_depth(const undo_tree_t *t);
|
||||
// int undo_can_undo(const undo_tree_t *t);
|
||||
// int undo_can_redo(const undo_tree_t *t);
|
||||
// void undo_tree_debug_dump(const undo_tree_t *t);
|
||||
|
||||
/* kill ring, marking, etc... */
|
||||
void killring_flush(void);
|
||||
void killring_yank(void);
|
||||
void killring_start_with_char(unsigned char ch);
|
||||
@@ -347,6 +441,13 @@ init_editor(void)
|
||||
editor.dirty = 0;
|
||||
editor.mark_set = 0;
|
||||
editor.mark_cury = editor.mark_curx = 0;
|
||||
|
||||
if (editor.undo != NULL) {
|
||||
undo_tree_free(editor.undo);
|
||||
editor.undo = NULL;
|
||||
}
|
||||
|
||||
editor.undo = undo_tree_new();
|
||||
}
|
||||
|
||||
|
||||
@@ -385,10 +486,17 @@ ab_init(struct abuf *buf)
|
||||
|
||||
|
||||
void
|
||||
ab_append(struct abuf *buf, const char *s, int len)
|
||||
ab_appendch(struct abuf *buf, char c)
|
||||
{
|
||||
char *nc = buf->b;
|
||||
int sz = buf->len + len;
|
||||
ab_append(buf, &c, 1);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
ab_append(struct abuf *buf, const char *s, size_t len)
|
||||
{
|
||||
char *nc = buf->b;
|
||||
size_t sz = buf->len + len;
|
||||
|
||||
if (sz >= buf->cap) {
|
||||
while (sz > buf->cap) {
|
||||
@@ -404,7 +512,7 @@ ab_append(struct abuf *buf, const char *s, int len)
|
||||
|
||||
memcpy(&nc[buf->len], s, len);
|
||||
buf->b = nc;
|
||||
buf->len += len; /* DANGER: overflow */
|
||||
buf->len += len;
|
||||
}
|
||||
|
||||
|
||||
@@ -656,6 +764,366 @@ erow_free(struct erow *row)
|
||||
}
|
||||
|
||||
|
||||
undo_node_t *
|
||||
undo_node_new(undo_flag_t type)
|
||||
{
|
||||
undo_node_t *node = calloc1(sizeof(undo_node_t));
|
||||
|
||||
node->type = type;
|
||||
node->row = node->col = 0;
|
||||
node->next = NULL;
|
||||
node->child = NULL;
|
||||
|
||||
ab_init(&node->text);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_node_free(undo_node_t *node)
|
||||
{
|
||||
if (node == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
ab_free(&node->text);
|
||||
undo_node_free(node->child);
|
||||
undo_node_free(node->next);
|
||||
free(node);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_node_free_branch(undo_node_t *node)
|
||||
{
|
||||
undo_node_t *next = NULL;
|
||||
|
||||
if (node == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (node != NULL) {
|
||||
next = node->next;
|
||||
undo_node_free(node->child);
|
||||
ab_free(&node->text);
|
||||
free(node);
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
undo_tree_t *
|
||||
undo_tree_new(void)
|
||||
{
|
||||
undo_tree_t *tree = NULL;
|
||||
|
||||
tree = calloc1(sizeof(undo_tree_t));
|
||||
tree->root = NULL;
|
||||
tree->current = NULL;
|
||||
tree->saved = NULL;
|
||||
tree->pending = NULL;
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_tree_free(undo_tree_t *ut)
|
||||
{
|
||||
if (ut == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
undo_node_free(ut->root);
|
||||
undo_node_free(ut->pending);
|
||||
|
||||
if (debug_log == NULL) {
|
||||
ut->root = NULL;
|
||||
ut->current = NULL;
|
||||
ut->saved = NULL;
|
||||
ut->pending = NULL;
|
||||
} else {
|
||||
ut->root =
|
||||
ut->current =
|
||||
ut->saved =
|
||||
ut->pending =
|
||||
(void *)0xDEADBEEF;
|
||||
}
|
||||
|
||||
free(ut);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
undo_continue_pending(undo_flag_t type)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *node = tree->pending;
|
||||
|
||||
/* no pending node, so we need to start anew. */
|
||||
if (node == NULL) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (node->type != type) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (node->row != editor.cury || node->col != editor.curx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* undo_begin starts a new undo sequence. Note that it is a non-op
|
||||
* if a new pending sequence doesn't need to be created.
|
||||
*/
|
||||
void
|
||||
undo_begin(const undo_flag_t type)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *node = tree->pending;
|
||||
|
||||
if (undo_continue_pending(type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tree->pending != NULL) {
|
||||
undo_commit();
|
||||
}
|
||||
|
||||
node = undo_node_new(type);
|
||||
assert(node != NULL);
|
||||
|
||||
node->type = type;
|
||||
node->row = editor.cury;
|
||||
node->col = editor.curx;
|
||||
tree->pending = node;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_append_char(const char c)
|
||||
{
|
||||
undo_node_t *node = editor.undo->pending;
|
||||
|
||||
assert(node != NULL);
|
||||
|
||||
ab_appendch(&node->text, c);
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_append(const char *data, const size_t len)
|
||||
{
|
||||
undo_node_t *node = editor.undo->pending;
|
||||
|
||||
assert(node != NULL);
|
||||
|
||||
ab_append(&node->text, data, len);
|
||||
}
|
||||
|
||||
|
||||
/* Finish the current batch and link it into the tree */
|
||||
void
|
||||
undo_commit(void)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *node = NULL;
|
||||
|
||||
if (tree->pending == NULL) {
|
||||
return; /* nothing to commit */
|
||||
}
|
||||
|
||||
node = tree->pending;
|
||||
tree->pending = NULL;
|
||||
|
||||
if (tree->current && tree->current->child) {
|
||||
undo_node_free_branch(tree->current->child);
|
||||
tree->current->child = NULL;
|
||||
}
|
||||
|
||||
|
||||
if (tree->root == NULL) {
|
||||
/* First edit ever */
|
||||
tree->root = node;
|
||||
tree->current = node;
|
||||
} else if (tree->current == NULL) {
|
||||
/* this shouldn't happen, so throw an
|
||||
* assert to catch it.
|
||||
*/
|
||||
assert(tree->current != NULL);
|
||||
tree->root = tree->current = node;
|
||||
} else {
|
||||
tree->current->child = node;
|
||||
tree->current = node;
|
||||
}
|
||||
|
||||
if (tree->saved && tree->current != tree->saved) {
|
||||
tree->saved = NULL;
|
||||
}
|
||||
|
||||
editor.dirty = 1;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_discard_redo_branches(struct undo_node *from)
|
||||
{
|
||||
undo_node_free(from->child);
|
||||
from->child = NULL;
|
||||
|
||||
undo_node_free(from->next);
|
||||
from->next = NULL;
|
||||
}
|
||||
|
||||
|
||||
undo_node_t *
|
||||
undo_parent_of(undo_node_t *node)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *parent = tree->root;
|
||||
|
||||
if (tree->root == node) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
parent = tree->root;
|
||||
while (parent != NULL && parent->child != node) {
|
||||
parent = parent->child;
|
||||
}
|
||||
|
||||
if (parent == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
editor_undo(void)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *node = tree->current;
|
||||
undo_node_t *parent = NULL;
|
||||
|
||||
if (node == NULL || node == tree->root) {
|
||||
editor_set_status("Nothing to undo.");
|
||||
return;
|
||||
}
|
||||
|
||||
parent = undo_parent_of(node);
|
||||
assert(parent != NULL);
|
||||
|
||||
undo_apply(node, -1);
|
||||
tree->current = parent;
|
||||
|
||||
display_refresh();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
editor_redo(void)
|
||||
{
|
||||
undo_tree_t *tree = editor.undo;
|
||||
undo_node_t *node = tree->current;
|
||||
|
||||
if (node == NULL || node->child == NULL) {
|
||||
editor_set_status("Nothing to redo.");
|
||||
return;
|
||||
}
|
||||
|
||||
tree->current = node->child;
|
||||
undo_apply(node->child, 1);
|
||||
|
||||
display_refresh();
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
undo_apply(const struct undo_node *node, const int direction)
|
||||
{
|
||||
int row = node->row;
|
||||
int col = node->col;
|
||||
const char *data = node->text.b;
|
||||
size_t len = node->text.len;
|
||||
|
||||
jump_to_position(col, row);
|
||||
|
||||
switch (node->type) {
|
||||
case UNDO_PASTE:
|
||||
case UNDO_INSERT:
|
||||
if (direction > 0) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
insertch((unsigned char)data[i]);
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
deletech(KILLRING_NO_OP);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UNDO_DELETE:
|
||||
if (direction > 0) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
deletech(KILLRING_NO_OP);
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
insertch((unsigned char)data[i]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UNDO_NEWLINE:
|
||||
if (direction > 0) {
|
||||
newline();
|
||||
} else {
|
||||
if (editor.cury > 0) {
|
||||
editor.curx = editor.row[editor.cury - 1].size;
|
||||
row_append_row(&editor.row[editor.cury - 1],
|
||||
editor.row[editor.cury].line,
|
||||
editor.row[editor.cury].size);
|
||||
delete_row(editor.cury);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case UNDO_DELETE_ROW:
|
||||
if (direction > 0) {
|
||||
/* redo = delete the whole row again */
|
||||
delete_row(editor.cury);
|
||||
} else {
|
||||
/* undo = re-insert the saved row (including final '\n') */
|
||||
if (len > 0 && data[len - 1] == '\n') len--;
|
||||
erow_insert(editor.cury, (char*)data, len);
|
||||
/* cursor goes to start of re-inserted row */
|
||||
editor.curx = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case UNDO_INDENT:
|
||||
case UNDO_KILL_REGION:
|
||||
/* These are more complex – you can implement later */
|
||||
/* For now just move cursor and say "not implemented yet" */
|
||||
editor_set_status("Undo of %s not implemented yet",
|
||||
direction > 0 ? "redo" : "complex op");
|
||||
break;
|
||||
default:
|
||||
editor_set_status("Unknown undo type: %d", node->type);
|
||||
break;
|
||||
}
|
||||
|
||||
editor.dirty = 1;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
killring_flush(void)
|
||||
{
|
||||
@@ -1723,76 +2191,6 @@ char
|
||||
}
|
||||
|
||||
|
||||
// void
|
||||
// editor_find_callback(char *query, int16_t c)
|
||||
// {
|
||||
// static int lmatch = -1;
|
||||
// static int dir = -1;
|
||||
// int i, current, traversed = 0;
|
||||
// int qlen = kstrnlen(query, 128);
|
||||
// char *match;
|
||||
// struct erow *row;
|
||||
//
|
||||
// if (c == '\r' || c == ESC_KEY || c == CTRL_KEY('g')) {
|
||||
// /* reset search */
|
||||
// lmatch = -1;
|
||||
// dir = 1;
|
||||
// return;
|
||||
// } else if (c == ARROW_RIGHT || c == ARROW_DOWN || c == CTRL_KEY('s')) {
|
||||
// dir = 1;
|
||||
// } else if (c == ARROW_LEFT || c == ARROW_UP) {
|
||||
// dir = -1;
|
||||
// } else {
|
||||
// lmatch = -1;
|
||||
// dir = 1;
|
||||
// }
|
||||
//
|
||||
// if (lmatch == -1) {
|
||||
// dir = 1;
|
||||
// current = editor.cury;
|
||||
// }
|
||||
// current = lmatch;
|
||||
//
|
||||
// for (i = 0; i < editor.nrows; i++) {
|
||||
// traversed++;
|
||||
// if (traversed >= editor.nrows) {
|
||||
// break;
|
||||
// }
|
||||
//
|
||||
// current += dir;
|
||||
// if (current < 0) {
|
||||
// current = editor.nrows - 1;
|
||||
// } else if (current >= editor.nrows) {
|
||||
// current = 0;
|
||||
// }
|
||||
//
|
||||
// row = &editor.row[current];
|
||||
// if (row == NULL || row->size < qlen) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// match = strnstr(row->render, query, row->size);
|
||||
// if (match) {
|
||||
// lmatch = current;
|
||||
// editor.cury = current;
|
||||
// editor.curx = erow_render_to_cursor(row,
|
||||
// match - row->render);
|
||||
// if (editor.curx > row->size) {
|
||||
// editor.curx = row->size;
|
||||
// }
|
||||
// /*
|
||||
// * after this, scroll will put the match line at
|
||||
// * the top of the screen.
|
||||
// */
|
||||
// editor.rowoffs = editor.nrows;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// display_refresh();
|
||||
// }
|
||||
|
||||
|
||||
void
|
||||
editor_find_callback(char* query, int16_t c)
|
||||
{
|
||||
@@ -2346,7 +2744,10 @@ process_kcommand(int16_t c)
|
||||
case 'x':
|
||||
exit(save_file());
|
||||
case 'u':
|
||||
editor_set_status("undo: todo");
|
||||
editor_undo();
|
||||
break;
|
||||
case 'U':
|
||||
editor_redo();
|
||||
break;
|
||||
case 'y':
|
||||
killring_yank();
|
||||
|
||||
Reference in New Issue
Block a user