diff options
Diffstat (limited to 'src/dged/buffer.c')
| -rw-r--r-- | src/dged/buffer.c | 1110 |
1 files changed, 1110 insertions, 0 deletions
diff --git a/src/dged/buffer.c b/src/dged/buffer.c new file mode 100644 index 0000000..25a8a4a --- /dev/null +++ b/src/dged/buffer.c @@ -0,0 +1,1110 @@ +#include "buffer.h" +#include "binding.h" +#include "bits/stdint-uintn.h" +#include "dged/vec.h" +#include "display.h" +#include "errno.h" +#include "lang.h" +#include "minibuffer.h" +#include "reactor.h" +#include "settings.h" +#include "utf8.h" + +#include <fcntl.h> +#include <libgen.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> +#include <wchar.h> + +struct modeline { + uint8_t *buffer; + uint32_t sz; +}; + +#define KILL_RING_SZ 64 +static struct kill_ring { + struct text_chunk buffer[KILL_RING_SZ]; + struct buffer_location last_paste; + bool paste_up_to_date; + uint32_t curr_idx; + uint32_t paste_idx; +} g_kill_ring = {.curr_idx = 0, + .buffer = {0}, + .last_paste = {0}, + .paste_idx = 0, + .paste_up_to_date = false}; + +#define MAX_CREATE_HOOKS 32 +static struct create_hook { + create_hook_cb callback; + void *userdata; +} g_create_hooks[MAX_CREATE_HOOKS]; +static uint32_t g_num_create_hooks = 0; + +struct update_hook_result buffer_linenum_hook(struct buffer_view *view, + struct command_list *commands, + uint32_t width, uint32_t height, + uint64_t frame_time, + void *userdata); + +struct update_hook_result buffer_modeline_hook(struct buffer_view *view, + struct command_list *commands, + uint32_t width, uint32_t height, + uint64_t frame_time, + void *userdata); + +struct buffer_view buffer_view_create(struct buffer *buffer, bool modeline, + bool line_numbers) { + struct buffer_view view = { + .dot = {0}, + .mark = {0}, + .mark_set = false, + .scroll = {0}, + .buffer = buffer, + .modeline = NULL, + .line_numbers = line_numbers, + }; + + if (modeline) { + view.modeline = calloc(1, sizeof(struct modeline)); + view.modeline->buffer = malloc(1024); + view.modeline->sz = 1024; + view.modeline->buffer[0] = '\0'; + } + + return view; +} + +struct buffer_view buffer_view_clone(struct buffer_view *view) { + struct buffer_view c = { + .dot = view->dot, + .mark = view->mark, + .mark_set = view->mark_set, + .scroll = view->scroll, + .buffer = view->buffer, + .modeline = NULL, + .line_numbers = view->line_numbers, + }; + + if (view->modeline) { + c.modeline = calloc(1, sizeof(struct modeline)); + c.modeline->buffer = malloc(view->modeline->sz); + memcpy(c.modeline->buffer, view->modeline->buffer, view->modeline->sz); + } + + return c; +} + +void buffer_view_destroy(struct buffer_view *view) { + if (view->modeline != NULL) { + free(view->modeline->buffer); + free(view->modeline); + } +} + +uint32_t buffer_add_create_hook(create_hook_cb hook, void *userdata) { + if (g_num_create_hooks < MAX_CREATE_HOOKS) { + g_create_hooks[g_num_create_hooks] = (struct create_hook){ + .callback = hook, + .userdata = userdata, + }; + ++g_num_create_hooks; + } + + return g_num_create_hooks - 1; +} + +struct buffer buffer_create(char *name) { + struct buffer b = (struct buffer){ + .filename = NULL, + .name = strdup(name), + .text = text_create(10), + .modified = false, + .readonly = false, + .lang = lang_from_id("fnd"), + }; + + undo_init(&b.undo, 100); + + for (uint32_t hooki = 0; hooki < g_num_create_hooks; ++hooki) { + g_create_hooks[hooki].callback(&b, g_create_hooks[hooki].userdata); + } + + return b; +} + +void buffer_destroy(struct buffer *buffer) { + text_destroy(buffer->text); + buffer->text = NULL; + + free(buffer->name); + buffer->name = NULL; + + free(buffer->filename); + buffer->filename = NULL; + + undo_destroy(&buffer->undo); +} + +void buffer_clear(struct buffer_view *view) { + text_clear(view->buffer->text); + view->dot.col = view->dot.line = 0; +} + +void buffer_static_init() { + settings_register_setting( + "editor.tab-width", + (struct setting_value){.type = Setting_Number, .number_value = 4}); + + settings_register_setting( + "editor.show-whitespace", + (struct setting_value){.type = Setting_Bool, .bool_value = true}); +} + +void buffer_static_teardown() { + for (uint32_t i = 0; i < KILL_RING_SZ; ++i) { + if (g_kill_ring.buffer[i].allocated) { + free(g_kill_ring.buffer[i].text); + } + } +} + +bool buffer_is_empty(struct buffer *buffer) { + return text_num_lines(buffer->text) == 0; +} + +bool buffer_is_modified(struct buffer *buffer) { return buffer->modified; } + +bool buffer_is_readonly(struct buffer *buffer) { return buffer->readonly; } +void buffer_set_readonly(struct buffer *buffer, bool readonly) { + buffer->readonly = readonly; +} + +void delete_with_undo(struct buffer *buffer, struct buffer_location start, + struct buffer_location end) { + if (buffer->readonly) { + minibuffer_echo_timeout(4, "buffer is read-only"); + return; + } + + struct text_chunk txt = + text_get_region(buffer->text, start.line, start.col, end.line, end.col); + + undo_push_delete( + &buffer->undo, + (struct undo_delete){.data = txt.text, + .nbytes = txt.nbytes, + .pos = {.row = start.line, .col = start.col}}); + undo_push_boundary(&buffer->undo, + (struct undo_boundary){.save_point = false}); + + text_delete(buffer->text, start.line, start.col, end.line, end.col); + buffer->modified = true; +} + +void buffer_goto_beginning(struct buffer_view *view) { + view->dot.col = 0; + view->dot.line = 0; +} + +void buffer_goto_end(struct buffer_view *view) { + view->dot.line = text_num_lines(view->buffer->text); + view->dot.col = 0; +} + +bool movev(struct buffer_view *view, int rowdelta) { + int64_t new_line = (int64_t)view->dot.line + rowdelta; + + if (new_line < 0) { + view->dot.line = 0; + return false; + } else if (new_line > text_num_lines(view->buffer->text)) { + view->dot.line = text_num_lines(view->buffer->text); + return false; + } else { + view->dot.line = (uint32_t)new_line; + + // make sure column stays on the line + uint32_t linelen = text_line_length(view->buffer->text, view->dot.line); + view->dot.col = view->dot.col > linelen ? linelen : view->dot.col; + return true; + } +} + +// move dot `coldelta` chars +bool moveh(struct buffer_view *view, int coldelta) { + int64_t new_col = (int64_t)view->dot.col + coldelta; + + if (new_col > (int64_t)text_line_length(view->buffer->text, view->dot.line)) { + if (movev(view, 1)) { + view->dot.col = 0; + } + } else if (new_col < 0) { + if (movev(view, -1)) { + view->dot.col = text_line_length(view->buffer->text, view->dot.line); + } else { + return false; + } + } else { + view->dot.col = new_col; + } + + return true; +} + +void buffer_goto(struct buffer_view *view, uint32_t line, uint32_t col) { + int64_t linedelta = (int64_t)line - (int64_t)view->dot.line; + movev(view, linedelta); + + int64_t coldelta = (int64_t)col - (int64_t)view->dot.col; + moveh(view, coldelta); +} + +struct region { + struct buffer_location begin; + struct buffer_location end; +}; + +struct region to_region(struct buffer_location dot, + struct buffer_location mark) { + struct region reg = {.begin = mark, .end = dot}; + + if (dot.line < mark.line || (dot.line == mark.line && dot.col < mark.col)) { + reg.begin = dot; + reg.end = mark; + } + + return reg; +} + +struct region buffer_get_region(struct buffer_view *view) { + return to_region(view->dot, view->mark); +} + +bool buffer_region_has_size(struct buffer_view *view) { + return view->mark_set && + (labs((int64_t)view->mark.line - (int64_t)view->dot.line) + + labs((int64_t)view->mark.col - (int64_t)view->dot.col)) > 0; +} + +struct text_chunk *copy_region(struct buffer *buffer, struct region region) { + struct text_chunk *curr = &g_kill_ring.buffer[g_kill_ring.curr_idx]; + g_kill_ring.curr_idx = g_kill_ring.curr_idx + 1 % KILL_RING_SZ; + + if (curr->allocated) { + free(curr->text); + } + + struct text_chunk txt = + text_get_region(buffer->text, region.begin.line, region.begin.col, + region.end.line, region.end.col); + *curr = txt; + return curr; +} + +void buffer_copy(struct buffer_view *view) { + if (buffer_region_has_size(view)) { + struct region reg = buffer_get_region(view); + struct text_chunk *curr = copy_region(view->buffer, reg); + buffer_clear_mark(view); + } +} + +void paste(struct buffer_view *view, uint32_t ring_idx) { + if (ring_idx > 0) { + struct text_chunk *curr = &g_kill_ring.buffer[ring_idx - 1]; + if (curr->text != NULL) { + g_kill_ring.last_paste = view->mark_set ? view->mark : view->dot; + buffer_add_text(view, curr->text, curr->nbytes); + g_kill_ring.paste_up_to_date = true; + } + } +} + +void buffer_paste(struct buffer_view *view) { + g_kill_ring.paste_idx = g_kill_ring.curr_idx; + paste(view, g_kill_ring.curr_idx); +} + +void buffer_paste_older(struct buffer_view *view) { + if (g_kill_ring.paste_up_to_date) { + + // remove previous paste + struct text_chunk *curr = &g_kill_ring.buffer[g_kill_ring.curr_idx]; + delete_with_undo(view->buffer, g_kill_ring.last_paste, view->dot); + + // place ourselves right + view->dot = g_kill_ring.last_paste; + + // paste older + if (g_kill_ring.paste_idx - 1 > 0) { + --g_kill_ring.paste_idx; + } else { + g_kill_ring.paste_idx = g_kill_ring.curr_idx; + } + + paste(view, g_kill_ring.paste_idx); + + } else { + buffer_paste(view); + } +} + +void buffer_cut(struct buffer_view *view) { + if (buffer_region_has_size(view)) { + struct region reg = buffer_get_region(view); + copy_region(view->buffer, reg); + delete_with_undo(view->buffer, reg.begin, reg.end); + buffer_clear_mark(view); + view->dot = reg.begin; + } +} + +bool maybe_delete_region(struct buffer_view *view) { + if (buffer_region_has_size(view)) { + struct region reg = buffer_get_region(view); + delete_with_undo(view->buffer, reg.begin, reg.end); + buffer_clear_mark(view); + view->dot = reg.begin; + return true; + } + + return false; +} + +void buffer_kill_line(struct buffer_view *view) { + uint32_t nchars = + text_line_length(view->buffer->text, view->dot.line) - view->dot.col; + if (nchars == 0) { + nchars = 1; + } + + struct region reg = { + .begin = view->dot, + .end = + { + .line = view->dot.line, + .col = view->dot.col + nchars, + }, + }; + copy_region(view->buffer, reg); + delete_with_undo(view->buffer, view->dot, + (struct buffer_location){ + .line = view->dot.line, + .col = view->dot.col + nchars, + }); +} + +void buffer_forward_delete_char(struct buffer_view *view) { + if (maybe_delete_region(view)) { + return; + } + + delete_with_undo(view->buffer, view->dot, + (struct buffer_location){ + .line = view->dot.line, + .col = view->dot.col + 1, + }); +} + +void buffer_backward_delete_char(struct buffer_view *view) { + if (maybe_delete_region(view)) { + return; + } + + if (moveh(view, -1)) { + buffer_forward_delete_char(view); + } +} + +void buffer_backward_char(struct buffer_view *view) { moveh(view, -1); } +void buffer_forward_char(struct buffer_view *view) { moveh(view, 1); } + +struct buffer_location find_next(struct buffer_view *view, uint8_t chars[], + uint32_t nchars, int direction) { + struct text_chunk line = text_get_line(view->buffer->text, view->dot.line); + int64_t bytei = + text_col_to_byteindex(view->buffer->text, view->dot.line, view->dot.col); + while (bytei < line.nbytes && bytei > 0 && + (line.text[bytei] == ' ' || line.text[bytei] == '.')) { + bytei += direction; + } + + for (; bytei < line.nbytes && bytei > 0; bytei += direction) { + uint8_t b = line.text[bytei]; + if (b == ' ' || b == '.') { + break; + } + } + + uint32_t target_col = + text_byteindex_to_col(view->buffer->text, view->dot.line, bytei); + return (struct buffer_location){.line = view->dot.line, .col = target_col}; +} + +void buffer_forward_word(struct buffer_view *view) { + moveh(view, 1); + uint8_t chars[] = {' ', '.'}; + view->dot = find_next(view, chars, 2, 1); +} + +void buffer_backward_word(struct buffer_view *view) { + moveh(view, -1); + uint8_t chars[] = {' ', '.'}; + view->dot = find_next(view, chars, 2, -1); +} + +void buffer_backward_line(struct buffer_view *view) { movev(view, -1); } +void buffer_forward_line(struct buffer_view *view) { movev(view, 1); } + +void buffer_end_of_line(struct buffer_view *view) { + view->dot.col = text_line_length(view->buffer->text, view->dot.line); +} + +void buffer_beginning_of_line(struct buffer_view *view) { view->dot.col = 0; } + +struct buffer buffer_from_file(char *filename) { + struct buffer b = buffer_create(basename((char *)filename)); + b.filename = strdup(filename); + if (access(b.filename, F_OK) == 0) { + FILE *file = fopen(filename, "r"); + + if (file == NULL) { + minibuffer_echo("Error opening %s: %s", filename, strerror(errno)); + return b; + } + + while (true) { + uint8_t buff[4096]; + int bytes = fread(buff, 1, 4096, file); + if (bytes > 0) { + uint32_t ignore; + text_append(b.text, buff, bytes, &ignore, &ignore); + } else if (bytes == 0) { + break; // EOF + } else { + minibuffer_echo("error reading from %s: %s", filename, strerror(errno)); + fclose(file); + return b; + } + } + + fclose(file); + } + + const char *ext = strrchr(b.filename, '.'); + if (ext != NULL) { + b.lang = lang_from_extension(ext + 1); + } + undo_push_boundary(&b.undo, (struct undo_boundary){.save_point = true}); + return b; +} + +void write_line(struct text_chunk *chunk, void *userdata) { + FILE *file = (FILE *)userdata; + fwrite(chunk->text, 1, chunk->nbytes, file); + + // final newline is not optional! + fputc('\n', file); +} + +void buffer_to_file(struct buffer *buffer) { + if (!buffer->filename) { + minibuffer_echo("buffer \"%s\" is not associated with a file", + buffer->name); + return; + } + + if (!buffer->modified) { + minibuffer_echo_timeout(4, "buffer already saved"); + return; + } + + FILE *file = fopen(buffer->filename, "w"); + if (file == NULL) { + minibuffer_echo("failed to open file %s for writing: %s", buffer->filename, + strerror(errno)); + return; + } + + uint32_t nlines = text_num_lines(buffer->text); + struct text_chunk lastline = text_get_line(buffer->text, nlines - 1); + uint32_t nlines_to_write = lastline.nbytes == 0 ? nlines - 1 : nlines; + + text_for_each_line(buffer->text, 0, nlines_to_write, write_line, file); + minibuffer_echo_timeout(4, "wrote %d lines to %s", nlines_to_write, + buffer->filename); + fclose(file); + + buffer->modified = false; + undo_push_boundary(&buffer->undo, (struct undo_boundary){.save_point = true}); +} + +void buffer_write_to(struct buffer *buffer, const char *filename) { + buffer->filename = strdup(filename); + buffer_to_file(buffer); +} + +struct search_data { + VEC(struct match) matches; + const char *pattern; +}; + +// TODO: maybe should live in text +void search_line(struct text_chunk *chunk, void *userdata) { + struct search_data *data = (struct search_data *)userdata; + size_t pattern_len = strlen(data->pattern); + uint32_t pattern_nchars = utf8_nchars((uint8_t *)data->pattern, pattern_len); + + char *line = malloc(chunk->nbytes + 1); + memcpy(line, chunk->text, chunk->nbytes); + line[chunk->nbytes] = '\0'; + char *hit = NULL; + uint32_t byteidx = 0; + while ((hit = strstr(line + byteidx, data->pattern)) != NULL) { + byteidx = hit - line; + uint32_t begin = utf8_nchars(chunk->text, byteidx); + struct match match = (struct match){ + .begin = {.col = begin, .line = chunk->line}, + .end = {.col = begin + pattern_nchars, .line = chunk->line}, + }; + + VEC_PUSH(&data->matches, match); + + // proceed to after match + byteidx += pattern_len; + } +} + +void buffer_find(struct buffer *buffer, const char *pattern, + struct match **matches, uint32_t *nmatches) { + + struct search_data data = (struct search_data){.pattern = pattern}; + VEC_INIT(&data.matches, 16); + text_for_each_line(buffer->text, 0, text_num_lines(buffer->text), search_line, + &data); + + *matches = VEC_ENTRIES(&data.matches); + *nmatches = VEC_SIZE(&data.matches); +} + +void buffer_set_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) { + text_clear(buffer->text); + uint32_t lines, cols; + text_append(buffer->text, text, nbytes, &lines, &cols); +} + +int buffer_add_text(struct buffer_view *view, uint8_t *text, uint32_t nbytes) { + if (view->buffer->readonly) { + minibuffer_echo_timeout(4, "buffer is read-only"); + return 0; + } + + // invalidate last paste + g_kill_ring.paste_up_to_date = false; + + /* If we currently have a selection active, + * replace it with the text to insert. */ + maybe_delete_region(view); + + struct buffer_location initial = view->dot; + + uint32_t lines_added, cols_added; + text_insert_at(view->buffer->text, initial.line, initial.col, text, nbytes, + &lines_added, &cols_added); + + // move to after inserted text + movev(view, lines_added); + if (lines_added > 0) { + // does not make sense to use position from another line + view->dot.col = 0; + } + moveh(view, cols_added); + + struct buffer_location final = view->dot; + undo_push_add( + &view->buffer->undo, + (struct undo_add){.begin = {.row = initial.line, .col = initial.col}, + .end = {.row = final.line, .col = final.col}}); + + if (lines_added > 0) { + undo_push_boundary(&view->buffer->undo, + (struct undo_boundary){.save_point = false}); + } + + view->buffer->modified = true; + return lines_added; +} + +void buffer_newline(struct buffer_view *view) { + buffer_add_text(view, (uint8_t *)"\n", 1); +} + +void buffer_indent(struct buffer_view *view) { + uint32_t tab_width = view->buffer->lang.tab_width; + buffer_add_text(view, (uint8_t *)" ", + tab_width > 16 ? 16 : tab_width); +} + +uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook, + void *userdata) { + struct update_hook *h = + &buffer->update_hooks.hooks[buffer->update_hooks.nhooks]; + h->callback = hook; + h->userdata = userdata; + + ++buffer->update_hooks.nhooks; + + // TODO: cant really have this if we actually want to remove a hook + return buffer->update_hooks.nhooks - 1; +} + +void buffer_set_mark(struct buffer_view *view) { + view->mark_set ? buffer_clear_mark(view) + : buffer_set_mark_at(view, view->dot.line, view->dot.col); +} + +void buffer_clear_mark(struct buffer_view *view) { + view->mark_set = false; + minibuffer_echo_timeout(2, "mark cleared"); +} + +void buffer_set_mark_at(struct buffer_view *view, uint32_t line, uint32_t col) { + view->mark_set = true; + view->mark.line = line; + view->mark.col = col; + minibuffer_echo_timeout(2, "mark set"); +} + +void buffer_undo(struct buffer_view *view) { + struct undo_stack *undo = &view->buffer->undo; + undo_begin(undo); + + // fetch and handle records + struct undo_record *records = NULL; + uint32_t nrecords = 0; + + if (undo_current_position(undo) == INVALID_TOP) { + minibuffer_echo_timeout(4, + "no more undo information, starting from top..."); + } + + undo_next(undo, &records, &nrecords); + + undo_push_boundary(undo, (struct undo_boundary){.save_point = false}); + for (uint32_t reci = 0; reci < nrecords; ++reci) { + struct undo_record *rec = &records[reci]; + switch (rec->type) { + case Undo_Boundary: { + struct undo_boundary *b = &rec->boundary; + if (b->save_point) { + view->buffer->modified = false; + } + break; + } + case Undo_Add: { + struct undo_add *add = &rec->add; + + delete_with_undo(view->buffer, + (struct buffer_location){ + .line = add->begin.row, + .col = add->begin.col, + }, + (struct buffer_location){ + .line = add->end.row, + .col = add->end.col, + }); + + buffer_goto(view, add->begin.row, add->begin.col); + break; + } + case Undo_Delete: { + struct undo_delete *del = &rec->delete; + buffer_goto(view, del->pos.row, del->pos.col); + buffer_add_text(view, del->data, del->nbytes); + break; + } + } + } + undo_push_boundary(undo, (struct undo_boundary){.save_point = false}); + + free(records); + undo_end(undo); +} + +struct cmdbuf { + struct command_list *cmds; + struct buffer_location scroll; + uint32_t line_offset; + uint32_t left_margin; + uint32_t width; + + struct region region; + bool mark_set; + + bool show_ws; + + struct line_render_hook *line_render_hooks; + uint32_t nlinerender_hooks; +}; + +void render_line(struct text_chunk *line, void *userdata) { + struct cmdbuf *cmdbuf = (struct cmdbuf *)userdata; + uint32_t visual_line = line->line - cmdbuf->scroll.line + cmdbuf->line_offset; + + for (uint32_t hooki = 0; hooki < cmdbuf->nlinerender_hooks; ++hooki) { + struct line_render_hook *hook = &cmdbuf->line_render_hooks[hooki]; + hook->callback(line, visual_line, cmdbuf->cmds, hook->userdata); + } + + uint32_t scroll_bytes = + utf8_nbytes(line->text, line->nbytes, cmdbuf->scroll.col); + uint32_t text_nbytes_scroll = + scroll_bytes > line->nbytes ? 0 : line->nbytes - scroll_bytes; + uint8_t *text = line->text + scroll_bytes; + + // calculate how many chars we can fit in 'width' + uint32_t linewidth = cmdbuf->left_margin; + uint32_t text_nbytes = 0; + for (uint32_t bytei = 0; + bytei < text_nbytes_scroll && linewidth < cmdbuf->width; ++bytei) { + uint8_t *txt = &text[bytei]; + if (*txt == '\t') { + linewidth += 3; + } else if (utf8_byte_is_unicode_start(*txt)) { + wchar_t wc; + if (mbrtowc(&wc, (char *)txt, 6, NULL) >= 0) { + linewidth += wcwidth(wc) - 1; + } + } + + ++linewidth; + ++text_nbytes; + } + + command_list_set_show_whitespace(cmdbuf->cmds, cmdbuf->show_ws); + struct buffer_location *begin = &cmdbuf->region.begin, + *end = &cmdbuf->region.end; + + // should we draw region? + if (cmdbuf->mark_set && line->line >= begin->line && + line->line <= end->line) { + uint32_t byte_offset = 0; + uint32_t col_offset = 0; + + // draw any text on the line that should not be part of region + if (begin->line == line->line) { + if (begin->col > cmdbuf->scroll.col) { + uint32_t nbytes = + utf8_nbytes(text, text_nbytes, begin->col - cmdbuf->scroll.col); + command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin, visual_line, + text, nbytes); + + byte_offset += nbytes; + } + + col_offset = begin->col - cmdbuf->scroll.col; + } + + // activate region color + command_list_set_index_color_bg(cmdbuf->cmds, 5); + + // draw any text on line that should be part of region + if (end->line == line->line) { + if (end->col > cmdbuf->scroll.col) { + uint32_t nbytes = + utf8_nbytes(text + byte_offset, text_nbytes - byte_offset, + end->col - col_offset - cmdbuf->scroll.col); + command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin + col_offset, + visual_line, text + byte_offset, nbytes); + byte_offset += nbytes; + } + + col_offset = end->col - cmdbuf->scroll.col; + command_list_reset_color(cmdbuf->cmds); + } + + // draw rest of line + if (text_nbytes - byte_offset > 0) { + command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin + col_offset, + visual_line, text + byte_offset, + text_nbytes - byte_offset); + } + + // done rendering region + command_list_reset_color(cmdbuf->cmds); + + } else { + command_list_draw_text(cmdbuf->cmds, cmdbuf->left_margin, visual_line, text, + text_nbytes); + } + + command_list_set_show_whitespace(cmdbuf->cmds, false); + + if (linewidth < cmdbuf->width) { + command_list_draw_repeated(cmdbuf->cmds, linewidth, visual_line, ' ', + cmdbuf->width - linewidth); + } +} + +void scroll(struct buffer_view *view, int line_delta, int col_delta) { + uint32_t nlines = text_num_lines(view->buffer->text); + int64_t new_line = (int64_t)view->scroll.line + line_delta; + if (new_line >= 0 && new_line < nlines) { + view->scroll.line = (uint32_t)new_line; + } else if (new_line < 0) { + view->scroll.line = 0; + } + + int64_t new_col = (int64_t)view->scroll.col + col_delta; + if (new_col >= 0 && + new_col < text_line_length(view->buffer->text, view->dot.line)) { + view->scroll.col = (uint32_t)new_col; + } else if (new_col < 0) { + view->scroll.col = 0; + } +} + +void to_relative(struct buffer_view *view, uint32_t line, uint32_t col, + int64_t *rel_line, int64_t *rel_col) { + *rel_col = (int64_t)col - (int64_t)view->scroll.col; + *rel_line = (int64_t)line - (int64_t)view->scroll.line; +} + +uint32_t visual_dot_col(struct buffer_view *view, uint32_t dot_col) { + uint32_t visual_dot_col = dot_col; + struct text_chunk line = text_get_line(view->buffer->text, view->dot.line); + for (uint32_t bytei = 0; + bytei < + text_col_to_byteindex(view->buffer->text, view->dot.line, view->dot.col); + ++bytei) { + if (line.text[bytei] == '\t') { + visual_dot_col += 3; + } else if (utf8_byte_is_unicode_start(line.text[bytei])) { + wchar_t wc; + if (mbrtowc(&wc, (char *)line.text + bytei, 6, NULL) >= 0) { + visual_dot_col += wcwidth(wc) - 1; + } + } + } + + return visual_dot_col; +} + +void render_modeline(struct modeline *modeline, struct buffer_view *view, + struct command_list *commands, uint32_t width, + uint32_t height, uint64_t frame_time) { + char buf[width * 4]; + + static uint64_t samples[10] = {0}; + static uint32_t samplei = 0; + static uint64_t avg = 0; + + // calc a moving average with a window of the last 10 frames + ++samplei; + samplei %= 10; + avg += 0.1 * (frame_time - samples[samplei]); + samples[samplei] = frame_time; + + time_t now = time(NULL); + struct tm *lt = localtime(&now); + char left[128], right[128]; + + snprintf(left, 128, " %c%c %-16s (%d, %d) (%s)", + view->buffer->modified ? '*' : '-', + view->buffer->readonly ? '%' : '-', view->buffer->name, + view->dot.line + 1, visual_dot_col(view, view->dot.col), + view->buffer->lang.name); + snprintf(right, 128, "(%.2f ms) %02d:%02d", frame_time / 1e6, lt->tm_hour, + lt->tm_min); + + snprintf(buf, width * 4, "%s%*s%s", left, + (int)(width - (strlen(left) + strlen(right))), "", right); + + if (strcmp(buf, (char *)modeline->buffer) != 0) { + modeline->buffer = realloc(modeline->buffer, width * 4); + modeline->sz = width * 4; + strcpy((char *)modeline->buffer, buf); + } + + command_list_set_index_color_bg(commands, 8); + command_list_draw_text(commands, 0, height - 1, modeline->buffer, + strlen((char *)modeline->buffer)); + command_list_reset_color(commands); +} + +struct linenumdata { + uint32_t longest_nchars; + uint32_t dot_line; +} linenum_data; + +void linenum_render_hook(struct text_chunk *line_data, uint32_t line, + struct command_list *commands, void *userdata) { + struct linenumdata *data = (struct linenumdata *)userdata; + static char buf[16]; + command_list_set_index_color_bg(commands, 236); + command_list_set_index_color_fg( + commands, line_data->line == data->dot_line ? 253 : 244); + uint32_t chars = + snprintf(buf, 16, "%*d", data->longest_nchars + 1, line_data->line + 1); + command_list_draw_text_copy(commands, 0, line, (uint8_t *)buf, chars); + command_list_reset_color(commands); + command_list_draw_text(commands, data->longest_nchars + 1, 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); +} + +uint32_t longest_linenum(struct buffer *buffer) { + uint32_t total_lines = text_num_lines(buffer->text); + uint32_t longest_nchars = 10; + if (total_lines < 10) { + longest_nchars = 1; + } else if (total_lines < 100) { + longest_nchars = 2; + } else if (total_lines < 1000) { + longest_nchars = 3; + } else if (total_lines < 10000) { + longest_nchars = 4; + } else if (total_lines < 100000) { + longest_nchars = 5; + } else if (total_lines < 1000000) { + longest_nchars = 6; + } else if (total_lines < 10000000) { + longest_nchars = 7; + } else if (total_lines < 100000000) { + longest_nchars = 8; + } else if (total_lines < 1000000000) { + longest_nchars = 9; + } + + return longest_nchars; +} + +void buffer_update(struct buffer_view *view, uint32_t width, uint32_t height, + struct command_list *commands, uint64_t frame_time, + uint32_t *relline, uint32_t *relcol) { + if (width == 0 || height == 0) { + return; + } + + uint32_t total_width = width, total_height = height; + struct margin total_margins = {0}; + struct line_render_hook line_hooks[16 + 1]; + uint32_t nlinehooks = 0; + for (uint32_t hooki = 0; hooki < view->buffer->update_hooks.nhooks; ++hooki) { + struct update_hook *h = &view->buffer->update_hooks.hooks[hooki]; + struct update_hook_result res = + h->callback(view, commands, width, height, frame_time, h->userdata); + + if (res.line_render_hook.callback != NULL) { + line_hooks[nlinehooks] = res.line_render_hook; + ++nlinehooks; + } + + total_margins.left += res.margins.left; + total_margins.right += res.margins.right; + total_margins.bottom += res.margins.bottom; + total_margins.top += res.margins.top; + + height -= total_margins.top + total_margins.bottom; + width -= total_margins.left + total_margins.right; + } + + if (view->line_numbers) { + linenum_data.longest_nchars = longest_linenum(view->buffer); + linenum_data.dot_line = view->dot.line; + line_hooks[nlinehooks].callback = linenum_render_hook; + line_hooks[nlinehooks].empty_callback = clear_empty_linenum_lines; + line_hooks[nlinehooks].userdata = &linenum_data; + ++nlinehooks; + + total_margins.left += linenum_data.longest_nchars + 2; + } + + if (view->modeline != NULL) { + render_modeline(view->modeline, view, commands, width, height, frame_time); + total_margins.bottom += 1; + } + + height -= total_margins.top + total_margins.bottom; + width -= total_margins.left + total_margins.right; + + int64_t rel_line, rel_col; + to_relative(view, view->dot.line, view->dot.col, &rel_line, &rel_col); + int line_delta = 0, col_delta = 0; + if (rel_line < 0) { + line_delta = rel_line - ((int)height / 2); + } else if (rel_line >= height) { + line_delta = (rel_line - height) + height / 2; + } + + if (rel_col < 0) { + col_delta = rel_col - ((int)width / 2); + } else if (rel_col >= width) { + col_delta = (rel_col - width) + width / 2; + } + + scroll(view, line_delta, col_delta); + + struct setting *show_ws = settings_get("editor.show-whitespace"); + + struct cmdbuf cmdbuf = (struct cmdbuf){ + .cmds = commands, + .scroll = view->scroll, + .left_margin = total_margins.left, + .width = total_width, + .line_offset = total_margins.top, + .line_render_hooks = line_hooks, + .nlinerender_hooks = nlinehooks, + .mark_set = view->mark_set, + .region = to_region(view->dot, view->mark), + .show_ws = show_ws != NULL ? show_ws->value.bool_value : true, + }; + text_for_each_line(view->buffer->text, view->scroll.line, height, render_line, + &cmdbuf); + + // draw empty lines + uint32_t nlines = text_num_lines(view->buffer->text); + for (uint32_t linei = nlines - view->scroll.line + total_margins.top; + linei < height; ++linei) { + + 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 + to_relative(view, view->dot.line, view->dot.col, &rel_line, &rel_col); + uint32_t visual_col = visual_dot_col(view, view->dot.col); + to_relative(view, view->dot.line, visual_col, &rel_line, &rel_col); + + *relline = rel_line < 0 ? 0 : (uint32_t)rel_line + total_margins.top; + *relcol = rel_col < 0 ? 0 : (uint32_t)rel_col + total_margins.left; +} + +struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line) { + return text_get_line(buffer->text, line); +} + +void buffer_view_scroll_down(struct buffer_view *view, uint32_t height) { + buffer_goto(view, view->dot.line + height, view->dot.col); + scroll(view, height, 0); +} +void buffer_view_scroll_up(struct buffer_view *view, uint32_t height) { + buffer_goto(view, view->dot.line - height, view->dot.col); + scroll(view, -height, 0); +} |
