ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit 5d691e9f97b979d571d38c101554d72e91cb7db5
parent 8779bb819724ae7d05491112ae700063f49cde93
Author: lumidify <nobody@lumidify.org>
Date:   Tue, 29 Jun 2021 19:06:30 +0200

Add basic undo/redo support

Diffstat:
MIDEAS | 1+
MMakefile | 4++--
Mbuffer.c | 350+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mbuffer.h | 21+++++++++++++--------
Mcache.c | 1+
Mcommon.h | 2++
Albuf.c | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Albuf.h | 12++++++++++++
Mledit.c | 160++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msearch.c | 1+
Aundo.c | 250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aundo.h | 23+++++++++++++++++++++++
12 files changed, 738 insertions(+), 138 deletions(-)

diff --git a/IDEAS b/IDEAS @@ -1,3 +1,4 @@ * allow editing same file in multiple places at same time (like in acme) * add different (more basic) text backend * visual selection mode - allow to switch cursor between selection ends +* https://drewdevault.com/2021/06/27/You-cant-capture-the-nuance.html diff --git a/Makefile b/Makefile @@ -9,8 +9,8 @@ MANPREFIX = ${PREFIX}/man BIN = ${NAME} MAN1 = ${BIN:=.1} -OBJ = ${BIN:=.o} cache.o buffer.o memory.o util.o search.o -HDR = cache.h buffer.h memory.h common.h util.h search.h +OBJ = ${BIN:=.o} cache.o buffer.o memory.o util.o search.o lbuf.o undo.o +HDR = cache.h buffer.h memory.h common.h util.h search.h lbuf.h undo.h CFLAGS_LEDIT = -g -Wall -Wextra -D_POSIX_C_SOURCE=200809L `pkg-config --cflags x11 xkbfile pangoxft xext` LDFLAGS_LEDIT = ${LDFLAGS} `pkg-config --libs x11 xkbfile pangoxft xext` -lm diff --git a/buffer.c b/buffer.c @@ -11,8 +11,10 @@ #include "memory.h" #include "common.h" +#include "lbuf.h" #include "buffer.h" #include "cache.h" +#include "undo.h" /* * Important notes: @@ -147,13 +149,16 @@ ledit_wipe_line_cursor_attrs(ledit_buffer *buffer, int line) { l->dirty = 1; } +/* FIXME: To simplify this a bit, maybe just copy text to lbuf first and + then insert it in one go instead of having this complex logic */ void ledit_insert_text_from_line( ledit_buffer *buffer, int dst_line, int dst_index, - int src_line, int src_index, int src_len) { + int src_line, int src_index, int src_len, + lbuf *text_ret) { ledit_insert_text_from_line_base( - buffer, dst_line, dst_index, src_line, src_index, src_len + buffer, dst_line, dst_index, src_line, src_index, src_len, text_ret ); ledit_recalc_line(buffer, dst_line); } @@ -162,23 +167,42 @@ void ledit_insert_text_from_line_base( ledit_buffer *buffer, int dst_line, int dst_index, - int src_line, int src_index, int src_len) { + int src_line, int src_index, int src_len, + lbuf *text_ret) { assert(dst_line != src_line); ledit_line *ll = ledit_get_line(buffer, src_line); if (src_len == -1) src_len = ll->len - src_index; + if (text_ret != NULL) { + lbuf_grow(text_ret, src_len); + text_ret->len = src_len; + } if (src_index >= ll->gap) { /* all text to insert is after gap */ ledit_insert_text_base( buffer, dst_line, dst_index, ll->text + src_index + ll->cap - ll->len, src_len ); + if (text_ret != NULL) { + memcpy( + text_ret->text, + ll->text + src_index + ll->cap - ll->len, + src_len + ); + } } else if (ll->gap - src_index >= src_len) { /* all text to insert is before gap */ ledit_insert_text_base( buffer, dst_line, dst_index, ll->text + src_index, src_len ); + if (text_ret != NULL) { + memcpy( + text_ret->text, + ll->text + src_index, + src_len + ); + } } else { /* insert part of text before gap */ ledit_insert_text_base( @@ -191,6 +215,18 @@ ledit_insert_text_from_line_base( ll->text + ll->gap + ll->cap - ll->len, src_len - ll->gap + src_index ); + if (text_ret != NULL) { + memcpy( + text_ret->text, + ll->text + src_index, + ll->gap - src_index + ); + memcpy( + text_ret + ll->gap - src_index, + ll->text + ll->gap + ll->cap - ll->len, + src_len - ll->gap + src_index + ); + } } } @@ -343,9 +379,9 @@ ledit_insert_text_with_newlines_base( int cur_line = line_index; int cur_index = index; while ((cur = strchr_len(last, '\n', rem_len)) != NULL) { - ledit_insert_text_base(buffer, cur_line, cur_index, last, cur - last); /* FIXME: inefficient because there's no gap buffer yet */ - ledit_append_line_base(buffer, cur_line, -1); + ledit_append_line_base(buffer, cur_line, cur_index); + ledit_insert_text_base(buffer, cur_line, cur_index, last, cur - last); cur_index = 0; cur_line++; last = cur + 1; @@ -453,7 +489,7 @@ ledit_append_line_base(ledit_buffer *buffer, int line_index, int text_index) { ledit_line *l = ledit_get_line(buffer, line_index); ledit_insert_text_from_line_base( buffer, line_index + 1, 0, - line_index, text_index, -1 + line_index, text_index, -1, NULL ); delete_line_section_base( buffer, line_index, @@ -653,21 +689,17 @@ ledit_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2 * - *dst is null-terminated * - the range must be sorted already * - returns the length of the text, not including the NUL */ -size_t -ledit_copy_text_with_resize( +void +ledit_copy_text_to_lbuf( ledit_buffer *buffer, - char **dst, size_t *alloc, + lbuf *buf, int line1, int byte1, int line2, int byte2) { assert(line1 < line2 || (line1 == line2 && byte1 <= byte2)); size_t len = ledit_textlen(buffer, line1, byte1, line2, byte2); - /* len + 1 because of nul */ - if (len + 1 > *alloc) { - *alloc = *alloc * 2 > len + 1 ? *alloc * 2 : len + 1; - *dst = ledit_realloc(*dst, *alloc); - } - ledit_copy_text(buffer, *dst, line1, byte1, line2, byte2); - return len; + lbuf_grow(buf, len + 1); + ledit_copy_text(buffer, buf->text, line1, byte1, line2, byte2); + buf->len = len; } /* get char with logical index i from line */ @@ -830,12 +862,14 @@ ledit_delete_range( ledit_buffer *buffer, int line_based, int line_index1, int byte_index1, int line_index2, int byte_index2, - int *new_line_ret, int *new_byte_ret) { + int *new_line_ret, int *new_byte_ret, + ledit_range *final_range_ret, lbuf *text_ret) { ledit_delete_range_base( buffer, line_based, line_index1, byte_index1, line_index2, byte_index2, - new_line_ret, new_byte_ret + new_line_ret, new_byte_ret, + final_range_ret, text_ret ); /* need to start recalculating one line before in case first line was deleted and offset is now wrong */ @@ -849,7 +883,12 @@ ledit_delete_range_base( ledit_buffer *buffer, int line_based, int line_index1, int byte_index1, int line_index2, int byte_index2, - int *new_line_ret, int *new_byte_ret) { + int *new_line_ret, int *new_byte_ret, + ledit_range *final_range_ret, lbuf *text_ret) { + /* FIXME: Oh boy, this is nasty */ + /* range line x, range byte x */ + int rgl1 = 0, rgb1 = 0, rgl2 = 0, rgb2 = 0; + int new_line = 0, new_byte = 0; if (line_based) { int x, softline1, softline2; ledit_line *line1 = ledit_get_line(buffer, line_index1); @@ -865,51 +904,76 @@ ledit_delete_range_base( PangoLayoutLine *pl2 = pango_layout_get_line_readonly(line1->layout, l2); /* don't delete entire line if it is the last one remaining */ if (l1 == 0 && l2 == softlines - 1 && buffer->lines_num > 1) { - ledit_delete_line_entry_base(buffer, line_index1); - /* note: line_index1 is now the index of the next - line since the current one was just deleted */ - if (line_index1 < buffer->lines_num) { - *new_line_ret = line_index1; + if (line_index1 < buffer->lines_num - 1) { + /* cursor can be moved to next hard line */ + new_line = line_index1; ledit_x_softline_to_pos( - ledit_get_line(buffer, line_index1), - x, 0, new_byte_ret + ledit_get_line(buffer, line_index1 + 1), + x, 0, &new_byte ); + rgl1 = line_index1; + rgb1 = 0; + rgl2 = line_index1 + 1; + rgb2 = 0; } else { - /* note: logically, this must be >= 0 because - buffer->lines_num > 1 && line_index1 >= buffer->lines_num */ - *new_line_ret = line_index1 - 1; + /* cursor has to be be moved to previous hard line + because last line in buffer is deleted */ + /* note: logically, line_index1 - 1 must be >= 0 because + buffer->lines_num > 1 && line_index1 >= buffer->lines_num - 1 */ + new_line = line_index1 - 1; ledit_line *prevline = ledit_get_line(buffer, line_index1 - 1); softlines = pango_layout_get_line_count(prevline->layout); - ledit_x_softline_to_pos(prevline, x, softlines - 1, new_byte_ret); + ledit_x_softline_to_pos(prevline, x, softlines - 1, &new_byte); + rgl1 = line_index1 - 1; + rgb1 = prevline->len; + rgl2 = line_index1; + rgb2 = line1->len; } + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } + ledit_delete_line_entry_base(buffer, line_index1); } else { - /* FIXME: sanity checks that the length is actually positive, etc. */ + assert(pl2->start_index + pl2->length - pl1->start_index >= 0); + rgl1 = rgl2 = line_index1; + rgb1 = pl1->start_index; + rgb2 = pl2->start_index + pl2->length; + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } delete_line_section_base( - buffer, line_index1, pl1->start_index, - pl2->start_index + pl2->length - pl1->start_index + buffer, line_index1, rgb1, rgb2 - rgb1 ); if (l2 == softlines - 1 && line_index1 < buffer->lines_num - 1) { - *new_line_ret = line_index1 + 1; + new_line = line_index1 + 1; ledit_x_softline_to_pos( ledit_get_line(buffer, line_index1 + 1), - x, 0, new_byte_ret + x, 0, &new_byte ); } else if (l2 < softlines - 1) { - *new_line_ret = line_index1; + new_line = line_index1; ledit_x_softline_to_pos( ledit_get_line(buffer, line_index1), - x, l1, new_byte_ret + x, l1, &new_byte ); } else if (l1 > 0) { - *new_line_ret = line_index1; + new_line = line_index1; ledit_x_softline_to_pos( ledit_get_line(buffer, line_index1), - x, l1 - 1, new_byte_ret + x, l1 - 1, &new_byte ); } else { /* the line has been emptied and is the last line remaining */ - *new_line_ret = 0; - *new_byte_ret = 0; + new_line = 0; + new_byte = 0; } } } else { @@ -937,96 +1001,186 @@ ledit_delete_range_base( int softlines = pango_layout_get_line_count(ll2->layout); if (sl1 == 0 && sl2 == softlines - 1) { if (l1 == 0 && l2 == buffer->lines_num - 1) { + rgl1 = l1; + rgl2 = l2; + rgb1 = 0; + rgb2 = ll2->len; + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } delete_line_section_base(buffer, l1, 0, ll1->len); ledit_delete_line_entries_base(buffer, l1 + 1, l2); - *new_line_ret = 0; - *new_byte_ret = 0; + new_line = 0; + new_byte = 0; } else { - ledit_delete_line_entries_base(buffer, l1, l2); - if (l1 >= buffer->lines_num) { - *new_line_ret = buffer->lines_num - 1; - ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret); + if (l2 == buffer->lines_num - 1) { + new_line = l1 - 1; + ledit_line *new_lline = ledit_get_line(buffer, new_line); int new_softlines = pango_layout_get_line_count(new_lline->layout); - ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret); + ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, &new_byte); + rgl1 = l1 - 1; + rgb1 = new_lline->len; + rgl2 = l2; + rgb2 = ll2->len; } else { - *new_line_ret = l1; + new_line = l1; + ledit_line *nextline = ledit_get_line(buffer, l2 + 1); ledit_x_softline_to_pos( - ledit_get_line(buffer, l1), - x, 0, new_byte_ret + nextline, x, 0, &new_byte ); + rgl1 = l1; + rgb1 = 0; + rgl2 = l2 + 1; + rgb2 = nextline->len; } + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } + ledit_delete_line_entries_base(buffer, l1, l2); } } else if (sl1 == 0) { + rgl1 = l1; + rgb1 = 0; + rgl2 = l2; + rgb2 = pl2->start_index + pl2->length; + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } delete_line_section_base(buffer, l2, 0, pl2->start_index + pl2->length); + new_line = l1; + ledit_x_softline_to_pos(ll2, x, 0, &new_byte); ledit_delete_line_entries_base(buffer, l1, l2 - 1); - *new_line_ret = l1; - ledit_x_softline_to_pos(ledit_get_line(buffer, l1), x, 0, new_byte_ret); } else if (sl2 == softlines - 1) { - delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index); - ledit_delete_line_entries_base(buffer, l1 + 1, l2); - if (l1 + 1 >= buffer->lines_num) { - *new_line_ret = buffer->lines_num - 1; - ledit_line *new_lline = ledit_get_line(buffer, *new_line_ret); - int new_softlines = pango_layout_get_line_count(new_lline->layout); - ledit_x_softline_to_pos(new_lline, x, new_softlines - 1, new_byte_ret); + rgl1 = l1; + rgb1 = pl1->start_index; + rgl2 = l2; + rgb2 = ll2->len; + if (l2 + 1 == buffer->lines_num) { + new_line = l1; + ledit_x_softline_to_pos(ll1, x, sl1 - 1, &new_byte); } else { - *new_line_ret = l1 + 1; + new_line = l1 + 1; ledit_x_softline_to_pos( - ledit_get_line(buffer, l1 + 1), - x, 0, new_byte_ret + ledit_get_line(buffer, l2 + 1), + x, 0, &new_byte + ); + } + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 ); } + delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index); + ledit_delete_line_entries_base(buffer, l1 + 1, l2); } else { - /* FIXME: should this join the two lines? */ + /* FIXME: this could be made nicer by just using the range to + delete all in one go at the end */ + rgl1 = l1; + rgb1 = pl1->start_index; + rgl2 = l2; + rgb2 = pl2->start_index + pl2->length; + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); + } delete_line_section_base(buffer, l1, pl1->start_index, ll1->len - pl1->start_index); - delete_line_section_base(buffer, l2, 0, pl2->start_index + pl2->length); - if (l2 > l1 + 1) - ledit_delete_line_entries_base(buffer, l1 + 1, l2 - 1); - *new_line_ret = l1 + 1; - ledit_x_softline_to_pos(ledit_get_line(buffer, l1 + 1), x, 0, new_byte_ret); + ledit_insert_text_from_line_base( + buffer, + l1, pl1->start_index, + l2, pl2->start_index + pl2->length, + ll2->len - (pl2->start_index + pl2->length), NULL + ); + ledit_delete_line_entries_base(buffer, l1 + 1, l2); + new_line = l1; + int new_softlines = pango_layout_get_line_count(ll1->layout); + /* it's technically possible that the remaining part of the + second line is so small that it doesn't generate a new + softline, so there needs to be a special case - this is + a bit weird because the cursor will seem to stay on the + same line, but it now includes the rest of the second line + (FIXME: this is probably not the best thing to do) */ + ledit_x_softline_to_pos( + ll1, x, sl1 + 1 < new_softlines ? sl1 + 1 : sl1, &new_byte + ); } } } else { if (line_index1 == line_index2) { - int b1, b2; + rgl1 = rgl2 = line_index1; if (byte_index1 < byte_index2) { - b1 = byte_index1; - b2 = byte_index2; + rgb1 = byte_index1; + rgb2 = byte_index2; } else { - b1 = byte_index2; - b2 = byte_index1; + rgb1 = byte_index2; + rgb2 = byte_index1; + } + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 + ); } - delete_line_section_base(buffer, line_index1, b1, b2 - b1); - *new_line_ret = line_index1; - *new_byte_ret = b1; + delete_line_section_base(buffer, line_index1, rgb1, rgb2 - rgb1); + new_line = line_index1; + new_byte = rgb1; } else { - int l1, l2, b1, b2; if (line_index1 < line_index2) { - l1 = line_index1; - b1 = byte_index1; - l2 = line_index2; - b2 = byte_index2; + rgl1 = line_index1; + rgb1 = byte_index1; + rgl2 = line_index2; + rgb2 = byte_index2; } else { - l1 = line_index2; - b1 = byte_index2; - l2 = line_index1; - b2 = byte_index1; + rgl1 = line_index2; + rgb1 = byte_index2; + rgl2 = line_index1; + rgb2 = byte_index1; } - ledit_line *line1 = ledit_get_line(buffer, l1); - ledit_line *line2 = ledit_get_line(buffer, l2); - line1->len = b1; - if (b2 > 0) { - ledit_insert_text_base( - buffer, l1, b1, - line2->text + b2, - line2->len - b2 + if (text_ret) { + ledit_copy_text_to_lbuf( + buffer, text_ret, + rgl1, rgb1, + rgl2, rgb2 ); } - *new_line_ret = l1; - *new_byte_ret = b1; - ledit_delete_line_entries_base(buffer, l1 + 1, l2); + ledit_line *line1 = ledit_get_line(buffer, rgl1); + ledit_line *line2 = ledit_get_line(buffer, rgl2); + delete_line_section_base(buffer, rgl1, rgb1, line1->len - rgb1); + ledit_insert_text_from_line_base( + buffer, rgl1, rgb1, rgl2, rgb2, line2->len - rgb2, NULL + ); + new_line = rgl1; + new_byte = rgb1; + ledit_delete_line_entries_base(buffer, rgl1 + 1, rgl2); } if (buffer->state->mode == NORMAL) - *new_byte_ret = ledit_get_legal_normal_pos(buffer, *new_line_ret, *new_byte_ret); + new_byte = ledit_get_legal_normal_pos(buffer, new_line, new_byte); + } + if (final_range_ret) { + final_range_ret->line1 = rgl1; + final_range_ret->byte1 = rgb1; + final_range_ret->line2 = rgl2; + final_range_ret->byte2 = rgb2; } + if (new_line_ret) + *new_line_ret = new_line; + if (new_byte_ret) + *new_byte_ret = new_byte; } diff --git a/buffer.h b/buffer.h @@ -3,7 +3,7 @@ typedef struct { int byte1; int line2; int byte2; -} ledit_selection; +} ledit_range; typedef struct ledit_buffer ledit_buffer; @@ -39,7 +39,8 @@ struct ledit_buffer { long total_height; /* total pixel height of all lines */ double display_offset; /* current pixel offset of viewport - this * is a double to make scrolling smoother */ - ledit_selection sel; /* current selection; all entries -1 if no selection */ + ledit_range sel; /* current selection; all entries -1 if no selection */ + ledit_undo_stack *undo; }; ledit_buffer *ledit_create_buffer(ledit_common_state *state); @@ -58,9 +59,9 @@ int ledit_next_utf8(ledit_line *line, int index); int ledit_prev_utf8(ledit_line *line, int index); size_t ledit_textlen(ledit_buffer *buffer, int line1, int byte1, int line2, int byte2); void ledit_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2); -size_t ledit_copy_text_with_resize( +void ledit_copy_text_to_lbuf( ledit_buffer *buffer, - char **dst, size_t *alloc, + lbuf *buf, /* oh, isn't that a very non-confusing name? */ int line1, int byte1, int line2, int byte2 ); @@ -90,12 +91,14 @@ void ledit_delete_range_base( ledit_buffer *buffer, int line_based, int line_index1, int byte_index1, int line_index2, int byte_index2, - int *new_line_ret, int *new_byte_ret + int *new_line_ret, int *new_byte_ret, + ledit_range *final_range_ret, lbuf *text_ret ); void ledit_insert_text_from_line_base( ledit_buffer *buffer, int dst_line, int dst_index, - int src_line, int src_index, int src_len + int src_line, int src_index, int src_len, + lbuf *text_ret ); void ledit_insert_text(ledit_buffer *buffer, int line_index, int index, char *text, int len); @@ -113,10 +116,12 @@ void ledit_delete_range( ledit_buffer *buffer, int line_based, int line_index1, int byte_index1, int line_index2, int byte_index2, - int *new_line_ret, int *new_byte_ret + int *new_line_ret, int *new_byte_ret, + ledit_range *final_range_ret, lbuf *text_ret ); void ledit_insert_text_from_line( ledit_buffer *buffer, int dst_line, int dst_index, - int src_line, int src_index, int src_len + int src_line, int src_index, int src_len, + lbuf *text_ret ); diff --git a/cache.c b/cache.c @@ -6,6 +6,7 @@ #include "common.h" #include "memory.h" +#include "lbuf.h" #include "buffer.h" #include "cache.h" diff --git a/common.h b/common.h @@ -1,3 +1,5 @@ +/* FIXME: it's ugly to put this here */ +typedef struct ledit_undo_stack ledit_undo_stack; enum ledit_mode { NORMAL = 1, INSERT = 2, diff --git a/lbuf.c b/lbuf.c @@ -0,0 +1,51 @@ +#include <stdlib.h> +#include <string.h> + +#include "memory.h" +#include "lbuf.h" + +lbuf * +lbuf_new(void) { + lbuf *buf = ledit_malloc(sizeof(lbuf)); + buf->text = NULL; + buf->cap = buf->len = 0; + return buf; +} + +void +lbuf_grow(lbuf *buf, size_t sz) { + /* always leave room for extra \0 */ + if (sz + 1 > buf->cap) { + /* FIXME: what are the best values here? */ + buf->cap = buf->cap * 2 > sz + 1 ? buf->cap * 2 : sz + 1; + buf->text = ledit_realloc(buf->text, buf->cap); + } +} + +void +lbuf_shrink(lbuf *buf) { + if ((buf->len + 1) * 4 < buf->cap) { + buf->cap /= 2; + buf->text = ledit_realloc(buf->text, buf->cap); + } +} + +void +lbuf_destroy(lbuf *buf) { + free(buf->text); + free(buf); +} + +void +lbuf_cpy(lbuf *dst, lbuf *src) { + lbuf_grow(dst, src->len); + memcpy(dst->text, src->text, src->len); + dst->len = src->len; +} + +lbuf * +lbuf_dup(lbuf *src) { + lbuf *dst = lbuf_new(); + lbuf_cpy(dst, src); + return dst; +} diff --git a/lbuf.h b/lbuf.h @@ -0,0 +1,12 @@ +/* FIXME: RENAME THIS */ +typedef struct { + size_t len, cap; + char *text; +} lbuf; + +lbuf *lbuf_new(void); +void lbuf_grow(lbuf *buf, size_t sz); +void lbuf_shrink(lbuf *buf); +void lbuf_destroy(lbuf *buf); +void lbuf_cpy(lbuf *dst, lbuf *src); +lbuf *lbuf_dup(lbuf *src); diff --git a/ledit.c b/ledit.c @@ -1,6 +1,6 @@ /* FIXME: Only redraw part of screen if needed */ /* FIXME: overflow in repeated commands */ -/* FIXME: Fix lag when scrolling */ +/* FIXME: Fix lag when scrolling - combine repeated mouse motion events */ /* FIXME: Fix lag when selecting with mouse */ /* FIXME: Use PANGO_PIXELS() */ /* FIXME: Fix cursor movement, especially buffer->trailing and writing at end of line */ @@ -31,10 +31,12 @@ #include "memory.h" #include "common.h" +#include "lbuf.h" #include "buffer.h" #include "search.h" #include "cache.h" #include "util.h" +#include "undo.h" enum key_type { KEY_NONE = 0, @@ -87,6 +89,12 @@ struct key_stack_elem { int data2; /* misc. data 2 */ }; +/* buffer for storing yanked text */ +lbuf *paste_buffer = NULL; +/* temporary buffer used for storing text + in order to add it to the undo stack */ +lbuf *tmp_buffer = NULL; + static struct { size_t len, alloc; struct key_stack_elem *stack; @@ -216,8 +224,7 @@ show_message(char *text, int len) { struct { Atom xtarget; - char *primary; - size_t primary_alloc; + lbuf *primary; char *clipboard; } xsel; @@ -243,6 +250,7 @@ set_mode(enum ledit_mode mode) { XftDrawRect(bottom_bar.mode_draw->xftdraw, &state.bg, 0, 0, bottom_bar.mode_w, bottom_bar.mode_h); pango_xft_render_layout(bottom_bar.mode_draw->xftdraw, &state.fg, bottom_bar.mode, 0, 0); recalc_text_size(); + ledit_change_mode_group(buffer); } void @@ -253,8 +261,9 @@ clipcopy(void) free(xsel.clipboard); xsel.clipboard = NULL; - if (xsel.primary != NULL) { - xsel.clipboard = ledit_strdup(xsel.primary); + /* FIXME: don't copy if text empty (no selection)? */ + if (xsel.primary->text != NULL) { + xsel.clipboard = ledit_strdup(xsel.primary->text); clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0); XSetSelectionOwner(state.dpy, clipboard, state.win, CurrentTime); } @@ -402,7 +411,7 @@ selrequest(XEvent *e) */ clipboard = XInternAtom(state.dpy, "CLIPBOARD", 0); if (xsre->selection == XA_PRIMARY) { - seltext = xsel.primary; + seltext = xsel.primary->text; } else if (xsre->selection == clipboard) { seltext = xsel.clipboard; } else { @@ -482,15 +491,63 @@ get_new_line_softline( } } +/* FIXME: don't overwrite buffer->cur_line, etc. here? */ +static void +delete_range( + int line_based, int selected, + int line_index1, int byte_index1, + int line_index2, int byte_index2) { + (void)selected; /* FIXME */ + ledit_range cur_range, del_range; + cur_range.line1 = buffer->cur_line; + cur_range.byte1 = buffer->cur_index; + ledit_delete_range( + buffer, line_based, + line_index1, byte_index1, + line_index2, byte_index2, + &buffer->cur_line, &buffer->cur_index, + &del_range, paste_buffer + ); + cur_range.line2 = buffer->cur_line; + cur_range.byte2 = buffer->cur_index; + ledit_push_undo_delete( + buffer, paste_buffer, del_range, cur_range, 1 + ); +} + +static void +insert_text(int line, int index, char *text, int len, int start_group) { + if (len < 0) + len = strlen(text); + /* FIXME: this is kind of hacky... */ + lbuf ins_buf = {.text = text, .len = len, .cap = len}; + ledit_range cur_range, del_range; + cur_range.line1 = buffer->cur_line; + cur_range.byte1 = buffer->cur_index; + del_range.line1 = line; + del_range.byte1 = index; + ledit_insert_text_with_newlines( + buffer, line, index, text, len, + &buffer->cur_line, &buffer->cur_index + ); + cur_range.line2 = buffer->cur_line; + cur_range.byte2 = buffer->cur_index; + del_range.line2 = buffer->cur_line; + del_range.byte2 = buffer->cur_index; + ledit_push_undo_insert( + buffer, &ins_buf, del_range, cur_range, start_group + ); +} + static int delete_selection(void) { if (buffer->sel.line1 != buffer->sel.line2 || buffer->sel.byte1 != buffer->sel.byte2) { - ledit_delete_range( - buffer, 0, + delete_range( + 0, 0, buffer->sel.line1, buffer->sel.byte1, - buffer->sel.line2, buffer->sel.byte2, - &buffer->cur_line, &buffer->cur_index + buffer->sel.line2, buffer->sel.byte2 ); + /* FIXME: maybe just set this to the current cursor pos? */ buffer->sel.line1 = buffer->sel.line2 = -1; buffer->sel.byte1 = buffer->sel.byte2 = -1; ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line); @@ -545,11 +602,10 @@ key_d(void) { static void key_d_cb(int line, int char_pos, enum key_type type) { int line_based = type == KEY_MOTION_LINE ? 1 : 0; - ledit_delete_range( - buffer, line_based, + delete_range( + line_based, 0, buffer->cur_line, buffer->cur_index, - line, char_pos, - &buffer->cur_line, &buffer->cur_index + line, char_pos ); ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); } @@ -565,6 +621,7 @@ key_x(void) { num = e->count; if (num <= 0) num = 1; + /* FIXME: actually do something */ } static void @@ -575,7 +632,7 @@ push_num(int num) { e->key = KEY_NUMBER; e->followup = KEY_NUMBER|KEY_NUMBERALLOWED; } - /* FIXME: error checking */ + /* FIXME: error (overflow) checking */ e->count *= 10; e->count += num; } @@ -662,6 +719,7 @@ push_key_stack(void) { /* Note: for peek and pop, the returned element is only valid * until the next element is pushed */ +/* Note on the note: that's not entirely true for peek */ static struct key_stack_elem * peek_key_stack(void) { if (key_stack.len > 0) @@ -947,7 +1005,6 @@ setup(int argc, char *argv[]) { pango_layout_set_font_description(bottom_bar.mode, state.font); /* FIXME: only create "dummy draw" at first and create with proper size when needed */ bottom_bar.mode_draw = ledit_create_draw(&state, 10, 10); - set_mode(INSERT); bottom_bar.line = pango_layout_new(state.context); pango_layout_set_font_description(bottom_bar.line, state.font); bottom_bar.line_draw = ledit_create_draw(&state, 10, 10); @@ -962,12 +1019,17 @@ setup(int argc, char *argv[]) { ledit_init_cache(&state); buffer = ledit_create_buffer(&state); + /* FIXME: move this to create_buffer */ + ledit_init_undo_stack(buffer); + set_mode(INSERT); key_stack.len = key_stack.alloc = 0; key_stack.stack = NULL; - xsel.primary = NULL; - xsel.primary_alloc = 0; + paste_buffer = lbuf_new(); + tmp_buffer = lbuf_new(); + + xsel.primary = lbuf_new(); xsel.clipboard = NULL; xsel.xtarget = XInternAtom(state.dpy, "UTF8_STRING", 0); if (xsel.xtarget == None) @@ -982,6 +1044,7 @@ cleanup(void) { /* FIXME: cleanup everything else */ ledit_cleanup_search(); ledit_destroy_cache(); + ledit_destroy_undo_stack(buffer); ledit_destroy_buffer(buffer); XDestroyWindow(state.dpy, state.win); XCloseDisplay(state.dpy); @@ -1153,10 +1216,7 @@ sort_selection(int *line1, int *byte1, int *line2, int *byte2) { /* lines and bytes need to be sorted already! */ static void copy_selection_to_x_primary(int line1, int byte1, int line2, int byte2) { - (void)ledit_copy_text_with_resize( - buffer, &xsel.primary, &xsel.primary_alloc, - line1, byte1, line2, byte2 - ); + ledit_copy_text_to_lbuf(buffer, xsel.primary, line1, byte1, line2, byte2); XSetSelectionOwner(state.dpy, XA_PRIMARY, state.win, CurrentTime); /* FIXME @@ -1343,19 +1403,27 @@ backspace(void) { } else if (buffer->cur_index == 0) { if (buffer->cur_line != 0) { ledit_line *l1 = ledit_get_line(buffer, buffer->cur_line - 1); + delete_range(0, 0, buffer->cur_line - 1, l1->len, buffer->cur_line, 0); + /* int old_len = l1->len; ledit_insert_text_from_line( buffer, buffer->cur_line - 1, l1->len, - buffer->cur_line, 0, -1 + buffer->cur_line, 0, l2->len, tmp_buffer ); ledit_delete_line_entry(buffer, buffer->cur_line); buffer->cur_line--; buffer->cur_index = old_len; + */ } } else { + ledit_line *l = ledit_get_line(buffer, buffer->cur_line); + int i = ledit_prev_utf8(l, buffer->cur_index); + delete_range(0, 0, buffer->cur_line, buffer->cur_index, buffer->cur_line, i); + /* buffer->cur_index = ledit_delete_unicode_char( buffer, buffer->cur_line, buffer->cur_index, -1 ); + */ } ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); } @@ -1367,19 +1435,24 @@ delete_key(void) { /* NOP */ } else if (buffer->cur_index == cur_line->len) { if (buffer->cur_line != buffer->lines_num - 1) { + delete_range(0, 0, buffer->cur_line, cur_line->len, buffer->cur_line + 1, 0); + /* int old_len = cur_line->len; - /* FIXME: THIS CURRENTLY DOESN'T RECALC LINE SIZE! */ ledit_insert_text_from_line( buffer, buffer->cur_line, cur_line->len, buffer->cur_line + 1, 0, -1 ); ledit_delete_line_entry(buffer, buffer->cur_line + 1); - buffer->cur_index = old_len; + */ } } else { + int i = ledit_next_utf8(cur_line, buffer->cur_index); + delete_range(0, 0, buffer->cur_line, buffer->cur_index, buffer->cur_line, i); + /* buffer->cur_index = ledit_delete_unicode_char( buffer, buffer->cur_line, buffer->cur_index, 1 ); + */ } ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); } @@ -1466,14 +1539,17 @@ cursor_right(void) { static void return_key(void) { - delete_selection(); - ledit_append_line(buffer, buffer->cur_line, buffer->cur_index); + int start_group = 1; + if (delete_selection()) + start_group = 0; + insert_text(buffer->cur_line, buffer->cur_index, "\n", -1, start_group); + /* ledit_append_line(buffer, buffer->cur_line, buffer->cur_index); */ /* FIXME: these aren't needed, right? This only works in insert mode * anyways, so there's nothing to wipe */ - ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line); + /* ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line); buffer->cur_line++; ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); - buffer->cur_index = 0; + buffer->cur_index = 0; */ } static void @@ -1690,6 +1766,23 @@ show_line(void) { show_message(str, len); } +/* FIXME: return status! */ +static void +undo(void) { + set_selection(0, 0, 0, 0); + ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line); + ledit_undo(buffer); + ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); +} + +static void +redo(void) { + set_selection(0, 0, 0, 0); + ledit_wipe_line_cursor_attrs(buffer, buffer->cur_line); + ledit_redo(buffer); + ledit_set_line_cursor_attrs(buffer, buffer->cur_line, buffer->cur_index); +} + /* FIXME: maybe sort these and use binary search */ static struct key keys_en[] = { {NULL, 0, XK_BackSpace, INSERT, KEY_ANY, KEY_ANY, &backspace}, @@ -1728,7 +1821,11 @@ static struct key keys_en[] = { {"/", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward}, {NULL, 0, XK_Return, COMMANDEDIT|SEARCHEDIT, KEY_ANY, KEY_ANY, &end_lineedit}, {"n", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_next}, - {"N", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_prev} + {"N", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &search_prev}, + {"u", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &undo}, + {"U", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &redo}, + {"z", ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &undo}, + {"y", ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &redo} }; static struct key keys_ur[] = { @@ -1859,12 +1956,15 @@ key_press(XEvent event) { state.message_shown--; } else if (state.mode == INSERT && !found && n > 0) { delete_selection(); + insert_text(buffer->cur_line, buffer->cur_index, buf, n, 1); + /* ledit_insert_text_with_newlines( buffer, buffer->cur_line, buffer->cur_index, buf, n, &buffer->cur_line, &buffer->cur_index ); + */ ensure_cursor_shown(); if (state.message_shown > 0) state.message_shown--; diff --git a/search.c b/search.c @@ -6,6 +6,7 @@ #include "memory.h" #include "common.h" +#include "lbuf.h" #include "buffer.h" #include "search.h" diff --git a/undo.c b/undo.c @@ -0,0 +1,250 @@ +#include <string.h> +#include <assert.h> +#include <stdlib.h> + +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <pango/pangoxft.h> +/* FIXME: move some parts of common to ledit.c so + this include isn't needed */ +#include <X11/extensions/Xdbe.h> + +#include "memory.h" +#include "common.h" +#include "lbuf.h" +#include "buffer.h" +#include "cache.h" +#include "undo.h" + +enum operation { + UNDO_INSERT, + UNDO_DELETE +}; + +typedef struct { + lbuf *text; + enum operation type; + enum ledit_mode mode; + ledit_range op_range; + ledit_range cursor_range; + int group; + int mode_group; +} undo_elem; + +struct ledit_undo_stack { + /* FIXME: size_t? */ + int len, cur, cap; + undo_elem *stack; + int change_mode_group; +}; + +/* FIXME: maybe make these work directly on the stack instead of buffer */ +void +ledit_init_undo_stack(ledit_buffer *buffer) { + buffer->undo = ledit_malloc(sizeof(ledit_undo_stack)); + buffer->undo->len = buffer->undo->cap = 0; + buffer->undo->cur = -1; + buffer->undo->stack = NULL; + buffer->undo->change_mode_group = 0; +} + +void +ledit_destroy_undo_stack(ledit_buffer *buffer) { + free(buffer->undo->stack); + free(buffer->undo); +} + +/* FIXME: resize text buffers when they aren't needed anymore */ +static undo_elem * +push_undo_elem(ledit_buffer *buffer) { + ledit_undo_stack *s = buffer->undo; + assert(s->cur >= -1); + s->cur++; + s->len = s->cur + 1; + if (s->len > s->cap) { + size_t cap = s->len * 2; + s->stack = ledit_realloc(s->stack, cap * sizeof(undo_elem)); + for (size_t i = s->cap; i < cap; i++) { + s->stack[i].text = NULL; + } + s->cap = cap; + } + return &s->stack[s->cur]; +} + +static undo_elem * +peek_undo_elem(ledit_buffer *buffer) { + ledit_undo_stack *s = buffer->undo; + if (s->cur < 0) + return NULL; + return &s->stack[s->cur]; +} + +void +ledit_change_mode_group(ledit_buffer *buffer) { + buffer->undo->change_mode_group = 1; +} + +/* FIXME: The current cursor position could be taken directly from the + buffer, but maybe it's better this way to make it a bit more explicit? */ +static void +push_undo( + ledit_buffer *buffer, lbuf *text, + ledit_range insert_range, + ledit_range cursor_range, + int start_group, enum operation type) { + undo_elem *old = peek_undo_elem(buffer); + int last_group = old == NULL ? 0 : old->group; + int last_mode_group = old == NULL ? 0 : old->mode_group; + undo_elem *e = push_undo_elem(buffer); + e->group = start_group ? !last_group : last_group; + e->mode_group = buffer->undo->change_mode_group ? !last_mode_group : last_mode_group; + buffer->undo->change_mode_group = 0; + e->op_range = insert_range; + e->cursor_range = cursor_range; + e->mode = buffer->state->mode; + e->type = type; + if (e->text != NULL) + lbuf_cpy(e->text, text); + else + e->text = lbuf_dup(text); +} + +void +ledit_push_undo_insert( + ledit_buffer *buffer, lbuf *text, + ledit_range insert_range, + ledit_range cursor_range, + int start_group) { + push_undo( + buffer, text, insert_range, + cursor_range, start_group, UNDO_INSERT + ); +} + +void +ledit_push_undo_delete( + ledit_buffer *buffer, lbuf *text, + ledit_range insert_range, + ledit_range cursor_range, + int start_group) { + push_undo( + buffer, text, insert_range, + cursor_range, start_group, UNDO_DELETE + ); +} + +ledit_undo_status +ledit_undo(ledit_buffer *buffer) { + undo_elem *e; + ledit_undo_stack *s = buffer->undo; + if (s->cur < 0) + return UNDO_OLDEST_CHANGE; + int group = s->stack[s->cur].group; + int mode_group = s->stack[s->cur].mode_group; + int min_line = buffer->lines_num - 1; + int mode_group_same = 0; + while (s->cur >= 0 && + (s->stack[s->cur].group == group || (mode_group_same = + ((buffer->state->mode == NORMAL || + buffer->state->mode == VISUAL) && + s->stack[s->cur].mode == INSERT && + s->stack[s->cur].mode_group == mode_group)))) { + e = &s->stack[s->cur]; + /* if the mode group is the same, we need to update the group, + otherwise it can happen that some iterations are performed + because the mode group (but not the normal group) is the + same, and then the next normal group is also undone because + it has the same group id as the original group here */ + if (mode_group_same) + group = e->group; + switch (e->type) { + case UNDO_INSERT: + /* FIXME: should the paste buffer also be modified? */ + ledit_delete_range_base( + buffer, 0, + e->op_range.line1, e->op_range.byte1, + e->op_range.line2, e->op_range.byte2, + NULL, NULL, NULL, NULL + ); + break; + case UNDO_DELETE: + ledit_insert_text_with_newlines_base( + buffer, e->op_range.line1, e->op_range.byte1, + e->text->text, e->text->len, NULL, NULL + ); + break; + default: + fprintf(stderr, "Error with undo. This should not happen. Fix the code please.\n"); + break; + } + /* FIXME: make sure this is always sorted already */ + if (e->op_range.line1 < min_line) + min_line = e->op_range.line1; + s->cur--; + buffer->cur_line = e->cursor_range.line1; + buffer->cur_index = e->cursor_range.byte1; + } + if (buffer->state->mode == NORMAL) { + buffer->cur_index = ledit_get_legal_normal_pos( + buffer, buffer->cur_line, buffer->cur_index + ); + } + ledit_recalc_from_line(buffer, min_line > 0 ? min_line - 1 : min_line); + return UNDO_NORMAL; +} + +ledit_undo_status +ledit_redo(ledit_buffer *buffer) { + undo_elem *e; + ledit_undo_stack *s = buffer->undo; + if (s->cur >= s->len - 1) + return UNDO_NEWEST_CHANGE; + s->cur++; + int group = s->stack[s->cur].group; + int mode_group = s->stack[s->cur].mode_group; + int min_line = buffer->lines_num - 1; + int mode_group_same = 0; + while (s->cur < s->len && + (s->stack[s->cur].group == group || (mode_group_same = + ((buffer->state->mode == NORMAL || + buffer->state->mode == VISUAL) && + s->stack[s->cur].mode == INSERT && + s->stack[s->cur].mode_group == mode_group)))) { + e = &s->stack[s->cur]; + if (mode_group_same) + group = e->group; + switch (e->type) { + case UNDO_INSERT: + ledit_insert_text_with_newlines_base( + buffer, e->op_range.line1, e->op_range.byte1, + e->text->text, e->text->len, NULL, NULL + ); + break; + case UNDO_DELETE: + ledit_delete_range_base( + buffer, 0, + e->op_range.line1, e->op_range.byte1, + e->op_range.line2, e->op_range.byte2, + NULL, NULL, NULL, NULL + ); + break; + default: + fprintf(stderr, "Error with redo. This should not happen. Fix the code please.\n"); + break; + } + if (e->op_range.line1 < min_line) + min_line = e->op_range.line1; + s->cur++; + buffer->cur_line = e->cursor_range.line2; + buffer->cur_index = e->cursor_range.byte2; + } + s->cur--; + if (buffer->state->mode == NORMAL) { + buffer->cur_index = ledit_get_legal_normal_pos( + buffer, buffer->cur_line, buffer->cur_index + ); + } + ledit_recalc_from_line(buffer, min_line > 0 ? min_line - 1 : min_line); + return UNDO_NORMAL; +} diff --git a/undo.h b/undo.h @@ -0,0 +1,23 @@ +typedef enum { + UNDO_NORMAL, + UNDO_OLDEST_CHANGE, + UNDO_NEWEST_CHANGE +} ledit_undo_status; + +void ledit_init_undo_stack(ledit_buffer *buffer); +void ledit_destroy_undo_stack(ledit_buffer *buffer); +ledit_undo_status ledit_undo(ledit_buffer *buffer); +ledit_undo_status ledit_redo(ledit_buffer *buffer); +void ledit_push_undo_insert( + ledit_buffer *buffer, lbuf *text, + ledit_range insert_range, + ledit_range cursor_range, + int start_group +); +void ledit_push_undo_delete( + ledit_buffer *buffer, lbuf *text, + ledit_range insert_range, + ledit_range cursor_range, + int start_group +); +void ledit_change_mode_group(ledit_buffer *buffer);