From c2976cea9bbca465712534b7e523783e2ccc6c6e Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Tue, 7 Feb 2023 14:06:53 +0100 Subject: Fix text to work more like GNU Emacs This means that empty lines are not added until they have content. --- src/buffer.c | 29 ++++++++++-- src/buffer.h | 5 ++ src/minibuffer.c | 15 +++--- src/text.c | 138 ++++++++++++++++++++++++++++++++++--------------------- src/text.h | 1 + test/text.c | 37 ++++++++++++--- 6 files changed, 153 insertions(+), 72 deletions(-) diff --git a/src/buffer.c b/src/buffer.c index 6d3f3d9..3812386 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -176,7 +176,7 @@ bool movev(struct buffer *buffer, int rowdelta) { } // move dot `coldelta` chars -void moveh(struct buffer *buffer, int coldelta) { +bool moveh(struct buffer *buffer, int coldelta) { int64_t new_col = (int64_t)buffer->dot.col + coldelta; if (new_col > (int64_t)text_line_length(buffer->text, buffer->dot.line)) { @@ -186,10 +186,14 @@ void moveh(struct buffer *buffer, int coldelta) { } else if (new_col < 0) { if (movev(buffer, -1)) { buffer->dot.col = text_line_length(buffer->text, buffer->dot.line); + } else { + return false; } } else { buffer->dot.col = new_col; } + + return true; } struct region { @@ -341,8 +345,9 @@ void buffer_backward_delete_char(struct buffer *buffer) { return; } - moveh(buffer, -1); - buffer_forward_delete_char(buffer); + if (moveh(buffer, -1)) { + buffer_forward_delete_char(buffer); + } } void buffer_backward_char(struct buffer *buffer) { moveh(buffer, -1); } @@ -741,6 +746,13 @@ void linenum_render_hook(struct text_chunk *line_data, uint32_t line, (uint8_t *)" ", 1); } +void clear_empty_linenum_lines(uint32_t line, struct command_list *commands, + void *userdata) { + struct linenumdata *data = (struct linenumdata *)userdata; + uint32_t longest_nchars = data->longest_nchars; + command_list_draw_repeated(commands, 0, line, ' ', longest_nchars + 2); +} + struct update_hook_result buffer_linenum_hook(struct buffer *buffer, struct command_list *commands, uint32_t width, uint32_t height, @@ -773,7 +785,9 @@ struct update_hook_result buffer_linenum_hook(struct buffer *buffer, struct update_hook_result res = {0}; res.margins.left = longest_nchars + 2; res.line_render_hook.callback = linenum_render_hook; + res.line_render_hook.empty_callback = clear_empty_linenum_lines; res.line_render_hook.userdata = &linenum_data; + return res; } @@ -845,7 +859,14 @@ void buffer_update(struct buffer *buffer, uint32_t width, uint32_t height, uint32_t nlines = text_num_lines(buffer->text); for (uint32_t linei = nlines - buffer->scroll.line + total_margins.top; linei < height; ++linei) { - command_list_draw_repeated(commands, 0, linei, ' ', total_width); + + for (uint32_t hooki = 0; hooki < nlinehooks; ++hooki) { + struct line_render_hook *hook = &line_hooks[hooki]; + hook->empty_callback(linei, commands, hook->userdata); + } + + command_list_draw_repeated(commands, total_margins.left, linei, ' ', + total_width - total_margins.left); } // update the visual cursor position diff --git a/src/buffer.h b/src/buffer.h index 7a7fb84..afb32b1 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -23,6 +23,10 @@ struct margin { typedef void (*line_render_cb)(struct text_chunk *line_data, uint32_t line, struct command_list *commands, void *userdata); +typedef void (*line_render_empty_cb)(uint32_t line, + struct command_list *commands, + void *userdata); + /** * A line render hook * @@ -30,6 +34,7 @@ typedef void (*line_render_cb)(struct text_chunk *line_data, uint32_t line, */ struct line_render_hook { line_render_cb callback; + line_render_empty_cb empty_callback; void *userdata; }; diff --git a/src/minibuffer.c b/src/minibuffer.c index e057262..63cc5a8 100644 --- a/src/minibuffer.c +++ b/src/minibuffer.c @@ -17,12 +17,10 @@ static struct minibuffer { struct keymap keymap; } g_minibuffer = {0}; -void draw_prompt(struct text_chunk *line_data, uint32_t line, - struct command_list *commands, void *userdata) { +void draw_prompt(struct command_list *commands, void *userdata) { uint32_t len = strlen(g_minibuffer.prompt); command_list_set_index_color_fg(commands, 4); - command_list_draw_text(commands, 0, line, (uint8_t *)g_minibuffer.prompt, - len); + command_list_draw_text(commands, 0, 0, (uint8_t *)g_minibuffer.prompt, len); command_list_reset_color(commands); } @@ -85,9 +83,9 @@ struct update_hook_result update(struct buffer *buffer, } struct update_hook_result res = {0}; - if (g_minibuffer.prompt_active) { - res.margins.left = strlen(g_minibuffer.prompt); - res.line_render_hook.callback = draw_prompt; + if (mb->prompt_active) { + res.margins.left = strlen(mb->prompt); + draw_prompt(commands, NULL); } return res; @@ -150,8 +148,7 @@ void minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...) { } minibuffer_clear(); - // make sure we have a line - buffer_add_text(g_minibuffer.buffer, (uint8_t *)"", 0); + g_minibuffer.prompt_active = true; g_minibuffer.prompt_command_ctx = command_ctx; diff --git a/src/text.c b/src/text.c index c733af2..04efcaa 100644 --- a/src/text.c +++ b/src/text.c @@ -1,10 +1,12 @@ #include "text.h" +#include #include #include #include #include "display.h" +#include "signal.h" #include "utf8.h" enum flags { @@ -82,28 +84,66 @@ uint32_t text_byteindex_to_col(struct text *text, uint32_t line, return byteidx_to_charidx(&text->lines[line], byteindex); } -void insert_at_col(struct line *line, uint32_t col, uint8_t *text, uint32_t len, - uint32_t nchars) { +void append_empty_lines(struct text *text, uint32_t numlines) { + + if (text->nlines + numlines >= text->capacity) { + text->capacity += text->capacity + numlines > text->capacity * 2 + ? numlines + 1 + : text->capacity; + text->lines = realloc(text->lines, sizeof(struct line) * text->capacity); + } + + for (uint32_t i = 0; i < numlines; ++i) { + struct line *nline = &text->lines[text->nlines]; + nline->data = NULL; + nline->nbytes = 0; + nline->nchars = 0; + nline->flags = 0; + + ++text->nlines; + } + + if (text->nlines > text->capacity) { + printf("text->nlines: %d, text->capacity: %d\n", text->nlines, + text->capacity); + raise(SIGTRAP); + } +} + +void ensure_line(struct text *text, uint32_t line) { + if (line >= text->nlines) { + append_empty_lines(text, line - text->nlines + 1); + } +} + +// It is assumed that `data` does not contain any \n, that is handled by +// higher-level functions +void insert_at(struct text *text, uint32_t line, uint32_t col, uint8_t *data, + uint32_t len, uint32_t nchars) { if (len == 0) { return; } - line->nbytes += len; - line->nchars += nchars; - line->flags = LineChanged; - line->data = realloc(line->data, line->nbytes); + ensure_line(text, line); + + struct line *l = &text->lines[line]; - uint32_t bytei = charidx_to_byteidx(line, col); + l->nbytes += len; + l->nchars += nchars; + l->flags = LineChanged; + l->data = realloc(l->data, l->nbytes); + + uint32_t bytei = charidx_to_byteidx(l, col); // move following bytes out of the way - if (bytei + len < line->nbytes) { + if (bytei + len < l->nbytes) { uint32_t start = bytei + len; - memmove(line->data + start, line->data + bytei, line->nbytes - start); + memmove(l->data + start, l->data + bytei, l->nbytes - start); } // insert new chars - memcpy(line->data + bytei, text, len); + memcpy(l->data + bytei, data, len); } uint32_t text_line_length(struct text *text, uint32_t lineidx) { @@ -164,35 +204,22 @@ void shift_lines(struct text *text, uint32_t start, int32_t direction) { memmove(dest, src, nlines * sizeof(struct line)); } -void append_empty_lines(struct text *text, uint32_t numlines) { - - for (uint32_t i = 0; i < numlines; ++i) { - struct line *nline = &text->lines[text->nlines]; - nline->data = NULL; - nline->nbytes = 0; - nline->nchars = 0; - nline->flags = 0; - - ++text->nlines; - } -} - void new_line_at(struct text *text, uint32_t line, uint32_t col) { - if (text->nlines == text->capacity) { - text->capacity *= 2; - text->lines = realloc(text->lines, sizeof(struct line) * text->capacity); - } + ensure_line(text, line); + + uint32_t newline = line + 1; + bool has_newline = col < text->lines[line].nchars || newline < text->nlines; + append_empty_lines(text, has_newline ? 1 : 0); - append_empty_lines(text, 1); mark_lines_changed(text, line, text->nlines - line); - // move following lines out of the way - shift_lines(text, line + 1, 1); + // move following lines out of the way, if there are any + if (newline + 1 < text->nlines) { + shift_lines(text, newline, 1); + } // split line if needed - struct line *pl = &text->lines[line]; - struct line *cl = &text->lines[line + 1]; - split_line(col, pl, cl); + split_line(col, &text->lines[line], &text->lines[newline]); } void delete_line(struct text *text, uint32_t line) { @@ -201,10 +228,11 @@ void delete_line(struct text *text, uint32_t line) { } mark_lines_changed(text, line, text->nlines - line); + free(text->lines[line].data); text->lines[line].data = NULL; - if (text->nlines > 1) { + if (line + 1 < text->nlines) { shift_lines(text, line + 1, -1); } @@ -226,24 +254,23 @@ void text_insert_at(struct text *text, uint32_t line, uint32_t col, uint8_t *bytes, uint32_t nbytes, uint32_t *lines_added, uint32_t *cols_added) { uint32_t linelen = 0, start_line = line; - if (start_line >= text->nlines) { - append_empty_lines(text, start_line - text->nlines + 1); - } - *cols_added = 0; + *cols_added = 0; for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { uint8_t byte = bytes[bytei]; if (byte == '\n') { uint8_t *line_data = bytes + (bytei - linelen); uint32_t nchars = utf8_nchars(line_data, linelen); - insert_at_col(&text->lines[line], col, line_data, linelen, nchars); - col += nchars; - new_line_at(text, line, col); - ++line; + insert_at(text, line, col, line_data, linelen, nchars); + + if (linelen == 0) { + new_line_at(text, line, col); + } - col = text_line_length(text, line); + ++line; linelen = 0; + col = 0; } else { ++linelen; } @@ -253,7 +280,7 @@ void text_insert_at(struct text *text, uint32_t line, uint32_t col, if (linelen > 0) { uint8_t *line_data = bytes + (nbytes - linelen); uint32_t nchars = utf8_nchars(line_data, linelen); - insert_at_col(&text->lines[line], col, line_data, linelen, nchars); + insert_at(text, line, col, line_data, linelen, nchars); *cols_added = nchars; } @@ -285,8 +312,8 @@ void text_delete(struct text *text, uint32_t start_line, uint32_t start_col, lastline->nbytes - bytei); } else { // otherwise we actually have to copy from the last line - insert_at_col(firstline, start_col, lastline->data + bytei, - lastline->nbytes - bytei, lastline->nchars - end_col); + insert_at(text, start_line, start_col, lastline->data + bytei, + lastline->nbytes - bytei, lastline->nchars - end_col); } firstline->nchars = start_col + (lastline->nchars - end_col); @@ -299,6 +326,11 @@ void text_delete(struct text *text, uint32_t start_line, uint32_t start_col, linei > start_line; --linei) { delete_line(text, linei); } + + // if this is the last line in the buffer, and it turns out empty, remove it + if (firstline->nbytes == 0 && start_line == text->nlines - 1) { + delete_line(text, start_line); + } } void text_for_each_chunk(struct text *text, chunk_cb callback, void *userdata) { @@ -334,7 +366,7 @@ struct text_chunk text_get_line(struct text *text, uint32_t line) { struct copy_cmd { uint32_t line; - uint32_t byteindex; + uint32_t byteoffset; uint32_t nbytes; }; @@ -352,7 +384,7 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, return (struct text_chunk){0}; } - // handle deletion of newlines + // handle copying of newlines if (end_col > last_line->nchars) { ++end_line; end_col = 0; @@ -370,7 +402,7 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, struct copy_cmd *cmd = ©_cmds[line - start_line]; cmd->line = line; - cmd->byteindex = 0; + cmd->byteoffset = 0; cmd->nbytes = l->nbytes; } @@ -378,7 +410,7 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, struct copy_cmd *cmd_first = ©_cmds[0]; uint32_t byteoff = utf8_nbytes(first_line->data, first_line->nbytes, start_col); - cmd_first->byteindex += byteoff; + cmd_first->byteoffset += byteoff; cmd_first->nbytes -= byteoff; total_bytes -= byteoff; total_chars -= start_col; @@ -390,13 +422,14 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, total_bytes -= (last_line->nbytes - byteindex); total_chars -= (last_line->nchars - end_col); - uint8_t *data = (uint8_t *)malloc(total_bytes + end_line - start_line); + uint8_t *data = (uint8_t *)malloc( + total_bytes + /* nr of newline chars */ (end_line - start_line)); // copy data for (uint32_t cmdi = 0, curr = 0; cmdi < nlines; ++cmdi) { struct copy_cmd *c = ©_cmds[cmdi]; struct line *l = &text->lines[c->line]; - memcpy(data + curr, l->data + c->byteindex, c->nbytes); + memcpy(data + curr, l->data + c->byteoffset, c->nbytes); curr += c->nbytes; if (cmdi != (nlines - 1)) { @@ -413,6 +446,7 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, .line = 0, .nbytes = total_bytes, .nchars = total_chars, + .allocated = true, }; } diff --git a/src/text.h b/src/text.h index 12fe576..fbee89b 100644 --- a/src/text.h +++ b/src/text.h @@ -37,6 +37,7 @@ struct text_chunk { uint32_t nbytes; uint32_t nchars; uint32_t line; + bool allocated; }; typedef void (*chunk_cb)(struct text_chunk *chunk, void *userdata); diff --git a/test/text.c b/test/text.c index 4ae5927..9c8d825 100644 --- a/test/text.c +++ b/test/text.c @@ -12,12 +12,15 @@ void assert_line_equal(struct text_chunk *line) {} void test_add_text() { uint32_t lines_added, cols_added; - struct text *t = text_create(10); + /* use a silly small initial capacity to test re-alloc */ + struct text *t = text_create(1); + const char *txt = "This is line 1\n"; text_insert_at(t, 0, 0, (uint8_t *)txt, strlen(txt), &lines_added, &cols_added); - ASSERT(text_num_lines(t) == 2, - "Expected text to have two lines after insertion"); + ASSERT( + text_num_lines(t) == 1, + "Expected text to have one line after insertion, since line 2 is empty"); ASSERT(text_line_size(t, 0) == 14 && text_line_length(t, 0) == 14, "Expected line 1 to have 14 chars and 14 bytes"); @@ -27,8 +30,8 @@ void test_add_text() { const char *txt2 = "This is line 2\n"; text_insert_at(t, 1, 0, (uint8_t *)txt2, strlen(txt2), &lines_added, &cols_added); - ASSERT(text_num_lines(t) == 3, - "Expected text to have three lines after second insertion"); + ASSERT(text_num_lines(t) == 2, + "Expected text to have two lines after second insertion"); ASSERT_STR_EQ((const char *)text_get_line(t, 1).text, "This is line 2", "Expected line 2 to be line 2"); @@ -36,13 +39,33 @@ void test_add_text() { const char *txt3 = " "; text_insert_at(t, 0, 0, (uint8_t *)txt3, strlen(txt3), &lines_added, &cols_added); - ASSERT(text_num_lines(t) == 3, - "Expected text to have three lines after second insertion"); + ASSERT(text_num_lines(t) == 2, + "Expected text to have two lines after second insertion"); ASSERT_STR_EQ((const char *)text_get_line(t, 0).text, " This is line 1", "Expected line 1 to be indented"); ASSERT_STR_EQ((const char *)text_get_line(t, 1).text, "This is line 2", "Expected line 2 to be line 2 still"); + // insert newline in middle of line + text_insert_at(t, 1, 4, (uint8_t *)"\n", 1, &lines_added, &cols_added); + ASSERT(text_num_lines(t) == 3, + "Expected text to have three lines after inserting a new line"); + ASSERT_STR_EQ((const char *)text_get_line(t, 1).text, "This", + "Expected line 2 to be split"); + ASSERT_STR_EQ((const char *)text_get_line(t, 2).text, " is line 2", + "Expected line 2 to be split"); + + // insert newline before line 1 + text_insert_at(t, 1, 0, (uint8_t *)"\n", 1, &lines_added, &cols_added); + ASSERT( + text_num_lines(t) == 4, + "Expected to have four lines after adding an empty line in the middle"); + ASSERT(text_line_length(t, 1) == 0, "Expected line 2 to be empty"); + ASSERT_STR_EQ((const char *)text_get_line(t, 2).text, "This", + "Expected line 3 to be previous line 2"); + ASSERT_STR_EQ((const char *)text_get_line(t, 3).text, " is line 2", + "Expected line 4 to be previous line 3"); + text_destroy(t); } -- cgit v1.2.3