summaryrefslogtreecommitdiff
path: root/examples/dte/change.c
diff options
context:
space:
mode:
Diffstat (limited to 'examples/dte/change.c')
-rw-r--r--examples/dte/change.c417
1 files changed, 417 insertions, 0 deletions
diff --git a/examples/dte/change.c b/examples/dte/change.c
new file mode 100644
index 0000000..529036d
--- /dev/null
+++ b/examples/dte/change.c
@@ -0,0 +1,417 @@
+#include <stdlib.h>
+#include <string.h>
+#include "change.h"
+#include "buffer.h"
+#include "edit.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/xmalloc.h"
+
+static ChangeMergeEnum change_merge;
+static ChangeMergeEnum prev_change_merge;
+
+static Change *alloc_change(void)
+{
+ return xcalloc(sizeof(Change));
+}
+
+static void add_change(Buffer *buffer, Change *change)
+{
+ Change *head = buffer->cur_change;
+ change->next = head;
+ xrenew(head->prev, head->nr_prev + 1);
+ head->prev[head->nr_prev++] = change;
+ buffer->cur_change = change;
+}
+
+// This doesn't need to be local to buffer because commands are atomic
+static Change *change_barrier;
+
+static bool is_change_chain_barrier(const Change *change)
+{
+ return !change->ins_count && !change->del_count;
+}
+
+static Change *new_change(Buffer *buffer)
+{
+ if (change_barrier) {
+ /*
+ * We are recording series of changes (:replace for example)
+ * and now we have just made the first change so we have to
+ * mark beginning of the chain.
+ *
+ * We could have done this before when starting the change
+ * chain but then we may have ended up with an empty chain.
+ * We don't want to record empty changes ever.
+ */
+ add_change(buffer, change_barrier);
+ change_barrier = NULL;
+ }
+
+ Change *change = alloc_change();
+ add_change(buffer, change);
+ return change;
+}
+
+static size_t buffer_offset(const View *view)
+{
+ return block_iter_get_offset(&view->cursor);
+}
+
+static void record_insert(View *view, size_t len)
+{
+ Change *change = view->buffer->cur_change;
+ BUG_ON(!len);
+ if (
+ change_merge == prev_change_merge
+ && change_merge == CHANGE_MERGE_INSERT
+ ) {
+ BUG_ON(change->del_count);
+ change->ins_count += len;
+ return;
+ }
+
+ change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->ins_count = len;
+}
+
+static void record_delete(View *view, char *buf, size_t len, bool move_after)
+{
+ BUG_ON(!len);
+ BUG_ON(!buf);
+
+ Change *change = view->buffer->cur_change;
+ if (change_merge == prev_change_merge) {
+ if (change_merge == CHANGE_MERGE_DELETE) {
+ xrenew(change->buf, change->del_count + len);
+ memcpy(change->buf + change->del_count, buf, len);
+ change->del_count += len;
+ free(buf);
+ return;
+ }
+ if (change_merge == CHANGE_MERGE_ERASE) {
+ xrenew(buf, len + change->del_count);
+ memcpy(buf + len, change->buf, change->del_count);
+ change->del_count += len;
+ free(change->buf);
+ change->buf = buf;
+ change->offset -= len;
+ return;
+ }
+ }
+
+ change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->del_count = len;
+ change->move_after = move_after;
+ change->buf = buf;
+}
+
+static void record_replace(View *view, char *deleted, size_t del_count, size_t ins_count)
+{
+ BUG_ON(del_count && !deleted);
+ BUG_ON(!del_count && deleted);
+ BUG_ON(!del_count && !ins_count);
+
+ Change *change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->ins_count = ins_count;
+ change->del_count = del_count;
+ change->buf = deleted;
+}
+
+void begin_change(ChangeMergeEnum m)
+{
+ change_merge = m;
+}
+
+void end_change(void)
+{
+ prev_change_merge = change_merge;
+}
+
+void begin_change_chain(void)
+{
+ BUG_ON(change_barrier);
+
+ // Allocate change chain barrier but add it to the change tree only if
+ // there will be any real changes
+ change_barrier = alloc_change();
+ change_merge = CHANGE_MERGE_NONE;
+}
+
+void end_change_chain(View *view)
+{
+ if (change_barrier) {
+ // There were no changes in this change chain
+ free(change_barrier);
+ change_barrier = NULL;
+ } else {
+ // There were some changes; add end of chain marker
+ add_change(view->buffer, alloc_change());
+ }
+}
+
+static void fix_cursors(const View *view, size_t offset, size_t del, size_t ins)
+{
+ const Buffer *buffer = view->buffer;
+ for (size_t i = 0, n = buffer->views.count; i < n; i++) {
+ View *v = buffer->views.ptrs[i];
+ if (v != view && offset < v->saved_cursor_offset) {
+ if (offset + del <= v->saved_cursor_offset) {
+ v->saved_cursor_offset -= del;
+ v->saved_cursor_offset += ins;
+ } else {
+ v->saved_cursor_offset = offset;
+ }
+ }
+ }
+}
+
+static void reverse_change(View *view, Change *change)
+{
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, change->offset, change->ins_count, change->del_count);
+ }
+
+ block_iter_goto_offset(&view->cursor, change->offset);
+ if (!change->ins_count) {
+ // Convert delete to insert
+ do_insert(view, change->buf, change->del_count);
+ if (change->move_after) {
+ block_iter_skip_bytes(&view->cursor, change->del_count);
+ }
+ change->ins_count = change->del_count;
+ change->del_count = 0;
+ free(change->buf);
+ change->buf = NULL;
+ } else if (change->del_count) {
+ // Reverse replace
+ size_t del_count = change->ins_count;
+ size_t ins_count = change->del_count;
+ char *buf = do_replace(view, del_count, change->buf, ins_count);
+ free(change->buf);
+ change->buf = buf;
+ change->ins_count = ins_count;
+ change->del_count = del_count;
+ } else {
+ // Convert insert to delete
+ change->buf = do_delete(view, change->ins_count, true);
+ change->del_count = change->ins_count;
+ change->ins_count = 0;
+ }
+}
+
+bool undo(View *view)
+{
+ Change *change = view->buffer->cur_change;
+ view_reset_preferred_x(view);
+ if (!change->next) {
+ return false;
+ }
+
+ if (is_change_chain_barrier(change)) {
+ unsigned long count = 0;
+ while (1) {
+ change = change->next;
+ if (is_change_chain_barrier(change)) {
+ break;
+ }
+ reverse_change(view, change);
+ count++;
+ }
+ if (count > 1) {
+ info_msg("Undid %lu changes", count);
+ }
+ } else {
+ reverse_change(view, change);
+ }
+
+ view->buffer->cur_change = change->next;
+ return true;
+}
+
+bool redo(View *view, unsigned long change_id)
+{
+ Change *change = view->buffer->cur_change;
+ view_reset_preferred_x(view);
+ if (!change->prev) {
+ // Don't complain if change_id is 0
+ if (change_id) {
+ error_msg("Nothing to redo");
+ }
+ return false;
+ }
+
+ const unsigned long nr_prev = change->nr_prev;
+ BUG_ON(nr_prev == 0);
+ if (change_id == 0) {
+ // Default to newest change
+ change_id = nr_prev - 1;
+ if (nr_prev > 1) {
+ unsigned long i = change_id + 1;
+ info_msg("Redoing newest (%lu) of %lu possible changes", i, nr_prev);
+ }
+ } else {
+ if (--change_id >= nr_prev) {
+ if (nr_prev == 1) {
+ return error_msg("There is only 1 possible change to redo");
+ }
+ return error_msg("There are only %lu possible changes to redo", nr_prev);
+ }
+ }
+
+ change = change->prev[change_id];
+ if (is_change_chain_barrier(change)) {
+ unsigned long count = 0;
+ while (1) {
+ change = change->prev[change->nr_prev - 1];
+ if (is_change_chain_barrier(change)) {
+ break;
+ }
+ reverse_change(view, change);
+ count++;
+ }
+ if (count > 1) {
+ info_msg("Redid %lu changes", count);
+ }
+ } else {
+ reverse_change(view, change);
+ }
+
+ view->buffer->cur_change = change;
+ return true;
+}
+
+void free_changes(Change *c)
+{
+top:
+ while (c->nr_prev) {
+ c = c->prev[c->nr_prev - 1];
+ }
+
+ // c is leaf now
+ while (c->next) {
+ Change *next = c->next;
+ free(c->buf);
+ free(c);
+
+ c = next;
+ if (--c->nr_prev) {
+ goto top;
+ }
+
+ // We have become leaf
+ free(c->prev);
+ }
+}
+
+void buffer_insert_bytes(View *view, const char *buf, const size_t len)
+{
+ view_reset_preferred_x(view);
+ if (len == 0) {
+ return;
+ }
+
+ size_t rec_len = len;
+ if (buf[len - 1] != '\n' && block_iter_is_eof(&view->cursor)) {
+ // Force newline at EOF
+ do_insert(view, "\n", 1);
+ rec_len++;
+ }
+
+ do_insert(view, buf, len);
+ record_insert(view, rec_len);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
+ }
+}
+
+static bool would_delete_last_bytes(const View *view, size_t count)
+{
+ const Block *blk = view->cursor.blk;
+ size_t offset = view->cursor.offset;
+ while (1) {
+ size_t avail = blk->size - offset;
+ if (avail > count) {
+ return false;
+ }
+
+ if (blk->node.next == view->cursor.head) {
+ return true;
+ }
+
+ count -= avail;
+ blk = BLOCK(blk->node.next);
+ offset = 0;
+ }
+}
+
+static void buffer_delete_bytes_internal(View *view, size_t len, bool move_after)
+{
+ view_reset_preferred_x(view);
+ if (len == 0) {
+ return;
+ }
+
+ // Check if all newlines from EOF would be deleted
+ if (would_delete_last_bytes(view, len)) {
+ BlockIter bi = view->cursor;
+ CodePoint u;
+ if (block_iter_prev_char(&bi, &u) && u != '\n') {
+ // No newline before cursor
+ if (--len == 0) {
+ begin_change(CHANGE_MERGE_NONE);
+ return;
+ }
+ }
+ }
+ record_delete(view, do_delete(view, len, true), len, move_after);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
+ }
+}
+
+void buffer_delete_bytes(View *view, size_t len)
+{
+ buffer_delete_bytes_internal(view, len, false);
+}
+
+void buffer_erase_bytes(View *view, size_t len)
+{
+ buffer_delete_bytes_internal(view, len, true);
+}
+
+void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count)
+{
+ view_reset_preferred_x(view);
+ if (del_count == 0) {
+ buffer_insert_bytes(view, ins, ins_count);
+ return;
+ }
+ if (ins_count == 0) {
+ buffer_delete_bytes(view, del_count);
+ return;
+ }
+
+ // Check if all newlines from EOF would be deleted
+ if (would_delete_last_bytes(view, del_count)) {
+ if (ins[ins_count - 1] != '\n') {
+ // Don't replace last newline
+ if (--del_count == 0) {
+ buffer_insert_bytes(view, ins, ins_count);
+ return;
+ }
+ }
+ }
+
+ char *deleted = do_replace(view, del_count, ins, ins_count);
+ record_replace(view, deleted, del_count, ins_count);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), del_count, ins_count);
+ }
+}