#include "buffer.h" #include "binding.h" #include "dged/vec.h" #include "display.h" #include "errno.h" #include "lang.h" #include "minibuffer.h" #include "path.h" #include "reactor.h" #include "settings.h" #include "utf8.h" #include #include #include #include #include #include #include #include #include #include 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 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; 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; } 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); } } } static struct buffer create_internal(const char *name, char *filename) { struct buffer b = (struct buffer){ .filename = filename, .name = strdup(name), .text = text_create(10), .modified = false, .readonly = false, .lang = filename != NULL ? lang_from_filename(filename) : lang_from_id("fnd"), .last_write = {0}, }; VEC_INIT(&b.update_hooks, 32); undo_init(&b.undo, 100); return b; } static bool movev(struct buffer *buffer, int64_t linedelta, struct location *location) { int64_t new_line = (int64_t)location->line + linedelta; if (new_line < 0) { location->line = 0; return false; } else if (new_line > text_num_lines(buffer->text)) { // allow addition of an extra line by going past the bottom location->line = text_num_lines(buffer->text); return false; } else { location->line = (uint32_t)new_line; // make sure column stays on the line uint32_t linelen = text_line_length(buffer->text, location->line); location->col = location->col > linelen ? linelen : location->col; return true; } } // move dot `coldelta` chars static bool moveh(struct buffer *buffer, int64_t coldelta, struct location *location) { int64_t new_col = (int64_t)location->col + coldelta; if (new_col > (int64_t)text_line_length(buffer->text, location->line)) { if (movev(buffer, 1, location)) { location->col = 0; } } else if (new_col < 0) { if (movev(buffer, -1, location)) { location->col = text_line_length(buffer->text, location->line); } else { return false; } } else { location->col = new_col; } return true; } static void delete_with_undo(struct buffer *buffer, struct location start, struct 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; } static void maybe_delete_region(struct buffer *buffer, struct region region) { if (region_has_size(region)) { delete_with_undo(buffer, region.begin, region.end); } } static void buffer_read_from_file(struct buffer *b) { struct stat sb; char *fullname = to_abspath(b->filename); if (stat(fullname, &sb) == 0) { FILE *file = fopen(fullname, "r"); free(fullname); if (file == NULL) { minibuffer_echo("Error opening %s: %s", b->filename, strerror(errno)); return; } 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", b->filename, strerror(errno)); fclose(file); return; } } fclose(file); b->last_write = sb.st_mtim; } else { minibuffer_echo("Error opening %s: %s", b->filename, strerror(errno)); free(fullname); return; } undo_push_boundary(&b->undo, (struct undo_boundary){.save_point = true}); } static 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); } static struct location find_next(struct buffer *buffer, struct location from, uint8_t chars[], uint32_t nchars, int direction) { struct text_chunk line = text_get_line(buffer->text, from.line); int64_t bytei = text_col_to_byteindex(buffer->text, from.line, from.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(buffer->text, from.line, bytei); return (struct location){.line = from.line, .col = target_col}; } static 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; } /* --------------------- buffer methods -------------------- */ struct buffer buffer_create(const char *name) { struct buffer b = create_internal(name, NULL); for (uint32_t hooki = 0; hooki < g_num_create_hooks; ++hooki) { g_create_hooks[hooki].callback(&b, g_create_hooks[hooki].userdata); } return b; } struct buffer buffer_from_file(const char *path) { char *full_path = to_abspath(path); struct buffer b = create_internal(basename((char *)path), full_path); buffer_read_from_file(&b); 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_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; } char *fullname = expanduser(buffer->filename); FILE *file = fopen(fullname, "w"); free(fullname); 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); clock_gettime(CLOCK_REALTIME, &buffer->last_write); buffer->modified = false; undo_push_boundary(&buffer->undo, (struct undo_boundary){.save_point = true}); } void buffer_set_filename(struct buffer *buffer, const char *filename) { buffer->filename = to_abspath(filename); buffer->modified = true; } void buffer_reload(struct buffer *buffer) { if (buffer->filename == NULL) { return; } // check if we actually need to reload struct stat sb; if (stat(buffer->filename, &sb) < 0) { minibuffer_echo_timeout(4, "failed to run stat on %s", buffer->filename); return; } if (sb.st_mtim.tv_sec != buffer->last_write.tv_sec) { text_clear(buffer->text); buffer_read_from_file(buffer); } else { minibuffer_echo_timeout(2, "buffer %s not changed", buffer->filename); } } 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); } struct location buffer_add(struct buffer *buffer, struct location at, uint8_t *text, uint32_t nbytes) { if (buffer->readonly) { minibuffer_echo_timeout(4, "buffer is read-only"); return at; } // invalidate last paste g_kill_ring.paste_up_to_date = false; struct location initial = at; struct location final = at; uint32_t lines_added, cols_added; text_insert_at(buffer->text, initial.line, initial.col, text, nbytes, &lines_added, &cols_added); // move to after inserted text movev(buffer, lines_added, &final); if (lines_added > 0) { // does not make sense to use position from another line final.col = 0; } moveh(buffer, cols_added, &final); undo_push_add( &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(&buffer->undo, (struct undo_boundary){.save_point = false}); } buffer->modified = true; return final; } struct location buffer_set_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) { uint32_t lines, cols; text_clear(buffer->text); text_append(buffer->text, text, nbytes, &lines, &cols); return buffer_clamp(buffer, lines, cols); } void buffer_clear(struct buffer *buffer) { text_clear(buffer->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; } bool buffer_is_backed(struct buffer *buffer) { return buffer->filename != NULL; } struct location buffer_previous_char(struct buffer *buffer, struct location dot) { moveh(buffer, -1, &dot); return dot; } struct location buffer_previous_word(struct buffer *buffer, struct location dot) { moveh(buffer, -1, &dot); uint8_t chars[] = {' ', '.'}; return find_next(buffer, dot, chars, 2, -1); } struct location buffer_previous_line(struct buffer *buffer, struct location dot) { movev(buffer, -1, &dot); return dot; } struct location buffer_next_char(struct buffer *buffer, struct location dot) { moveh(buffer, 1, &dot); return dot; } struct location buffer_next_word(struct buffer *buffer, struct location dot) { moveh(buffer, 1, &dot); uint8_t chars[] = {' ', '.'}; return find_next(buffer, dot, chars, 2, 1); } struct location buffer_next_line(struct buffer *buffer, struct location dot) { movev(buffer, 1, &dot); return dot; } struct location buffer_clamp(struct buffer *buffer, int64_t line, int64_t col) { struct location location = {.line = 0, .col = 0}; movev(buffer, line, &location); moveh(buffer, col, &location); return location; } struct location buffer_end(struct buffer *buffer) { uint32_t nlines = buffer_num_lines(buffer); return (struct location){nlines, buffer_num_chars(buffer, nlines)}; } uint32_t buffer_num_lines(struct buffer *buffer) { return text_num_lines(buffer->text); } uint32_t buffer_num_chars(struct buffer *buffer, uint32_t line) { return text_line_length(buffer->text, line); } struct location buffer_newline(struct buffer *buffer, struct location at) { return buffer_add(buffer, at, (uint8_t *)"\n", 1); } struct location buffer_indent(struct buffer *buffer, struct location at) { uint32_t tab_width = buffer->lang.tab_width; buffer_add(buffer, at, (uint8_t *)" ", tab_width > 16 ? 16 : tab_width); } struct location buffer_undo(struct buffer *buffer, struct location dot) { struct undo_stack *undo = &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); struct location pos = dot; 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) { buffer->modified = false; } break; } case Undo_Add: { struct undo_add *add = &rec->add; pos = buffer_delete(buffer, (struct region){.begin = (struct location){ .line = add->begin.row, .col = add->begin.col, }, .end = (struct location){ .line = add->end.row, .col = add->end.col, }}); break; } case Undo_Delete: { struct undo_delete *del = &rec->delete; pos = buffer_add(buffer, (struct location){ .line = del->pos.row, .col = del->pos.col, }, del->data, del->nbytes); break; } } } undo_push_boundary(undo, (struct undo_boundary){.save_point = false}); free(records); undo_end(undo); return pos; } /* --------------- searching and supporting types ---------------- */ struct search_data { VEC(struct region) matches; const char *pattern; }; // TODO: maybe should live in text static 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); strncpy(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 region match = region_new((struct location){.col = begin, .line = chunk->line}, (struct location){.col = begin + pattern_nchars - 1, .line = chunk->line}); VEC_PUSH(&data->matches, match); // proceed to after match byteidx += pattern_len; } free(line); } void buffer_find(struct buffer *buffer, const char *pattern, struct region **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); } struct location buffer_copy(struct buffer *buffer, struct region region) { if (region_has_size(region)) { struct text_chunk *curr = copy_region(buffer, region); } return region.begin; } struct location buffer_cut(struct buffer *buffer, struct region region) { if (region_has_size(region)) { copy_region(buffer, region); buffer_delete(buffer, region); } return region.begin; } struct location buffer_delete(struct buffer *buffer, struct region region) { if (buffer->readonly) { minibuffer_echo_timeout(4, "buffer is read-only"); return region.end; } if (!region_has_size(region)) { return region.begin; } struct text_chunk txt = text_get_region(buffer->text, region.begin.line, region.begin.col, region.end.line, region.end.col); undo_push_delete(&buffer->undo, (struct undo_delete){.data = txt.text, .nbytes = txt.nbytes, .pos = {.row = region.begin.line, .col = region.begin.col}}); undo_push_boundary(&buffer->undo, (struct undo_boundary){.save_point = false}); text_delete(buffer->text, region.begin.line, region.begin.col, region.end.line, region.end.col); buffer->modified = true; return region.begin; } static struct location paste(struct buffer *buffer, struct location at, uint32_t ring_idx) { struct location new_loc = at; if (ring_idx > 0) { struct text_chunk *curr = &g_kill_ring.buffer[ring_idx - 1]; if (curr->text != NULL) { g_kill_ring.last_paste = at; new_loc = buffer_add(buffer, at, curr->text, curr->nbytes); g_kill_ring.paste_up_to_date = true; } } return new_loc; } struct location buffer_paste(struct buffer *buffer, struct location at) { g_kill_ring.paste_idx = g_kill_ring.curr_idx; return paste(buffer, at, g_kill_ring.curr_idx); } struct location buffer_paste_older(struct buffer *buffer, struct location at) { 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(buffer, g_kill_ring.last_paste, at); // 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(buffer, g_kill_ring.last_paste, g_kill_ring.paste_idx); } else { buffer_paste(buffer, at); } } struct text_chunk buffer_line(struct buffer *buffer, uint32_t line) { return text_get_line(buffer->text, line); } uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook, void *userdata) { VEC_APPEND(&buffer->update_hooks, struct update_hook_entry * e); struct update_hook *h = &e->hook; h->callback = hook; h->userdata = userdata; // TODO: cant really have this if we actually want to remove a hook return VEC_SIZE(&buffer->update_hooks) - 1; } struct cmdbuf { struct command_list *cmds; struct location origin; uint32_t width; uint32_t height; bool show_ws; struct buffer *buffer; }; static uint32_t visual_char_width(uint8_t *byte, uint32_t maxlen) { if (*byte == '\t') { return 4; } else { return utf8_visual_char_width(byte, maxlen); } } static uint32_t visual_string_width(uint8_t *txt, uint32_t len, uint32_t start_col, uint32_t end_col) { uint32_t start_byte = utf8_nbytes(txt, len, start_col); uint32_t end_byte = utf8_nbytes(txt, len, end_col); uint32_t width = 0; for (uint32_t bytei = start_byte; bytei < end_byte; ++bytei) { width += visual_char_width(&txt[bytei], len - bytei); } return width; } void render_line(struct text_chunk *line, void *userdata) { struct cmdbuf *cmdbuf = (struct cmdbuf *)userdata; uint32_t visual_line = line->line - cmdbuf->origin.line; command_list_set_show_whitespace(cmdbuf->cmds, cmdbuf->show_ws); // calculate scroll offsets uint32_t scroll_bytes = utf8_nbytes(line->text, line->nbytes, cmdbuf->origin.col); uint32_t text_nbytes_scroll = scroll_bytes > line->nbytes ? 0 : line->nbytes - scroll_bytes; uint8_t *text = line->text + scroll_bytes; uint32_t visual_col_start = 0; uint32_t cur_visual_col = 0; uint32_t start_byte = 0, text_nbytes = 0; struct text_property *properties[16] = {0}; struct text_property *prev_properties[16] = {0}; uint32_t prev_nproperties; for (uint32_t cur_byte = start_byte, coli = 0; cur_byte < text_nbytes_scroll && cur_visual_col < cmdbuf->width && coli < line->nchars - cmdbuf->origin.col; ++coli) { uint32_t bytes_remaining = text_nbytes_scroll - cur_byte; uint32_t char_nbytes = utf8_nbytes(text + cur_byte, bytes_remaining, 1); uint32_t char_vwidth = visual_char_width(text + cur_byte, bytes_remaining); // calculate character properties uint32_t nproperties = 0; text_get_properties(cmdbuf->buffer->text, (struct location){.line = line->line, .col = coli + cmdbuf->origin.col}, properties, 16, &nproperties); // handle changes to properties uint32_t nnew_props = 0; struct text_property *new_props[16] = {0}; for (uint32_t propi = 0; propi < nproperties; ++propi) { if (propi >= prev_nproperties || prev_properties[propi] != properties[propi]) { new_props[nnew_props] = properties[propi]; ++nnew_props; } } // if we have any new or lost props, flush text up until now if (nnew_props > 0 || nproperties < prev_nproperties) { command_list_draw_text(cmdbuf->cmds, visual_col_start, visual_line, text + start_byte, cur_byte - start_byte); visual_col_start = cur_visual_col; start_byte = cur_byte; } // apply new properties for (uint32_t propi = 0; propi < nnew_props; ++propi) { struct text_property *prop = new_props[propi]; switch (prop->type) { case TextProperty_Colors: struct text_property_colors *colors = &prop->colors; if (colors->set_bg) { command_list_set_index_color_bg(cmdbuf->cmds, colors->bg); } if (colors->set_fg) { command_list_set_index_color_fg(cmdbuf->cmds, colors->fg); } break; } } if (nproperties == 0 && prev_nproperties > 0) { command_list_reset_color(cmdbuf->cmds); } memcpy(prev_properties, properties, nproperties * sizeof(struct text_property *)); prev_nproperties = nproperties; cur_byte += char_nbytes; text_nbytes += char_nbytes; cur_visual_col += char_vwidth; } // flush remaining command_list_draw_text(cmdbuf->cmds, visual_col_start, visual_line, text + start_byte, text_nbytes - start_byte); command_list_reset_color(cmdbuf->cmds); command_list_set_show_whitespace(cmdbuf->cmds, false); if (cur_visual_col < cmdbuf->width) { command_list_draw_repeated(cmdbuf->cmds, cur_visual_col, visual_line, ' ', cmdbuf->width - cur_visual_col); } } void buffer_update(struct buffer *buffer, struct buffer_update_params *params) { if (params->width == 0 || params->height == 0) { return; } VEC_FOR_EACH(&buffer->update_hooks, struct update_hook_entry * entry) { struct update_hook *h = &entry->hook; h->callback(buffer, params->width, params->height, h->userdata); } struct setting *show_ws = settings_get("editor.show-whitespace"); struct cmdbuf cmdbuf = (struct cmdbuf){ .cmds = params->commands, .origin = params->origin, .width = params->width, .height = params->height, .show_ws = show_ws != NULL ? show_ws->value.bool_value : true, .buffer = buffer, }; text_for_each_line(buffer->text, params->origin.line, params->height, render_line, &cmdbuf); // draw empty lines uint32_t nlines = text_num_lines(buffer->text); for (uint32_t linei = nlines - params->origin.line; linei < params->height; ++linei) { command_list_draw_repeated(params->commands, 0, linei, ' ', params->width); } } void buffer_add_text_property(struct buffer *buffer, struct location start, struct location end, struct text_property property) { text_add_property( buffer->text, (struct location){.line = start.line, .col = start.col}, (struct location){.line = end.line, .col = end.col}, property); } void buffer_get_text_properties(struct buffer *buffer, struct location location, struct text_property **properties, uint32_t max_nproperties, uint32_t *nproperties) { text_get_properties( buffer->text, (struct location){.line = location.line, .col = location.col}, properties, max_nproperties, nproperties); } void buffer_clear_text_properties(struct buffer *buffer) { text_clear_properties(buffer->text); }