diff options
Diffstat (limited to 'src/dged')
34 files changed, 5339 insertions, 0 deletions
diff --git a/src/dged/allocator.c b/src/dged/allocator.c new file mode 100644 index 0000000..308b97c --- /dev/null +++ b/src/dged/allocator.c @@ -0,0 +1,24 @@ +#include "allocator.h" + +struct frame_allocator frame_allocator_create(size_t capacity) { + return (struct frame_allocator){ + .capacity = capacity, .offset = 0, .buf = (uint8_t *)malloc(capacity)}; +} + +void frame_allocator_destroy(struct frame_allocator *alloc) { + free(alloc->buf); + alloc->buf = NULL; +} + +void *frame_allocator_alloc(struct frame_allocator *alloc, size_t sz) { + if (alloc->offset + sz > alloc->capacity) { + return NULL; + } + + void *mem = alloc->buf + alloc->offset; + alloc->offset += sz; + + return mem; +} + +void frame_allocator_clear(struct frame_allocator *alloc) { alloc->offset = 0; } diff --git a/src/dged/allocator.h b/src/dged/allocator.h new file mode 100644 index 0000000..49e3aec --- /dev/null +++ b/src/dged/allocator.h @@ -0,0 +1,46 @@ +#include <stddef.h> +#include <stdint.h> +#include <stdlib.h> + +/** + * Simple bump allocator that can be used for + * allocations with a frame lifetime. + */ +struct frame_allocator { + uint8_t *buf; + size_t offset; + size_t capacity; +}; + +/** + * Create a new frame allocator + * + * @param capacity The capacity in bytes of the frame allocator + * @returns The frame allocator + */ +struct frame_allocator frame_allocator_create(size_t capacity); + +/** + * Destroy a frame allocator. + * + * @param alloc The @ref frame_allocator "frame allocator" to destroy. + */ +void frame_allocator_destroy(struct frame_allocator *alloc); + +/** + * Allocate memory in this @ref frame_allocator "frame allocator" + * + * @param alloc The allocator to allocate in + * @param sz The size in bytes to allocate. + * @returns void* representing the start of the allocated region on success, + * NULL on failure. + */ +void *frame_allocator_alloc(struct frame_allocator *alloc, size_t sz); + +/** + * Clear this @ref frame_allocator "frame allocator". + * + * This does not free any memory, but simply resets the offset to 0. + * @param alloc The frame allocator to clear + */ +void frame_allocator_clear(struct frame_allocator *alloc); diff --git a/src/dged/binding.c b/src/dged/binding.c new file mode 100644 index 0000000..5111548 --- /dev/null +++ b/src/dged/binding.c @@ -0,0 +1,75 @@ +#include "binding.h" +#include "command.h" + +#include <stdlib.h> +#include <string.h> + +struct keymap keymap_create(const char *name, uint32_t capacity) { + return (struct keymap){ + .name = name, + .bindings = calloc(capacity, sizeof(struct binding)), + .nbindings = 0, + .capacity = capacity, + }; +} + +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings) { + if (keymap->nbindings + nbindings >= keymap->capacity) { + keymap->capacity = + nbindings > keymap->capacity * 2 ? nbindings * 2 : keymap->capacity * 2; + keymap->bindings = + realloc(keymap->bindings, sizeof(struct binding) * keymap->capacity); + } + memcpy(keymap->bindings + keymap->nbindings, bindings, + sizeof(struct binding) * nbindings); + + keymap->nbindings += nbindings; +} + +void keymap_destroy(struct keymap *keymap) { + free(keymap->bindings); + keymap->bindings = 0; + keymap->capacity = 0; + keymap->nbindings = 0; +} + +struct lookup_result lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands) { + // lookup in reverse order in the keymaps + uint32_t kmi = nkeymaps; + while (kmi > 0) { + --kmi; + struct keymap *keymap = &keymaps[kmi]; + + for (uint32_t bi = keymap->nbindings; bi > 0; --bi) { + struct binding *binding = &keymap->bindings[bi - 1]; + if (key_equal(key, &binding->key)) { + switch (binding->type) { + case BindingType_Command: { + return (struct lookup_result){ + .found = true, + .type = BindingType_Command, + .command = lookup_command_by_hash(commands, binding->command), + }; + } + case BindingType_Keymap: { + return (struct lookup_result){ + .found = true, + .type = BindingType_Keymap, + .keymap = binding->keymap, + }; + } + case BindingType_DirectCommand: + return (struct lookup_result){ + .found = true, + .type = BindingType_Command, + .command = binding->direct_command, + }; + } + } + } + } + + return (struct lookup_result){.found = false}; +} diff --git a/src/dged/binding.h b/src/dged/binding.h new file mode 100644 index 0000000..f2a531d --- /dev/null +++ b/src/dged/binding.h @@ -0,0 +1,136 @@ +#include "hash.h" +#include "keyboard.h" + +/** + * Directory of keyboard mappings. + */ +struct keymap { + /** Keymap name */ + const char *name; + + /** The bindings in this keymap */ + struct binding *bindings; + + /** The number of bindings in this keymap */ + uint32_t nbindings; + + /** The number of bindings this keymap can currently hold */ + uint32_t capacity; +}; + +/** + * Type of a keyboard binding + */ +enum binding_type { + /** This binding is to a command */ + BindingType_Command, + + /** This binding is to another keymap */ + BindingType_Keymap, + + /** This binding is to an already resolved command, + * a.k.a. anonymous binding. + */ + BindingType_DirectCommand +}; + +#define BINDING_INNER(mod_, c_, command_) \ + (struct binding) { \ + .key = {.mod = mod_, .key = c_}, .type = BindingType_Command, \ + .command = hash_name(command_) \ + } + +#define ANONYMOUS_BINDING_INNER(mod_, c_, command_) \ + (struct binding) { \ + .key = {.mod = mod_, .key = c_}, .type = BindingType_DirectCommand, \ + .direct_command = command_ \ + } + +#define PREFIX_INNER(mod_, c_, keymap_) \ + (struct binding) { \ + .key = {.mod = mod_, .key = c_}, .type = BindingType_Keymap, \ + .keymap = keymap_ \ + } + +#define BINDING(...) BINDING_INNER(__VA_ARGS__) +#define PREFIX(...) PREFIX_INNER(__VA_ARGS__) +#define ANONYMOUS_BINDING(...) ANONYMOUS_BINDING_INNER(__VA_ARGS__) + +/** + * A keyboard key bound to an action + */ +struct binding { + /** The keyboard key that triggers the action in this binding */ + struct key key; + + /** Type of this binding, see @ref binding_type */ + uint8_t type; + + union { + /** A hash of a command name */ + uint32_t command; + /** A command */ + struct command *direct_command; + /** A keymap */ + struct keymap *keymap; + }; +}; + +/** + * Result of a binding lookup + */ +struct lookup_result { + /** True if a binding was found */ + bool found; + + /** Type of binding in the result */ + uint8_t type; + + union { + /** A command */ + struct command *command; + /** A keymap */ + struct keymap *keymap; + }; +}; + +struct commands; + +/** + * Create a new keymap + * + * @param name Name of the keymap + * @param capacity Initial capacity of the keymap. + * @returns The created keymap + */ +struct keymap keymap_create(const char *name, uint32_t capacity); + +/** + * Bind keys in a keymap. + * + * @param keymap The keymap to bind keys in. + * @param bindings Bindings to add. + * @param nbindings Number of bindings in @ref bindings. + */ +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings); + +/** + * Destroy a keymap. + * + * This clears all keybindings associated with the keymap. + * @param keymap Keymap to destroy. + */ +void keymap_destroy(struct keymap *keymap); + +/** + * Lookup the binding for a key in a set of keymaps. + * + * @param keymaps The keymaps to look in. + * @param nkeymaps The number of keymaps in @ref keymaps. + * @param key The keystroke to look up bindings for. + * @param commands Available commands for lookup. + * @returns A @ref lookup_result with the result of the lookup. + */ +struct lookup_result lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands); diff --git a/src/dged/btree.h b/src/dged/btree.h new file mode 100644 index 0000000..8743b32 --- /dev/null +++ b/src/dged/btree.h @@ -0,0 +1,113 @@ +#ifndef _BTREE_H +#define _BTREE_H + +#include "vec.h" + +#include <stdlib.h> + +#define BINTREE_ENTRY_TYPE(name, entry) \ + struct name { \ + struct name *parent; \ + struct name *left; \ + struct name *right; \ + entry value; \ + } + +#define BINTREE(entry) \ + struct { \ + struct entry *root; \ + } + +#define BINTREE_INIT(tree) ((tree)->root = NULL) +#define BINTREE_DESTROY(tree, entry_type) BINTREE_INIT(tree) + +#define BINTREE_ROOT(tree) (tree)->root + +#define BINTREE_LEFT(node) (node)->left +#define BINTREE_RIGHT(node) (node)->right +#define BINTREE_PARENT(node) (node)->parent +#define BINTREE_VALUE(node) (node)->value +#define BINTREE_HAS_PARENT(node) ((node)->parent != NULL) +#define BINTREE_HAS_LEFT(node) ((node)->left != NULL) +#define BINTREE_HAS_RIGHT(node) ((node)->right != NULL) + +#define BINTREE_FREE_NODE(node) free(node) +#define BINTREE_FREE_NODES(root, entry_type) \ + { \ + BINTREE_FIRST(root); \ + VEC(struct entry_type *) to_delete; \ + VEC_INIT(&to_delete, 10); \ + while (root != NULL) { \ + VEC_PUSH(&to_delete, root); \ + BINTREE_NEXT(root); \ + } \ + VEC_FOR_EACH(&to_delete, struct entry_type **e) { BINTREE_FREE_NODE(*e); } \ + VEC_DESTROY(&to_delete); \ + } + +#define BINTREE_FIRST(res) \ + if (res == NULL) { \ + res = NULL; \ + } else { \ + while (BINTREE_HAS_LEFT(res)) { \ + res = BINTREE_LEFT(res); \ + } \ + } + +#define BINTREE_NEXT(res) \ + if (res == NULL) { \ + res = NULL; \ + } else { \ + if (BINTREE_HAS_RIGHT(res)) { \ + res = BINTREE_RIGHT(res); \ + BINTREE_FIRST(res) \ + } else { \ + while (BINTREE_HAS_PARENT(res) && \ + res == BINTREE_RIGHT(BINTREE_PARENT(res))) \ + res = BINTREE_PARENT(res); \ + res = BINTREE_PARENT(res); \ + } \ + } + +#define BINTREE_INSERT(parent, entry) \ + if (parent != NULL) { \ + if (!BINTREE_HAS_LEFT(parent)) { \ + BINTREE_LEFT(parent) = calloc(1, sizeof(*(parent))); \ + BINTREE_PARENT(BINTREE_LEFT(parent)) = parent; \ + BINTREE_VALUE(BINTREE_LEFT(parent)) = entry; \ + } else { \ + BINTREE_RIGHT(parent) = calloc(1, sizeof(*(parent))); \ + BINTREE_PARENT(BINTREE_RIGHT(parent)) = parent; \ + BINTREE_VALUE(BINTREE_RIGHT(parent)) = entry; \ + } \ + } + +#define BINTREE_REMOVE(node) \ + if (BINTREE_HAS_PARENT(node)) { \ + if (BINTREE_LEFT(BINTREE_PARENT(node)) == node) { \ + BINTREE_LEFT(BINTREE_PARENT(node)) = NULL; \ + } else { \ + BINTREE_RIGHT(BINTREE_PARENT(node)) = NULL; \ + } \ + BINTREE_PARENT(node) = NULL; \ + } + +#define BINTREE_SET_ROOT(tree, value) \ + (tree)->root = calloc(1, sizeof(*(tree)->root)); \ + BINTREE_VALUE((tree)->root) = value; + +#define BINTREE_FIND(tree, needle, res) \ + { \ + res = BINTREE_ROOT(tree); \ + BINTREE_FIRST(res); \ + bool found = false; \ + while (res != NULL) { \ + if (BINTREE_VALUE(res) == needle) { \ + found = true; \ + break; \ + } \ + BINTREE_NEXT(res); \ + } \ + res = found ? res : NULL; \ + } +#endif 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); +} diff --git a/src/dged/buffer.h b/src/dged/buffer.h new file mode 100644 index 0000000..539c427 --- /dev/null +++ b/src/dged/buffer.h @@ -0,0 +1,219 @@ +#include <stddef.h> +#include <stdint.h> +#include <stdio.h> + +#include "bits/stdint-uintn.h" +#include "command.h" +#include "lang.h" +#include "text.h" +#include "undo.h" +#include "window.h" + +struct keymap; +struct command_list; + +/** + * Margins where buffer text should not be + */ +struct margin { + uint32_t left; + uint32_t right; + uint32_t top; + uint32_t bottom; +}; + +/** Callback for line rendering hooks */ +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 + * + * A callback paired with userdata + */ +struct line_render_hook { + line_render_cb callback; + line_render_empty_cb empty_callback; + void *userdata; +}; + +/** + * Result of updating a buffer hook + */ +struct update_hook_result { + /** Desired margins for this hook */ + struct margin margins; + + /** Hook to be added to rendering of buffer lines */ + struct line_render_hook line_render_hook; +}; + +/** Buffer update hook callback function */ +typedef struct update_hook_result (*update_hook_cb)( + struct buffer_view *view, struct command_list *commands, uint32_t width, + uint32_t height, uint64_t frame_time, void *userdata); + +/** + * A buffer update hook. + * + * Can be used to implement custom behavior on top of a buffer. Used for + * minibuffer, line numbers, modeline etc. + */ +struct update_hook { + /** Callback function */ + update_hook_cb callback; + + /** Optional userdata to pass to the callback function unmodified */ + void *userdata; +}; + +typedef void (*create_hook_cb)(struct buffer *buffer, void *userdata); + +/** + * A set of update hooks + */ +struct update_hooks { + /** The update hooks */ + struct update_hook hooks[32]; + + /** The number of update hooks */ + uint32_t nhooks; +}; + +struct buffer_location { + uint32_t line; + uint32_t col; +}; + +struct match { + struct buffer_location begin; + struct buffer_location end; +}; + +struct buffer_view { + /** Location of dot (cursor) */ + struct buffer_location dot; + + /** Location of mark (where a selection starts) */ + struct buffer_location mark; + + /** Current buffer scroll position */ + struct buffer_location scroll; + + /** True if the start of a selection has been set */ + bool mark_set; + + /** Modeline buffer (may be NULL) */ + struct modeline *modeline; + + bool line_numbers; + + struct buffer *buffer; +}; + +struct buffer_view buffer_view_create(struct buffer *buffer, bool modeline, + bool line_numbers); +struct buffer_view buffer_view_clone(struct buffer_view *view); + +void buffer_view_scroll_down(struct buffer_view *view, uint32_t height); +void buffer_view_scroll_up(struct buffer_view *view, uint32_t height); + +void buffer_view_destroy(struct buffer_view *view); + +/** + * A buffer of text that can be modified, read from and written to disk. + * + * This is the central data structure of dged and most other behavior is + * implemented on top of it. + */ +struct buffer { + + /** Buffer name */ + char *name; + + /** Associated filename, this is where the buffer will be saved to */ + char *filename; + + /** Text data structure */ + struct text *text; + + /** Buffer update hooks */ + struct update_hooks update_hooks; + + /** Buffer undo stack */ + struct undo_stack undo; + + /** Has this buffer been modified from when it was last saved */ + bool modified; + + /** Can this buffer be changed */ + bool readonly; + + /** Buffer programming language */ + struct language lang; +}; + +struct buffer buffer_create(char *name); +void buffer_destroy(struct buffer *buffer); + +void buffer_static_init(); +void buffer_static_teardown(); + +int buffer_add_text(struct buffer_view *view, uint8_t *text, uint32_t nbytes); +void buffer_set_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes); +void buffer_clear(struct buffer_view *view); +bool buffer_is_empty(struct buffer *buffer); +bool buffer_is_modified(struct buffer *buffer); +bool buffer_is_readonly(struct buffer *buffer); +void buffer_set_readonly(struct buffer *buffer, bool readonly); + +void buffer_kill_line(struct buffer_view *view); +void buffer_forward_delete_char(struct buffer_view *view); +void buffer_backward_delete_char(struct buffer_view *view); +void buffer_backward_char(struct buffer_view *view); +void buffer_backward_word(struct buffer_view *view); +void buffer_forward_char(struct buffer_view *view); +void buffer_forward_word(struct buffer_view *view); +void buffer_backward_line(struct buffer_view *view); +void buffer_forward_line(struct buffer_view *view); +void buffer_end_of_line(struct buffer_view *view); +void buffer_beginning_of_line(struct buffer_view *view); +void buffer_newline(struct buffer_view *view); +void buffer_indent(struct buffer_view *view); + +void buffer_undo(struct buffer_view *view); + +void buffer_goto_beginning(struct buffer_view *view); +void buffer_goto_end(struct buffer_view *view); +void buffer_goto(struct buffer_view *view, uint32_t line, uint32_t col); + +void buffer_find(struct buffer *buffer, const char *pattern, + struct match **matches, uint32_t *nmatches); + +void buffer_set_mark(struct buffer_view *view); +void buffer_clear_mark(struct buffer_view *view); +void buffer_set_mark_at(struct buffer_view *view, uint32_t line, uint32_t col); + +void buffer_copy(struct buffer_view *view); +void buffer_paste(struct buffer_view *view); +void buffer_paste_older(struct buffer_view *view); +void buffer_cut(struct buffer_view *view); + +struct text_chunk buffer_get_line(struct buffer *buffer, uint32_t line); + +uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook, + void *userdata); + +uint32_t buffer_add_create_hook(create_hook_cb hook, void *userdata); + +struct buffer buffer_from_file(char *filename); +void buffer_to_file(struct buffer *buffer); +void buffer_write_to(struct buffer *buffer, const char *filename); + +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); diff --git a/src/dged/buffers.c b/src/dged/buffers.c new file mode 100644 index 0000000..38b51b7 --- /dev/null +++ b/src/dged/buffers.c @@ -0,0 +1,43 @@ +#include "buffers.h" +#include "buffer.h" + +#include <stdlib.h> +#include <string.h> + +void buffers_init(struct buffers *buffers, uint32_t initial_capacity) { + buffers->buffers = calloc(initial_capacity, sizeof(struct buffer)); + buffers->nbuffers = 0; + buffers->capacity = initial_capacity; +} + +struct buffer *buffers_add(struct buffers *buffers, struct buffer buffer) { + if (buffers->nbuffers == buffers->capacity) { + buffers->capacity *= 2; + buffers->buffers = + realloc(buffers->buffers, sizeof(struct buffer) * buffers->capacity); + } + + buffers->buffers[buffers->nbuffers] = buffer; + ++buffers->nbuffers; + + return &buffers->buffers[buffers->nbuffers - 1]; +} + +struct buffer *buffers_find(struct buffers *buffers, const char *name) { + for (uint32_t bufi = 0; bufi < buffers->nbuffers; ++bufi) { + if (strcmp(name, buffers->buffers[bufi].name) == 0) { + return &buffers->buffers[bufi]; + } + } + + return NULL; +} + +void buffers_destroy(struct buffers *buffers) { + for (uint32_t bufi = 0; bufi < buffers->nbuffers; ++bufi) { + buffer_destroy(&buffers->buffers[bufi]); + } + + buffers->nbuffers = 0; + free(buffers->buffers); +} diff --git a/src/dged/buffers.h b/src/dged/buffers.h new file mode 100644 index 0000000..edf772c --- /dev/null +++ b/src/dged/buffers.h @@ -0,0 +1,17 @@ +#include <stdint.h> + +struct buffer; + +struct buffers { + // TODO: more buffers + struct buffer *buffers; + uint32_t nbuffers; + uint32_t capacity; +}; + +void buffers_init(struct buffers *buffers, uint32_t initial_capacity); + +struct buffer *buffers_add(struct buffers *buffers, struct buffer buffer); +struct buffer *buffers_find(struct buffers *buffers, const char *name); + +void buffers_destroy(struct buffers *buffers); diff --git a/src/dged/command.c b/src/dged/command.c new file mode 100644 index 0000000..9144058 --- /dev/null +++ b/src/dged/command.c @@ -0,0 +1,79 @@ +#include "command.h" +#include "buffer.h" +#include "buffers.h" +#include "hash.h" +#include "hashmap.h" +#include "minibuffer.h" + +#include <string.h> + +struct commands command_registry_create(uint32_t capacity) { + + struct commands cmds = {0}; + HASHMAP_INIT(&cmds.commands, capacity, hash_name); + return cmds; +} + +void command_registry_destroy(struct commands *commands) { + HASHMAP_DESTROY(&commands->commands); +} + +uint32_t register_command(struct commands *commands, struct command command) { + uint32_t hash = 0; + HASHMAP_INSERT(&commands->commands, struct command_entry, command.name, + command, hash); + return hash; +} + +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands) { + for (uint32_t ci = 0; ci < ncommands; ++ci) { + register_command(command_list, commands[ci]); + } +} + +struct command *lookup_command(struct commands *command_list, + const char *name) { + HASHMAP_GET(&command_list->commands, struct command_entry, name, + struct command * command); + return command; +} + +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash) { + HASHMAP_GET_BY_HASH(&commands->commands, struct command_entry, hash, + struct command * command); + return command; +} + +int32_t execute_command(struct command *command, struct commands *commands, + struct window *active_window, struct buffers *buffers, + int argc, const char *argv[]) { + + return command->fn( + (struct command_ctx){ + .buffers = buffers, + .active_window = active_window, + .userdata = command->userdata, + .commands = commands, + .self = command, + .saved_argv = {0}, + .saved_argc = 0, + }, + argc, argv); +} + +void command_ctx_push_arg(struct command_ctx *ctx, const char *argv) { + if (ctx->saved_argc < 64) { + ctx->saved_argv[ctx->saved_argc] = strdup(argv); + ++ctx->saved_argc; + } +} + +void command_ctx_free(struct command_ctx *ctx) { + for (uint32_t i = 0; i < ctx->saved_argc; ++i) { + free((char *)ctx->saved_argv[i]); + } + + ctx->saved_argc = 0; +} diff --git a/src/dged/command.h b/src/dged/command.h new file mode 100644 index 0000000..bbc57f2 --- /dev/null +++ b/src/dged/command.h @@ -0,0 +1,179 @@ +#ifndef _COMMAND_H +#define _COMMAND_H + +/** @file command.h + * Commands and command registries + */ +#include "hashmap.h" +#include <stdint.h> + +struct buffer; +struct buffers; +struct window; + +/** + * Execution context for a command + */ +struct command_ctx { + /** + * The current list of buffers. + * + * Can be used to insert new buffers or + * inspect existing. + */ + struct buffers *buffers; + + /** + * The currently active window. + */ + struct window *active_window; + + /** + * A registry of available commands. + * + * Can be used to execute other commands as part of a command implementation. + */ + struct commands *commands; + + /** + * The command that is currently being executed + */ + struct command *self; + + /** + * User data set up by the command currently being executed. + */ + void *userdata; + + const char *saved_argv[64]; + + int saved_argc; +}; + +/** A command function callback which holds the implementation of a command */ +typedef int32_t (*command_fn)(struct command_ctx ctx, int argc, + const char *argv[]); + +/** + * A command that can be bound to a key or executed directly + */ +struct command { + /** + * Name of the command + * + * Used to look the command up for execution and keybinds. + */ + const char *name; + + /** + * Implementation of command behavior + */ + command_fn fn; + + /** + * Userdata passed to each invocation of the command. + */ + void *userdata; +}; + +/** + * A command registry + */ +HASHMAP_ENTRY_TYPE(command_entry, struct command); + +struct commands { + HASHMAP(struct command_entry) commands; +}; + +/** + * Create a new command registry. + * + * @param[in] capacity The initial capacity for the registry + */ +struct commands command_registry_create(uint32_t capacity); + +/** + * Destroy a command registry. + * + * This will free all memory associated with stored commands. + * @param[in] commands A pointer to a commands structure created by @ref + * command_registry_create(uint32_t) + */ +void command_registry_destroy(struct commands *commands); + +/** + * Register a new command in the registry @ref commands. + * + * @param[in] commands The registry to insert into + * @param[in] command The command to insert + */ +uint32_t register_command(struct commands *commands, struct command command); + +/** + * Register multiple commands in the registry @ref commands "command_list". + * + * @param[in] command_list The registry to insert into + * @param[in] commands The commands to insert + * @param[in] ncommands Number of commands contained in @ref commands + */ +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands); + +/** + * Execute a command and return the result. + * + * @param[in] command The @ref command to execute + * @param[in] commands A @ref command "command registry" to use for context in + * the executed command. Can for example be used to implement commands that + * execute arbitrary other commands. + * @param[in] active_window A @ref window representing the currently active + * window in the editor. This provides a way to access the current buffer as + * well. + * @param[in] buffers The current list of buffers for context. Can be used for + * example to create a buffer list. + * @param[in] argc Number of arguments to the command. + * @param[in] argv The arguments to the command. + * + * @returns Integer representing the exit status where 0 means success and + * anything else means there was an error. + */ +int32_t execute_command(struct command *command, struct commands *commands, + struct window *active_window, struct buffers *buffers, + int argc, const char *argv[]); + +/** + * Hash the name of a command. + * + * @param[in] name The command name + * @returns An integer representing the hash of the name + */ +uint32_t hash_command_name(const char *name); + +/** + * Lookup a command by name. + * + * @param[in] commands The @ref commands "command registry" to look for the @ref + * command in. + * @param[in] name The name of the command to look for + * @returns A pointer to the command if found, NULL otherwise. + */ +struct command *lookup_command(struct commands *commands, const char *name); + +/** + * Lookup a command by hash. + * + * The hash value is expected to have been computed with @ref + * hash_command_name(const char* name). + * + * @param[in] commands The @ref commands "command registry" to look for the @ref + * command in. + * @param[in] hash The hash value for the name of the command to look for + * @returns A pointer to the command if found, NULL otherwise. + */ +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash); + +void command_ctx_push_arg(struct command_ctx *ctx, const char *argv); +void command_ctx_free(struct command_ctx *ctx); + +#endif diff --git a/src/dged/display.c b/src/dged/display.c new file mode 100644 index 0000000..d9eeb11 --- /dev/null +++ b/src/dged/display.c @@ -0,0 +1,386 @@ +#define _DEFAULT_SOURCE +#include "display.h" + +#include "buffer.h" + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +#define ESC 0x1b + +struct display { + struct termios term; + struct termios orig_term; + uint32_t width; + uint32_t height; +}; + +enum render_cmd_type { + RenderCommand_DrawText = 0, + RenderCommand_PushFormat = 1, + RenderCommand_Repeat = 2, + RenderCommand_ClearFormat = 3, + RenderCommand_SetShowWhitespace = 4, +}; + +struct render_command { + enum render_cmd_type type; + union { + struct draw_text_cmd *draw_txt; + struct push_fmt_cmd *push_fmt; + struct repeat_cmd *repeat; + struct show_ws_cmd *show_ws; + }; +}; + +struct draw_text_cmd { + uint32_t col; + uint32_t row; + + uint8_t *data; + uint32_t len; +}; + +struct push_fmt_cmd { + uint8_t fmt[64]; + uint32_t len; +}; + +struct repeat_cmd { + uint32_t col; + uint32_t row; + uint8_t c; + uint32_t nrepeat; +}; + +struct show_ws_cmd { + bool show; +}; + +struct command_list { + struct render_command *cmds; + uint64_t ncmds; + uint64_t capacity; + + uint32_t xoffset; + uint32_t yoffset; + + void *(*allocator)(size_t); + + char name[16]; +}; + +struct winsize getsize() { + struct winsize ws; + ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + return ws; +} + +struct display *display_create() { + + struct winsize ws = getsize(); + + // save old settings + struct termios orig_term; + tcgetattr(0, &orig_term); + + // set terminal to raw mode + struct termios term = {0}; + cfmakeraw(&term); + + tcsetattr(0, TCSADRAIN, &term); + + struct display *d = calloc(1, sizeof(struct display)); + d->orig_term = orig_term; + d->term = term; + d->height = ws.ws_row; + d->width = ws.ws_col; + return d; +} + +void display_resize(struct display *display) { + struct winsize sz = getsize(); + display->width = sz.ws_col; + display->height = sz.ws_row; +} + +void display_destroy(struct display *display) { + // reset old terminal mode + tcsetattr(0, TCSADRAIN, &display->orig_term); + + free(display); +} + +uint32_t display_width(struct display *display) { return display->width; } +uint32_t display_height(struct display *display) { return display->height; } + +void putbyte(uint8_t c) { + if (c != '\r') { + putc(c, stdout); + } +} + +void putbyte_ws(uint8_t c, bool show_whitespace) { + if (show_whitespace && c == '\t') { + fputs("\x1b[90m → \x1b[39m", stdout); + } else if (show_whitespace && c == ' ') { + fputs("\x1b[90m·\x1b[39m", stdout); + } else { + putbyte(c); + } +} + +void putbytes(uint8_t *line_bytes, uint32_t line_length, bool show_whitespace) { + for (uint32_t bytei = 0; bytei < line_length; ++bytei) { + uint8_t byte = line_bytes[bytei]; + putbyte_ws(byte, show_whitespace); + } +} + +void put_ansiparm(int n) { + int q = n / 10; + if (q != 0) { + int r = q / 10; + if (r != 0) { + putbyte((r % 10) + '0'); + } + putbyte((q % 10) + '0'); + } + putbyte((n % 10) + '0'); +} + +void display_move_cursor(struct display *display, uint32_t row, uint32_t col) { + putbyte(ESC); + putbyte('['); + put_ansiparm(row + 1); + putbyte(';'); + put_ansiparm(col + 1); + putbyte('H'); +} + +void display_clear(struct display *display) { + display_move_cursor(display, 0, 0); + uint8_t bytes[] = {ESC, '[', 'J'}; + putbytes(bytes, 3, false); +} + +struct command_list *command_list_create(uint32_t capacity, + void *(*allocator)(size_t), + uint32_t xoffset, uint32_t yoffset, + const char *name) { + struct command_list *command_list = allocator(sizeof(struct command_list)); + + command_list->capacity = capacity; + command_list->ncmds = 0; + command_list->xoffset = xoffset; + command_list->yoffset = yoffset; + strncpy(command_list->name, name, 15); + + command_list->cmds = allocator(sizeof(struct render_command) * capacity); + command_list->allocator = allocator; + + return command_list; +} + +struct render_command *add_command(struct command_list *list, + enum render_cmd_type tp) { + if (list->ncmds == list->capacity) { + /* TODO: better. Currently a bit tricky to provide dynamic scaling of this + * since it is initially allocated with the frame allocator that does not + * support realloc. + */ + return NULL; + } + + struct render_command *cmd = &list->cmds[list->ncmds]; + cmd->type = tp; + switch (tp) { + case RenderCommand_DrawText: + cmd->draw_txt = list->allocator(sizeof(struct draw_text_cmd)); + break; + case RenderCommand_Repeat: + cmd->repeat = list->allocator(sizeof(struct repeat_cmd)); + break; + case RenderCommand_PushFormat: + cmd->push_fmt = list->allocator(sizeof(struct push_fmt_cmd)); + break; + case RenderCommand_SetShowWhitespace: + cmd->show_ws = list->allocator(sizeof(struct show_ws_cmd)); + break; + case RenderCommand_ClearFormat: + break; + } + ++list->ncmds; + return cmd; +} + +void command_list_draw_text(struct command_list *list, uint32_t col, + uint32_t row, uint8_t *data, uint32_t len) { + struct draw_text_cmd *cmd = + add_command(list, RenderCommand_DrawText)->draw_txt; + cmd->data = data; + cmd->col = col; + cmd->row = row; + cmd->len = len; +} + +void command_list_draw_text_copy(struct command_list *list, uint32_t col, + uint32_t row, uint8_t *data, uint32_t len) { + uint8_t *bytes = (uint8_t *)list->allocator(len); + memcpy(bytes, data, len); + + command_list_draw_text(list, col, row, bytes, len); +} + +void command_list_draw_repeated(struct command_list *list, uint32_t col, + uint32_t row, uint8_t c, uint32_t nrepeat) { + struct repeat_cmd *cmd = add_command(list, RenderCommand_Repeat)->repeat; + cmd->col = col; + cmd->row = row; + cmd->c = c; + cmd->nrepeat = nrepeat; +} + +void command_list_set_index_color_fg(struct command_list *list, + uint8_t color_idx) { + struct push_fmt_cmd *cmd = + add_command(list, RenderCommand_PushFormat)->push_fmt; + + if (color_idx < 8) { + cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 30 + color_idx); + } else if (color_idx < 16) { + cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 90 + color_idx - 8); + } else { + cmd->len = snprintf((char *)cmd->fmt, 64, "38;5;%d", color_idx); + } +} + +void command_list_set_color_fg(struct command_list *list, uint8_t red, + uint8_t green, uint8_t blue) { + struct push_fmt_cmd *cmd = + add_command(list, RenderCommand_PushFormat)->push_fmt; + cmd->len = snprintf((char *)cmd->fmt, 64, "38;2;%d;%d;%d", red, green, blue); +} + +void command_list_set_index_color_bg(struct command_list *list, + uint8_t color_idx) { + struct push_fmt_cmd *cmd = + add_command(list, RenderCommand_PushFormat)->push_fmt; + if (color_idx < 8) { + cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 40 + color_idx); + } else if (color_idx < 16) { + cmd->len = snprintf((char *)cmd->fmt, 64, "%d", 100 + color_idx - 8); + } else { + cmd->len = snprintf((char *)cmd->fmt, 64, "48;5;%d", color_idx); + } +} + +void command_list_set_color_bg(struct command_list *list, uint8_t red, + uint8_t green, uint8_t blue) { + struct push_fmt_cmd *cmd = + add_command(list, RenderCommand_PushFormat)->push_fmt; + cmd->len = snprintf((char *)cmd->fmt, 64, "48;2;%d;%d;%d", red, green, blue); +} + +void command_list_reset_color(struct command_list *list) { + add_command(list, RenderCommand_ClearFormat); +} + +void command_list_set_show_whitespace(struct command_list *list, bool show) { + add_command(list, RenderCommand_SetShowWhitespace)->show_ws->show = show; +} + +void display_render(struct display *display, + struct command_list *command_list) { + + struct command_list *cl = command_list; + uint8_t fmt_stack[256] = {0}; + fmt_stack[0] = ESC; + fmt_stack[1] = '['; + fmt_stack[2] = '0'; + uint32_t fmt_stack_len = 3; + bool show_whitespace_state = false; + + for (uint64_t cmdi = 0; cmdi < cl->ncmds; ++cmdi) { + struct render_command *cmd = &cl->cmds[cmdi]; + switch (cmd->type) { + case RenderCommand_DrawText: { + struct draw_text_cmd *txt_cmd = cmd->draw_txt; + display_move_cursor(display, txt_cmd->row + cl->yoffset, + txt_cmd->col + cl->xoffset); + putbytes(fmt_stack, fmt_stack_len, false); + putbyte('m'); + putbytes(txt_cmd->data, txt_cmd->len, show_whitespace_state); + break; + } + + case RenderCommand_Repeat: { + struct repeat_cmd *repeat_cmd = cmd->repeat; + display_move_cursor(display, repeat_cmd->row + cl->yoffset, + repeat_cmd->col + cl->xoffset); + putbytes(fmt_stack, fmt_stack_len, false); + putbyte('m'); + if (show_whitespace_state) { + for (uint32_t i = 0; i < repeat_cmd->nrepeat; ++i) { + putbyte_ws(repeat_cmd->c, show_whitespace_state); + } + } else { + char *buf = malloc(repeat_cmd->nrepeat + 1); + memset(buf, repeat_cmd->c, repeat_cmd->nrepeat); + buf[repeat_cmd->nrepeat] = '\0'; + fputs(buf, stdout); + free(buf); + } + break; + } + + case RenderCommand_PushFormat: { + struct push_fmt_cmd *fmt_cmd = cmd->push_fmt; + + fmt_stack[fmt_stack_len] = ';'; + ++fmt_stack_len; + + memcpy(fmt_stack + fmt_stack_len, fmt_cmd->fmt, fmt_cmd->len); + fmt_stack_len += fmt_cmd->len; + break; + } + + case RenderCommand_ClearFormat: + fmt_stack_len = 3; + break; + + case RenderCommand_SetShowWhitespace: + show_whitespace_state = cmd->show_ws->show; + break; + } + } +} + +void hide_cursor() { + putbyte(ESC); + putbyte('['); + putbyte('?'); + putbyte('2'); + putbyte('5'); + putbyte('l'); +} + +void show_cursor() { + putbyte(ESC); + putbyte('['); + putbyte('?'); + putbyte('2'); + putbyte('5'); + putbyte('h'); +} + +void display_begin_render(struct display *display) { hide_cursor(); } +void display_end_render(struct display *display) { + show_cursor(); + fflush(stdout); +} diff --git a/src/dged/display.h b/src/dged/display.h new file mode 100644 index 0000000..14dd246 --- /dev/null +++ b/src/dged/display.h @@ -0,0 +1,212 @@ +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +struct display; + +struct render_command; +struct command_list; + +/** + * Create a new display + * + * The only implementation of this is currently a termios one. + * @returns A pointer to the display. + */ +struct display *display_create(); + +/** + * Resize the display + * + * Resize the display to match the underlying size of the device. + * @param display The display to resize. + */ +void display_resize(struct display *display); + +/** + * Destroy the display. + * + * Clear all resources associated with the display and reset the underlying + * device to original state. + * @param display The display to destroy. + */ +void display_destroy(struct display *display); + +/** + * Get the current width of the display. + * + * @param display The display to get width for. + * @returns The width in number of chars as a positive integer. + */ +uint32_t display_width(struct display *display); + +/** + * Get the current height of the display. + * + * @param display The display to get height for. + * @returns The height in number of chars as a positive integer. + */ +uint32_t display_height(struct display *display); + +/** + * Clear the display + * + * This will clear all text from the display. + * @param display The display to clear. + */ +void display_clear(struct display *display); + +/** + * Move the cursor to the specified location + * + * Move the cursor to the specified row and column. + * @param display The display to move the cursor for. + * @param row The row to move the cursor to. + * @param col The col to move the cursor to. + */ +void display_move_cursor(struct display *display, uint32_t row, uint32_t col); + +/** + * Start a render pass on the display. + * + * A begin_render call can be followed by any number of render calls followed by + * a single end_render call. + * @param display The display to begin rendering on. + */ +void display_begin_render(struct display *display); + +/** + * Render a command list on the display. + * + * Render a command list on the given display. A command list is a series of + * rendering instructions. + * @param display The display to render on. + * @param command_list The command list to render. + */ +void display_render(struct display *display, struct command_list *command_list); + +/** + * Finish a render pass on the display. + * + * This tells the display that rendering is done for now and a flush is + * triggered to update the display hardware. + * @param display The display to end rendering on. + */ +void display_end_render(struct display *display); + +/** + * Create a new command list. + * + * A command list records a series of commands for drawing text to a display. + * @param capacity The capacity of the command list. + * @param allocator Allocation callback to use for data in the command list. + * @param xoffset Column offset to apply to all operations in the list. + * @param yoffset Row offset to apply to all operations in the list. + * @param name Name for the command list. Useful for debugging. + * @returns A pointer to the created command list. + */ +struct command_list *command_list_create(uint32_t capacity, + void *(*allocator)(size_t), + uint32_t xoffset, uint32_t yoffset, + const char *name); + +/** + * Enable/disable rendering of whitespace characters. + * + * ' ' will be rendered with a dot and '\t' as an arrow. + * @param list Command list to record command in. + * @param show True if whitespace chars should be displayed, false otherwise. + */ +void command_list_set_show_whitespace(struct command_list *list, bool show); + +/** + * Set background color + * + * Set the background color to use for following draw commands to the specified + * index. Note that color indices > 15 might not be supported on all displays. + * @param list The command list to record command in. + * @param color_idx The color index to use as background (0-255) + */ +void command_list_set_index_color_bg(struct command_list *list, + uint8_t color_idx); + +/** + * Set background color + * + * Set the background color to use for following draw commands to the specified + * RGB color. Note that this might not be supported on all displays. + * @param list The command list to record command in. + * @param red Red value. + * @param green Green value. + * @param blue Blue value. + */ +void command_list_set_color_bg(struct command_list *list, uint8_t red, + uint8_t green, uint8_t blue); + +/** + * Set foreground color + * + * Set the foreground color to use for following draw commands to the specified + * index. Note that color indices > 15 might not be supported on all displays. + * @param list The command list to record command in. + * @param color_idx The color index to use as foreground (0-255) + */ +void command_list_set_index_color_fg(struct command_list *list, + uint8_t color_idx); + +/** + * Set foreground color + * + * Set the foreground color to use for following draw commands to the specified + * RGB color. Note that this might not be supported on all displays. + * @param list The command list to record command in. + * @param red Red value. + * @param green Green value. + * @param blue Blue value. + */ +void command_list_set_color_fg(struct command_list *list, uint8_t red, + uint8_t green, uint8_t blue); + +/** + * Reset the color and styling information. + * + * The following draw commands will have their formatting reset to the default. + * @param list The command list to record command in. + */ +void command_list_reset_color(struct command_list *list); + +/** + * Draw text + * + * @param list Command list to record draw command in. + * @param col Column to start text at. + * @param row Row to start text at. + * @param data Bytes to write. + * @param len Number of bytes to write. + */ +void command_list_draw_text(struct command_list *list, uint32_t col, + uint32_t row, uint8_t *data, uint32_t len); + +/** + * Draw text, copying it to internal storage first. + * + * @param list Command list to record draw command in. + * @param col Column to start text at. + * @param row Row to start text at. + * @param data Bytes to write. + * @param len Number of bytes to write. + */ +void command_list_draw_text_copy(struct command_list *list, uint32_t col, + uint32_t row, uint8_t *data, uint32_t len); + +/** + * Draw a repeated character. + * + * @param list Command list to record draw command in. + * @param col Column to start text at. + * @param row Row to start text at. + * @param c Byte to repeat. + * @param nrepeat Number of times to repeat byte. + */ +void command_list_draw_repeated(struct command_list *list, uint32_t col, + uint32_t row, uint8_t c, uint32_t nrepeat); diff --git a/src/dged/hash.h b/src/dged/hash.h new file mode 100644 index 0000000..0fd689b --- /dev/null +++ b/src/dged/hash.h @@ -0,0 +1,11 @@ +#include <stdint.h> + +static uint32_t hash_name(const char *s) { + unsigned long hash = 5381; + int c; + + while ((c = *s++)) + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + + return hash; +} diff --git a/src/dged/hashmap.h b/src/dged/hashmap.h new file mode 100644 index 0000000..405c193 --- /dev/null +++ b/src/dged/hashmap.h @@ -0,0 +1,86 @@ +#ifndef _HASHMAP_H +#define _HASHMAP_H + +#include "vec.h" +#include <stdint.h> + +#define HASHMAP_ENTRY_TYPE(name, entry) \ + struct name { \ + uint32_t key; \ + entry value; \ + } + +#define HASHMAP(entry) \ + struct { \ + VEC(entry) entries; \ + uint32_t (*hash_fn)(const char *); \ + } + +#define HASHMAP_INIT(map, initial_capacity, hasher) \ + VEC_INIT(&(map)->entries, initial_capacity) \ + (map)->hash_fn = hasher; + +#define HASHMAP_DESTROY(map) VEC_DESTROY(&(map)->entries) + +#define HASHMAP_INSERT(map, type, k, v, hash_var) \ + uint32_t key = (map)->hash_fn(k); \ + bool duplicate = false; \ + VEC_FOR_EACH(&(map)->entries, type *pair) { \ + if (pair->key == key) { \ + duplicate = true; \ + break; \ + } \ + } \ + if (!duplicate) { \ + VEC_PUSH(&(map)->entries, ((type){.key = key, .value = v})); \ + } \ + hash_var = key; + +#define HASHMAP_APPEND(map, type, k, var) \ + uint32_t key = (map)->hash_fn(k); \ + bool duplicate = false; \ + VEC_FOR_EACH(&(map)->entries, type *pair) { \ + if (pair->key == key) { \ + duplicate = true; \ + break; \ + } \ + } \ + type *v = NULL; \ + if (!duplicate) { \ + VEC_APPEND(&(map)->entries, v); \ + v->key = key; \ + } \ + var = v; + +#define HASHMAP_GET(map, type, k, var) \ + HASHMAP_GET_BY_HASH(map, type, (map)->hash_fn(k), var) + +#define HASHMAP_GET_BY_HASH(map, type, h, var) \ + type *res = NULL; \ + uint32_t needle = h; \ + VEC_FOR_EACH(&(map)->entries, type *pair) { \ + if (needle == pair->key) { \ + res = pair; \ + break; \ + } \ + } \ + var = res != NULL ? &(res->value) : NULL; + +#define HASHMAP_CONTAINS_KEY(map, key) \ + uint32_t needle = (map)->hash_fn(key); \ + bool exists = false; \ + VEC_FOR_EACH((map)->entries, struct pair *pair) { \ + if (needle == pair->key) { \ + exists = true; \ + break; \ + } \ + } \ + exists + +#define HASHMAP_FOR_EACH(map, var) VEC_FOR_EACH_INDEXED(&(map)->entries, var, i) + +#define HASHMAP_SIZE(map) VEC_SIZE(&(map)->entries) +#define HASHMAP_CAPACITY(map) VEC_CAPACITY(&(map)->entries) +#define HASHMAP_EMPTY(map) HASHMAP_SIZE == 0 + +#endif diff --git a/src/dged/keyboard.c b/src/dged/keyboard.c new file mode 100644 index 0000000..4b142ee --- /dev/null +++ b/src/dged/keyboard.c @@ -0,0 +1,159 @@ +#define _DEFAULT_SOURCE +#include "keyboard.h" +#include "reactor.h" +#include "stdio.h" +#include "utf8.h" + +#include <ctype.h> +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <termios.h> +#include <unistd.h> + +struct keyboard keyboard_create(struct reactor *reactor) { + struct termios term; + tcgetattr(0, &term); + + // set input to non-blocking + term.c_cc[VMIN] = 0; + term.c_cc[VTIME] = 0; + tcsetattr(0, TCSADRAIN, &term); + return keyboard_create_fd(reactor, STDIN_FILENO); +} + +struct keyboard keyboard_create_fd(struct reactor *reactor, int fd) { + return (struct keyboard){ + .fd = fd, + .reactor_event_id = reactor_register_interest(reactor, fd, ReadInterest), + }; +} + +void parse_keys(uint8_t *bytes, uint32_t nbytes, struct key *out_keys, + uint32_t *out_nkeys) { + // TODO: can be optimized if "bytes" contains no special chars + uint32_t nkps = 0; + for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { + uint8_t b = bytes[bytei]; + bool has_more = bytei + 1 < nbytes; + uint8_t next = has_more ? bytes[bytei + 1] : 0; + + struct key *kp = &out_keys[nkps]; + if (b == 0x1b) { // meta + kp->start = bytei; + kp->mod = Meta; + } else if (has_more && isalnum(next) && kp->mod & Meta && + (b == '[' || + b == '0')) { // special char (function keys, pgdn, etc) + kp->mod = Spec; + } else if (b == 0x7f) { // ? + kp->mod |= Ctrl; + kp->key = '?'; + kp->start = bytei; + kp->end = bytei + 1; + ++nkps; + } else if (iscntrl(b)) { // ctrl char + kp->mod |= Ctrl; + kp->key = b | 0x40; + kp->start = bytei; + kp->end = bytei + 1; + ++nkps; + } else { + if (kp->mod & Spec && b == '~') { + // skip tilde in special chars + kp->end = bytei + 1; + ++nkps; + } else if (kp->mod & Meta || kp->mod & Spec) { + kp->key = b; + kp->end = bytei + 1; + + if (kp->mod & Meta || (kp->mod & Spec && next != '~')) { + ++nkps; + } + } else if (utf8_byte_is_unicode_continuation(b)) { + // do nothing for these + } else if (utf8_byte_is_unicode_start(b)) { // unicode char + kp->mod = None; + kp->key = 0; + kp->start = bytei; + kp->end = bytei + utf8_nbytes(bytes + bytei, nbytes - bytei, 1); + ++nkps; + } else { // normal ASCII char + kp->mod = None; + kp->key = b; + kp->start = bytei; + kp->end = bytei + 1; + ++nkps; + } + } + } + + *out_nkeys = nkps; +} + +struct keyboard_update keyboard_update(struct keyboard *kbd, + struct reactor *reactor, + void *(*frame_alloc)(size_t)) { + + struct keyboard_update upd = (struct keyboard_update){ + .keys = NULL, + .nkeys = 0, + .nbytes = 0, + .raw = NULL, + }; + + // check if there is anything to do + if (!reactor_poll_event(reactor, kbd->reactor_event_id)) { + return upd; + } + + // read all input in chunks of `bufsize` bytes + const uint32_t bufsize = 128; + uint8_t *buf = malloc(bufsize), *writepos = buf; + int nbytes = 0, nread = 0; + while ((nread = read(kbd->fd, writepos, bufsize)) == bufsize) { + nbytes += bufsize; + buf = realloc(buf, nbytes + bufsize); + writepos = buf + nbytes; + } + + nbytes += nread; + + if (nbytes > 0) { + upd.nbytes = nbytes; + upd.raw = frame_alloc(nbytes); + memcpy(upd.raw, buf, nbytes); + upd.keys = frame_alloc(sizeof(struct key) * nbytes); + memset(upd.keys, 0, sizeof(struct key) * nbytes); + + parse_keys(upd.raw, upd.nbytes, upd.keys, &upd.nkeys); + } + + free(buf); + return upd; +} + +bool key_equal_char(struct key *key, uint8_t mod, uint8_t c) { + return key->key == c && key->mod == mod; +} + +bool key_equal(struct key *key1, struct key *key2) { + return key_equal_char(key1, key2->mod, key2->key); +} + +uint32_t key_name(struct key *key, char *buf, size_t capacity) { + const char *mod = ""; + switch (key->mod) { + case Ctrl: + mod = "c-"; + break; + case Meta: + mod = "m-"; + break; + case Spec: + mod = "special-"; + break; + } + + snprintf(buf, capacity, "%s%c", mod, tolower(key->key)); +} diff --git a/src/dged/keyboard.h b/src/dged/keyboard.h new file mode 100644 index 0000000..e602b69 --- /dev/null +++ b/src/dged/keyboard.h @@ -0,0 +1,145 @@ +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +/** + * Key press modifiers + * + * Modifiers a key press can have. + */ +enum modifiers { + /** No modifier, bare key press */ + None = 0, + + /** Ctrl key */ + Ctrl = 1 << 0, + + /** Meta (Alt) key */ + Meta = 1 << 1, + + /** Special key (F keys, arrow keys, etc) */ + Spec = 1 << 2, +}; + +/** Backspace key */ +#define BACKSPACE Ctrl, '?' +/** Tab key */ +#define TAB Ctrl, 'I' +/** Enter key */ +#define ENTER Ctrl, 'M' +/** Delete key */ +#define DELETE Spec, '3' + +/** Up arrow key */ +#define UP Spec, 'A' +/** Down arrow key */ +#define DOWN Spec, 'B' +/** Right arrow key */ +#define RIGHT Spec, 'C' +/** Left arrow key */ +#define LEFT Spec, 'D' + +/** + * A key press + */ +struct key { + /** The key pressed, will be 0 for a unicode char */ + uint8_t key; + /** Modifier keys pressed (or-ed together) */ + uint8_t mod; + /** Index where this key press starts in the raw input buffer */ + uint8_t start; + /** Index where this key press ends in the raw input buffer */ + uint8_t end; +}; + +/** + * The keyboard used to input characters. + */ +struct keyboard { + uint32_t reactor_event_id; + int fd; +}; + +/** + * The result of updating the keyboard + */ +struct keyboard_update { + /** The key presses */ + struct key *keys; + /** Number of key presses in @ref keys */ + uint32_t nkeys; + + /** The raw input */ + uint8_t *raw; + /** The number of bytes in the raw input */ + uint32_t nbytes; +}; + +struct reactor; + +/** + * Create a new keyboard + * + * @param reactor @ref reactor "Reactor" to use for polling keyboard for + * readiness. + * @returns The created keyboard. + */ +struct keyboard keyboard_create(struct reactor *reactor); + +/** + * Create a new keyboard, reading input from fd + * + * @param reactor @ref reactor "Reactor" to use for polling keyboard for + * readiness. + * @param fd The file descriptor to get input from + * @returns The created keyboard. + */ +struct keyboard keyboard_create_fd(struct reactor *reactor, int fd); + +/** + * Update the keyboard. + * + * This will check the reactor for readiness to avoid blocking. If there is + * data, it will be read and converted to key presses. + * + * @param kbd The @ref keyboard to update. + * @param reactor The @ref reactor used when creating the @ref keyboard. + * @returns An instance of @ref keyboard_update representing the result of the + * update operation. + */ +struct keyboard_update keyboard_update(struct keyboard *kbd, + struct reactor *reactor, + void *(*frame_alloc)(size_t)); + +/** + * Does key represent the same key press as mod and c. + * + * @param key The key to check. + * @param mod Modifier of a key to compare against. + * @param c Char of a key to compare against. + * @returns true if key represents the same key press as mod together with c, + * false otherwise + */ +bool key_equal_char(struct key *key, uint8_t mod, uint8_t c); + +/** + * Does key1 represent the same key press as key2? + * + * @param key1 First key to compare. + * @param key2 Second key to compare. + * @returns true if key1 and key2 represents the same key press, false + * otherwise. + */ +bool key_equal(struct key *key1, struct key *key2); + +/** + * Get a text representation of a key + * + * @param key @ref key "Key" to get text representation for. + * @param buf character buffer for holding the result. + * @param capacity The capacity of buf. + * + * @returns The number of characters written to buf. + */ +uint32_t key_name(struct key *key, char *buf, size_t capacity); diff --git a/src/dged/lang.c b/src/dged/lang.c new file mode 100644 index 0000000..6919780 --- /dev/null +++ b/src/dged/lang.c @@ -0,0 +1,160 @@ +#include "lang.h" +#include "minibuffer.h" +#include "settings.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +void define_lang(const char *name, const char *id, const char *extensions, + uint32_t tab_width, const char *lang_srv) { + char namebuf[128] = {0}; + + size_t offs = snprintf(namebuf, 128, "languages.%s.", id); + + char *b = namebuf + offs; + snprintf(b, 128 - offs, "%s", "extensions"); + settings_register_setting( + namebuf, (struct setting_value){.type = Setting_String, + .string_value = (char *)extensions}); + + snprintf(b, 128 - offs, "%s", "tab-width"); + settings_register_setting(namebuf, + (struct setting_value){.type = Setting_Number, + .number_value = tab_width}); + + snprintf(b, 128 - offs, "%s", "lang-srv"); + settings_register_setting( + namebuf, (struct setting_value){.type = Setting_String, + .string_value = (char *)lang_srv}); + + snprintf(b, 128 - offs, "%s", "name"); + settings_register_setting( + namebuf, (struct setting_value){.type = Setting_String, + .string_value = (char *)name}); +} + +static struct language g_fundamental = { + .name = "Fundamental", + .tab_width = 4, + .lang_srv = NULL, +}; + +void languages_init(bool register_default) { + if (register_default) { + define_lang("C", "c", "c h", 2, "clangd"); + define_lang("C++", "cxx", "cpp cxx cc c++ hh h", 2, "clangd"); + define_lang("Rust", "rs", "rs", 4, "rust-analyzer"); + define_lang("Nix", "nix", "nix", 4, "rnix-lsp"); + } +} + +struct language lang_from_settings(const char *lang_path) { + char setting_name_buf[128] = {0}; + size_t offs = snprintf(setting_name_buf, 128, "%s.", lang_path); + char *b = setting_name_buf + offs; + + struct language l; + + snprintf(b, 128 - offs, "%s", "name"); + struct setting *name = settings_get(setting_name_buf); + l.name = name != NULL ? name->value.string_value : "Unknown"; + + snprintf(b, 128 - offs, "%s", "tab-width"); + struct setting *tab_width = settings_get(setting_name_buf); + + // fall back to global value + if (tab_width == NULL) { + tab_width = settings_get("editor.tab-width"); + } + l.tab_width = tab_width != NULL ? tab_width->value.number_value : 4; + + snprintf(b, 128 - offs, "%s", "lang-srv"); + struct setting *lang_srv = settings_get(setting_name_buf); + l.lang_srv = lang_srv->value.string_value; + + return l; +} + +void next_ext(const char *curr, const char **nxt, const char **end) { + if (curr == NULL) { + *nxt = *end = NULL; + return; + } + + *nxt = curr; + *end = curr + strlen(curr); + + const char *spc = strchr(curr, ' '); + if (spc != NULL) { + *end = spc; + } +} + +struct language lang_from_extension(const char *ext) { + + uint32_t extlen = strlen(ext); + if (extlen == 0) { + return g_fundamental; + } + + // get "languages.*" settings + struct setting **settings = NULL; + uint32_t nsettings = 0; + settings_get_prefix("languages.", &settings, &nsettings); + + // find the first one with a matching extension list + for (uint32_t i = 0; i < nsettings; ++i) { + struct setting *setting = settings[i]; + char *setting_name = strrchr(setting->path, '.'); + if (setting_name != NULL && + strncmp(setting_name + 1, "extensions", 10) == 0) { + const char *val = setting->value.string_value; + + // go over extensions + const char *cext = val, *nxt = NULL, *end = NULL; + next_ext(cext, &nxt, &end); + while (nxt != end) { + if (extlen == (end - nxt) && strncmp(ext, nxt, (end - nxt)) == 0) { + char lang_path[128] = {0}; + strncpy(lang_path, setting->path, setting_name - setting->path); + + free(settings); + return lang_from_settings(lang_path); + } + + cext = end + 1; + next_ext(cext, &nxt, &end); + } + } + } + + free(settings); + + // fall back to fundamental + return g_fundamental; +} + +struct language lang_from_id(const char *id) { + if (id == NULL || (strlen(id) == 3 && strncmp(id, "fnd", 3) == 0) || + strlen(id) == 0) { + return g_fundamental; + } + + char lang_path[128] = {0}; + snprintf(lang_path, 128, "languages.%s", id); + + // check that it exists + struct setting **settings = NULL; + uint32_t nsettings = 0; + + settings_get_prefix(lang_path, &settings, &nsettings); + free(settings); + + if (nsettings > 0) { + return lang_from_settings(lang_path); + } else { + minibuffer_echo_timeout(4, "failed to find language \"%s\"", id); + return lang_from_settings("languages.fnd"); + } +} diff --git a/src/dged/lang.h b/src/dged/lang.h new file mode 100644 index 0000000..984e207 --- /dev/null +++ b/src/dged/lang.h @@ -0,0 +1,46 @@ +#ifndef _LANG_H +#define _LANG_H + +#include <stdbool.h> +#include <stdint.h> + +/** + * Settings for a programming language. + */ +struct language { + /** Descriptive name of the programming language */ + const char *name; + + /** Tab width for indentation */ + uint32_t tab_width; + + /** Path to the language server */ + const char *lang_srv; +}; + +/** + * Initialize languages. + * + * @param[in] register_default Set to true to register some well known + * languages. + */ +void languages_init(bool register_default); + +/** + * Get a language config by file name extension. + * + * @param[in] ext File extension + * @returns A language config instance or the default language if not found. + */ +struct language lang_from_extension(const char *ext); + +/** + * Get a language config by id. The language id is a short (all-lowercase) + * string identifying the language. + * + * @param[in] id The language id. + * @returns A language config instance or the default language if not found. + */ +struct language lang_from_id(const char *id); + +#endif diff --git a/src/dged/minibuffer.c b/src/dged/minibuffer.c new file mode 100644 index 0000000..0ff32a8 --- /dev/null +++ b/src/dged/minibuffer.c @@ -0,0 +1,223 @@ +#include "minibuffer.h" +#include "binding.h" +#include "buffer.h" +#include "command.h" +#include "display.h" + +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static struct minibuffer { + struct buffer *buffer; + struct timespec expires; + + char prompt[128]; + struct command_ctx prompt_command_ctx; + bool prompt_active; + bool clear; + + void (*update_callback)(); + +} g_minibuffer = {0}; + +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, 0, (uint8_t *)g_minibuffer.prompt, len); + command_list_reset_color(commands); +} + +int32_t minibuffer_execute() { + if (g_minibuffer.prompt_active) { + struct command_ctx *c = &g_minibuffer.prompt_command_ctx; + + struct text_chunk line = minibuffer_content(); + char *l = (char *)malloc(line.nbytes + 1); + memcpy(l, line.text, line.nbytes); + l[line.nbytes] = '\0'; + + // propagate any saved arguments + char *argv[64]; + for (uint32_t i = 0; i < c->saved_argc; ++i) { + argv[i] = (char *)c->saved_argv[i]; + } + argv[c->saved_argc] = l; + uint32_t argc = c->saved_argc + (line.nbytes > 0 ? 1 : 0); + + // split on ' ' + for (uint32_t bytei = 0; bytei < line.nbytes; ++bytei) { + uint8_t byte = line.text[bytei]; + if (byte == ' ' && argc < 64) { + l[bytei] = '\0'; + argv[argc] = l + bytei + 1; + ++argc; + } + } + + minibuffer_abort_prompt(); + int32_t res = execute_command(c->self, c->commands, c->active_window, + c->buffers, argc, (const char **)argv); + + free(l); + + return res; + } else { + return 0; + } +} + +struct update_hook_result update(struct buffer_view *view, + struct command_list *commands, uint32_t width, + uint32_t height, uint64_t frame_time, + void *userdata) { + struct timespec current; + struct minibuffer *mb = (struct minibuffer *)userdata; + clock_gettime(CLOCK_MONOTONIC, ¤t); + if ((!mb->prompt_active && current.tv_sec >= mb->expires.tv_sec) || + mb->clear) { + buffer_clear(view); + mb->clear = false; + } + + struct update_hook_result res = {0}; + if (mb->prompt_active) { + res.margins.left = strlen(mb->prompt); + draw_prompt(commands, NULL); + } + + if (mb->update_callback != NULL) { + mb->update_callback(); + } + + return res; +} + +void minibuffer_init(struct buffer *buffer) { + if (g_minibuffer.buffer != NULL) { + return; + } + + g_minibuffer.buffer = buffer; + g_minibuffer.clear = false; + buffer_add_update_hook(g_minibuffer.buffer, update, &g_minibuffer); +} + +void echo(uint32_t timeout, const char *fmt, va_list args) { + if (g_minibuffer.prompt_active || g_minibuffer.buffer == NULL) { + return; + } + + char buff[2048]; + size_t nbytes = vsnprintf(buff, 2048, fmt, args); + + // vsnprintf returns how many characters it would have wanted to write in case + // of overflow + buffer_set_text(g_minibuffer.buffer, (uint8_t *)buff, + nbytes > 2048 ? 2048 : nbytes); + g_minibuffer.clear = false; + + clock_gettime(CLOCK_MONOTONIC, &g_minibuffer.expires); + g_minibuffer.expires.tv_sec += timeout; +} + +void minibuffer_destroy() { + command_ctx_free(&g_minibuffer.prompt_command_ctx); +} + +struct text_chunk minibuffer_content() { + return buffer_get_line(g_minibuffer.buffer, 0); +} + +struct buffer *minibuffer_buffer() { + return g_minibuffer.buffer; +} + +void minibuffer_echo(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + echo(1000, fmt, args); + va_end(args); +} + +void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + echo(timeout, fmt, args); + va_end(args); +} + +void minibuffer_set_prompt_internal(const char *fmt, va_list args) { + vsnprintf(g_minibuffer.prompt, sizeof(g_minibuffer.prompt), fmt, args); +} + +int32_t minibuffer_prompt_internal(struct command_ctx command_ctx, + void (*update_callback)(), const char *fmt, + va_list args) { + if (g_minibuffer.buffer == NULL) { + return 1; + } + + minibuffer_clear(); + g_minibuffer.prompt_active = true; + + command_ctx_free(&g_minibuffer.prompt_command_ctx); + g_minibuffer.prompt_command_ctx = command_ctx; + + minibuffer_set_prompt_internal(fmt, args); + + if (update_callback != NULL) { + g_minibuffer.update_callback = update_callback; + } + + return 0; +} + +int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, + ...) { + va_list args; + va_start(args, fmt); + int32_t r = minibuffer_prompt_internal(command_ctx, NULL, fmt, args); + va_end(args); + + return r; +} + +int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx, + void (*update_callback)(), + const char *fmt, ...) { + va_list args; + va_start(args, fmt); + int32_t r = + minibuffer_prompt_internal(command_ctx, update_callback, fmt, args); + va_end(args); + + return r; +} + +void minibuffer_set_prompt(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + minibuffer_set_prompt_internal(fmt, args); + va_end(args); +} + +void minibuffer_abort_prompt() { + minibuffer_clear(); + g_minibuffer.update_callback = NULL; + g_minibuffer.prompt_active = false; +} + +bool minibuffer_empty() { return !minibuffer_displaying(); } + +bool minibuffer_displaying() { + return g_minibuffer.buffer != NULL && !buffer_is_empty(g_minibuffer.buffer); +} + +void minibuffer_clear() { + g_minibuffer.expires.tv_sec = 0; + g_minibuffer.clear = true; +} + +bool minibuffer_focused() { return g_minibuffer.prompt_active; } diff --git a/src/dged/minibuffer.h b/src/dged/minibuffer.h new file mode 100644 index 0000000..24f54cf --- /dev/null +++ b/src/dged/minibuffer.h @@ -0,0 +1,108 @@ +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> +#include <time.h> + +struct buffer; +struct command_ctx; +struct keymap; + +/** + * Initialize the minibuffer. + * + * Note that the minibuffer is a global instance and this function will do + * nothing if called more than once. + * @param buffer underlying buffer to use for text IO in the minibuffer. + */ +void minibuffer_init(struct buffer *buffer); + +/** + * Destroy the minibuffer + * + * Note that this does not release the buffer used. + */ +void minibuffer_destroy(); + +struct text_chunk minibuffer_content(); + +struct buffer *minibuffer_buffer(); + +/** + * Echo a message to the minibuffer. + * + * @param fmt Format string for the message. + * @param ... Format arguments. + */ +void minibuffer_echo(const char *fmt, ...); + +/** + * Echo a message to the minibuffer that disappears after @ref timeout. + * + * @param timeout The timeout in seconds after which the message should + * disappear. + * @param fmt Format string for the message. + * @param ... Format arguments. + */ +void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...); + +/** + * Prompt for user input in the minibuffer. + * + * This will move focus to the minibuffer and wait for user input, with the + * given prompt. + * @param command_ctx The command context to use to re-execute the calling + * command (or other command) when the user confirms the input. + * @param fmt Format string for the prompt. + * @param ... Format arguments. + * @returns 0 on success. + */ +int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...); + +int32_t minibuffer_prompt_interactive(struct command_ctx command_ctx, + void (*update_callback)(), + const char *fmt, ...); + +void minibuffer_set_prompt(const char *fmt, ...); + +/** + * Evaluate the current contents of the minibuffer + * + * @returns zero on success, non-zero to indicate failure + */ +int32_t minibuffer_execute(); + +/** + * Abort the current minibuffer prompt. + * + * This returns focus to the previously focused window. + */ +void minibuffer_abort_prompt(); + +/** + * Minibuffer prompt args + */ +struct minibuffer_prompt_args { + int argc; + const char **argv; +}; + +/** + * Clear the current text in the minibuffer. + */ +void minibuffer_clear(); + +bool minibuffer_empty(); + +/** + * Is the minibuffer currently displaying something? + * + * @returns True if the minibuffer is displaying anything, false otherwise. + */ +bool minibuffer_displaying(); + +/** + * Is the minibuffer currently focused? + * + * @returns True if the minibuffer is currently focused, receiving user input. + */ +bool minibuffer_focused(); diff --git a/src/dged/reactor-epoll.c b/src/dged/reactor-epoll.c new file mode 100644 index 0000000..e488fef --- /dev/null +++ b/src/dged/reactor-epoll.c @@ -0,0 +1,76 @@ +#include "reactor.h" + +#include <stdio.h> +#include <stdlib.h> +#include <sys/epoll.h> + +struct reactor { + int epoll_fd; + void *events; +}; + +struct events { + struct epoll_event events[10]; + uint32_t nevents; +}; + +struct reactor *reactor_create() { + int epollfd = epoll_create1(0); + if (epollfd == -1) { + perror("epoll_create1"); + } + + struct reactor *r = (struct reactor *)calloc(1, sizeof(struct reactor)); + r->epoll_fd = epollfd; + r->events = calloc(1, sizeof(struct events)); + + return r; +} + +void reactor_destroy(struct reactor *reactor) { + free(reactor->events); + free(reactor); +} + +uint32_t reactor_register_interest(struct reactor *reactor, int fd, + enum interest interest) { + struct epoll_event ev; + ev.events = 0; + ev.events |= (interest & ReadInterest) != 0 ? EPOLLIN : 0; + ev.events |= (interest & WriteInterest) != 0 ? EPOLLOUT : 0; + ev.data.fd = fd; + if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + perror("epoll_ctl"); + return -1; + } + + return fd; +} + +void reactor_unregister_interest(struct reactor *reactor, uint32_t ev_id) { + epoll_ctl(reactor->epoll_fd, EPOLL_CTL_DEL, ev_id, NULL); +} + +bool reactor_poll_event(struct reactor *reactor, uint32_t ev_id) { + struct events *events = (struct events *)reactor->events; + for (uint32_t ei = 0; ei < events->nevents; ++ei) { + struct epoll_event *ev = &events->events[ei]; + + if (ev->data.fd == ev_id) { + return true; + } + } + + return false; +} + +void reactor_update(struct reactor *reactor) { + struct events *events = (struct events *)reactor->events; + int nfds = epoll_wait(reactor->epoll_fd, events->events, 10, -1); + + if (nfds == -1) { + // TODO: log failure + } + + events->nevents = nfds; +} diff --git a/src/dged/reactor.h b/src/dged/reactor.h new file mode 100644 index 0000000..e54afda --- /dev/null +++ b/src/dged/reactor.h @@ -0,0 +1,17 @@ +#include <stdbool.h> +#include <stdint.h> + +enum interest { + ReadInterest = 1, + WriteInterest = 2, +}; + +struct reactor; + +struct reactor *reactor_create(); +void reactor_destroy(struct reactor *reactor); +void reactor_update(struct reactor *reactor); +bool reactor_poll_event(struct reactor *reactor, uint32_t ev_id); +uint32_t reactor_register_interest(struct reactor *reactor, int fd, + enum interest interest); +void reactor_unregister_interest(struct reactor *reactor, uint32_t ev_id); diff --git a/src/dged/settings.c b/src/dged/settings.c new file mode 100644 index 0000000..524aa9b --- /dev/null +++ b/src/dged/settings.c @@ -0,0 +1,95 @@ +#include "settings.h" +#include "command.h" +#include "hash.h" +#include "hashmap.h" +#include "minibuffer.h" +#include "vec.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static struct settings g_settings = {0}; + +void settings_init(uint32_t initial_capacity) { + HASHMAP_INIT(&g_settings.settings, initial_capacity, hash_name); +} + +void settings_destroy() { + HASHMAP_FOR_EACH(&g_settings.settings, struct setting_entry * entry) { + struct setting *setting = &entry->value; + if (setting->value.type == Setting_String) { + free(setting->value.string_value); + } + } + + HASHMAP_DESTROY(&g_settings.settings); +} + +void setting_set_value(struct setting *setting, struct setting_value val) { + if (setting->value.type == val.type) { + if (setting->value.type == Setting_String && val.string_value != NULL) { + setting->value.string_value = strdup(val.string_value); + } else { + setting->value = val; + } + } +} + +void settings_register_setting(const char *path, + struct setting_value default_value) { + HASHMAP_APPEND(&g_settings.settings, struct setting_entry, path, + struct setting_entry * s); + + if (s != NULL) { + struct setting *new_setting = &s->value; + new_setting->value.type = default_value.type; + setting_set_value(new_setting, default_value); + strncpy(new_setting->path, path, 128); + new_setting->path[127] = '\0'; + } +} + +struct setting *settings_get(const char *path) { + HASHMAP_GET(&g_settings.settings, struct setting_entry, path, + struct setting * s); + return s; +} + +void settings_get_prefix(const char *prefix, struct setting **settings_out[], + uint32_t *nsettings_out) { + + uint32_t capacity = 16; + VEC(struct setting *) res; + VEC_INIT(&res, 16); + HASHMAP_FOR_EACH(&g_settings.settings, struct setting_entry * entry) { + struct setting *setting = &entry->value; + if (strncmp(prefix, setting->path, strlen(prefix)) == 0) { + VEC_PUSH(&res, setting); + } + } + + *nsettings_out = VEC_SIZE(&res); + *settings_out = VEC_ENTRIES(&res); +} + +void settings_set(const char *path, struct setting_value value) { + struct setting *setting = settings_get(path); + if (setting != NULL) { + setting_set_value(setting, value); + } +} + +void setting_to_string(struct setting *setting, char *buf, size_t n) { + switch (setting->value.type) { + case Setting_Bool: + snprintf(buf, n, "%s", setting->value.bool_value ? "true" : "false"); + break; + case Setting_Number: + snprintf(buf, n, "%ld", setting->value.number_value); + break; + case Setting_String: + snprintf(buf, n, "%s", setting->value.string_value); + break; + } +} diff --git a/src/dged/settings.h b/src/dged/settings.h new file mode 100644 index 0000000..5d245d9 --- /dev/null +++ b/src/dged/settings.h @@ -0,0 +1,137 @@ +#include "hashmap.h" + +#include <stdbool.h> +#include <stdint.h> + +struct commands; + +/** + * The type of setting value. + */ +enum setting_type { + /** String setting. */ + Setting_String = 0, + + /** Number setting (a signed 64 bit integer). */ + Setting_Number, + + /** Boolean setting. */ + Setting_Bool, +}; + +/** + * Value for a setting. + */ +struct setting_value { + /** Type of setting. */ + enum setting_type type; + + union { + /** String setting. */ + char *string_value; + + /** Real number setting. */ + int64_t number_value; + + /** Boolean setting value. */ + bool bool_value; + }; +}; + +/** + * A single setting. + * + * A setting has a "path", denoted by a string + * containing a number (0-) of dots. + * Example: editor.tab-width. + */ +struct setting { + + /** Path of the setting. */ + char path[128]; + + /** Value of the setting. */ + struct setting_value value; +}; + +HASHMAP_ENTRY_TYPE(setting_entry, struct setting); + +/** + * A collection of settings. + */ +struct settings { + HASHMAP(struct setting_entry) settings; +}; + +/** + * Initialize the global collection of settings. + * + * @param initial_capacity Initial capacity of the settings collection. + * @returns Nothing, the settings collection is a global instance. + */ +void settings_init(uint32_t initial_capacity); + +/** + * Destroy the global collection of settings. + */ +void settings_destroy(); + +/** + * Register a new setting. + * + * @param path The path of the new setting on + * the form <category>.<sub-category>.<setting-name>. + * @param default_value The default value for the setting. + * All settings are required to declare a default value. + */ +void settings_register_setting(const char *path, + struct setting_value default_value); + +/** + * Retrieve a single setting by path. + * + * @param path The exact path of the setting on + * the form <category>.<sub-category>.<setting-name>. + * @returns A pointer to the setting if found, NULL otherwise. + */ +struct setting *settings_get(const char *path); + +/** + * Retrieve a collection of settings by prefix + * + * @param prefix Path prefix for the settings to retrieve. + * @param settings_out Pointer to an array that will be modified to point to the + * result. + * @param nsettings_out Pointer to an integer that will be set to the number of + * settings pointers in the result. + */ +void settings_get_prefix(const char *prefix, struct setting **settings_out[], + uint32_t *nsettings_out); + +/** + * Set a value for a setting. + * + * @param path The exact path of the setting on + * the form <category>.<sub-category>.<setting-name>. + * @param value The new value of the setting. The type has to match the declared + * type for the setting. If not, the new value is ignored. + */ +void settings_set(const char *path, struct setting_value value); + +/** + * Set a value for a setting. + * + * @param setting Pointer to a setting to set. + * @param value The new value of the setting. The type has to match the declared + * type for the setting. If not, the new value is ignored. + */ +void setting_set_value(struct setting *setting, struct setting_value val); + +/** + * Create a string representation for a setting. + * + * @param setting Pointer to a setting to turn into a string. + * @param buf Character buffer to store resulting string in. + * @param n Size in bytes of @ref buf. + */ +void setting_to_string(struct setting *setting, char *buf, size_t n); diff --git a/src/dged/text.c b/src/dged/text.c new file mode 100644 index 0000000..f8ba72d --- /dev/null +++ b/src/dged/text.c @@ -0,0 +1,496 @@ +#include "text.h" + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "display.h" +#include "signal.h" +#include "utf8.h" + +enum flags { + LineChanged = 1 << 0, +}; + +struct line { + uint8_t *data; + uint8_t flags; + uint32_t nbytes; + uint32_t nchars; +}; + +struct text { + // raw bytes without any null terminators + struct line *lines; + uint32_t nlines; + uint32_t capacity; +}; + +struct text *text_create(uint32_t initial_capacity) { + struct text *txt = calloc(1, sizeof(struct text)); + txt->lines = calloc(initial_capacity, sizeof(struct line)); + txt->capacity = initial_capacity; + txt->nlines = 0; + + return txt; +} + +void text_destroy(struct text *text) { + for (uint32_t li = 0; li < text->nlines; ++li) { + free(text->lines[li].data); + text->lines[li].data = NULL; + text->lines[li].flags = 0; + text->lines[li].nbytes = 0; + text->lines[li].nchars = 0; + } + + free(text->lines); + + free(text); +} + +void text_clear(struct text *text) { + for (uint32_t li = 0; li < text->nlines; ++li) { + free(text->lines[li].data); + text->lines[li].data = NULL; + text->lines[li].flags = 0; + text->lines[li].nbytes = 0; + text->lines[li].nchars = 0; + } + + text->nlines = 0; +} + +// given `char_idx` as a character index, return the byte index +uint32_t charidx_to_byteidx(struct line *line, uint32_t char_idx) { + if (char_idx > line->nchars) { + return line->nbytes; + } + return utf8_nbytes(line->data, line->nbytes, char_idx); +} + +uint32_t text_col_to_byteindex(struct text *text, uint32_t line, uint32_t col) { + return charidx_to_byteidx(&text->lines[line], col); +} + +// given `byte_idx` as a byte index, return the character index +uint32_t byteidx_to_charidx(struct line *line, uint32_t byte_idx) { + if (byte_idx > line->nbytes) { + return line->nchars; + } + + return utf8_nchars(line->data, byte_idx); +} + +uint32_t text_byteindex_to_col(struct text *text, uint32_t line, + uint32_t byteindex) { + return byteidx_to_charidx(&text->lines[line], byteindex); +} + +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; + } + + ensure_line(text, line); + + struct line *l = &text->lines[line]; + + 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 < l->nbytes) { + uint32_t start = bytei + len; + memmove(l->data + start, l->data + bytei, l->nbytes - start); + } + + // insert new chars + memcpy(l->data + bytei, data, len); +} + +uint32_t text_line_length(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nchars; +} + +uint32_t text_line_size(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nbytes; +} + +uint32_t text_num_lines(struct text *text) { return text->nlines; } + +void split_line(uint32_t col, struct line *line, struct line *next) { + uint8_t *data = line->data; + uint32_t nbytes = line->nbytes; + uint32_t nchars = line->nchars; + + uint32_t chari = col; + uint32_t bytei = charidx_to_byteidx(line, chari); + + line->nbytes = bytei; + line->nchars = chari; + next->nbytes = nbytes - bytei; + next->nchars = nchars - chari; + line->flags = next->flags = line->flags; + + next->data = NULL; + line->data = NULL; + + // first, handle some cases where the new line or the pre-existing one is + // empty + if (next->nbytes == 0) { + line->data = data; + } else if (line->nbytes == 0) { + next->data = data; + } else { + // actually split the line + next->data = (uint8_t *)realloc(next->data, next->nbytes); + memcpy(next->data, data + bytei, next->nbytes); + + line->data = (uint8_t *)realloc(line->data, line->nbytes); + memcpy(line->data, data, line->nbytes); + + free(data); + } +} + +void mark_lines_changed(struct text *text, uint32_t line, uint32_t nlines) { + for (uint32_t linei = line; linei < (line + nlines); ++linei) { + text->lines[linei].flags |= LineChanged; + } +} + +void shift_lines(struct text *text, uint32_t start, int32_t direction) { + struct line *dest = &text->lines[((int64_t)start + direction)]; + struct line *src = &text->lines[start]; + uint32_t nlines = text->nlines - (dest > src ? (start + direction) : start); + memmove(dest, src, nlines * sizeof(struct line)); +} + +void new_line_at(struct text *text, uint32_t line, uint32_t col) { + ensure_line(text, line); + + uint32_t newline = line + 1; + append_empty_lines(text, 1); + + mark_lines_changed(text, line, text->nlines - line); + + // 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 + split_line(col, &text->lines[line], &text->lines[newline]); +} + +void delete_line(struct text *text, uint32_t line) { + if (text->nlines == 0) { + return; + } + + mark_lines_changed(text, line, text->nlines - line); + + free(text->lines[line].data); + text->lines[line].data = NULL; + + if (line + 1 < text->nlines) { + shift_lines(text, line + 1, -1); + } + + --text->nlines; + text->lines[text->nlines].data = NULL; + text->lines[text->nlines].nbytes = 0; + text->lines[text->nlines].nchars = 0; +} + +void text_insert_at_inner(struct text *text, uint32_t line, uint32_t col, + uint8_t *bytes, uint32_t nbytes, + uint32_t *lines_added, uint32_t *cols_added, + bool force_newline) { + uint32_t linelen = 0, start_line = line; + + *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(text, line, col, line_data, linelen, nchars); + + col += nchars; + + // only insert a newline if we have to + if (force_newline || linelen == 0 || col < text_line_length(text, line) || + line + 1 < text->nlines) { + new_line_at(text, line, col); + } + + ++line; + linelen = 0; + col = 0; + } else { + ++linelen; + } + } + + // handle remaining + if (linelen > 0) { + uint8_t *line_data = bytes + (nbytes - linelen); + uint32_t nchars = utf8_nchars(line_data, linelen); + insert_at(text, line, col, line_data, linelen, nchars); + *cols_added = nchars; + } + + *lines_added = line - start_line; +} + +void text_append(struct text *text, uint8_t *bytes, uint32_t nbytes, + uint32_t *lines_added, uint32_t *cols_added) { + uint32_t line = text->nlines > 0 ? text->nlines - 1 : 0; + uint32_t col = text_line_length(text, line); + + text_insert_at_inner(text, line, col, bytes, nbytes, lines_added, cols_added, + true); +} + +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) { + text_insert_at_inner(text, line, col, bytes, nbytes, lines_added, cols_added, + false); +} + +void text_delete(struct text *text, uint32_t start_line, uint32_t start_col, + uint32_t end_line, uint32_t end_col) { + + if (text->nlines == 0) { + return; + } + + uint32_t maxline = text->nlines > 0 ? text->nlines - 1 : 0; + + // make sure we stay inside + if (start_line > maxline) { + start_line = maxline; + start_col = text->lines[start_line].nchars > 0 + ? text->lines[start_line].nchars - 1 + : 0; + } + + if (end_line > maxline) { + end_line = maxline; + end_col = text->lines[end_line].nchars; + } + + struct line *firstline = &text->lines[start_line]; + struct line *lastline = &text->lines[end_line]; + + // clamp column + if (start_col > firstline->nchars) { + start_col = firstline->nchars > 0 ? firstline->nchars - 1 : 0; + } + + // handle deletion of newlines + if (end_col > lastline->nchars) { + if (end_line + 1 < text->nlines) { + end_col = 0; + ++end_line; + lastline = &text->lines[end_line]; + } else { + end_col = lastline->nchars; + } + } + + uint32_t bytei = utf8_nbytes(lastline->data, lastline->nbytes, end_col); + if (lastline == firstline) { + // in this case we can "overwrite" + uint32_t dstbytei = + utf8_nbytes(firstline->data, firstline->nbytes, start_col); + memcpy(firstline->data + dstbytei, lastline->data + bytei, + lastline->nbytes - bytei); + } else { + // otherwise we actually have to copy from the last line + 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); + firstline->nbytes = + utf8_nbytes(firstline->data, firstline->nbytes, start_col) + + (lastline->nbytes - bytei); + + // delete full lines, backwards to not shift old, crappy data upwards + for (uint32_t linei = end_line >= text->nlines ? end_line - 1 : end_line; + 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) { + // if representation of text is changed, this can be changed as well + text_for_each_line(text, 0, text->nlines, callback, userdata); +} + +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + chunk_cb callback, void *userdata) { + uint32_t nlines_max = + (line + nlines) > text->nlines ? text->nlines : (line + nlines); + for (uint32_t li = line; li < nlines_max; ++li) { + struct line *src_line = &text->lines[li]; + struct text_chunk line = (struct text_chunk){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + .line = li, + }; + callback(&line, userdata); + } +} + +struct text_chunk text_get_line(struct text *text, uint32_t line) { + struct line *src_line = &text->lines[line]; + return (struct text_chunk){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + .line = line, + }; +} + +struct copy_cmd { + uint32_t line; + uint32_t byteoffset; + uint32_t nbytes; +}; + +struct text_chunk text_get_region(struct text *text, uint32_t start_line, + uint32_t start_col, uint32_t end_line, + uint32_t end_col) { + if (start_line == end_line && start_col == end_col) { + return (struct text_chunk){0}; + } + + struct line *first_line = &text->lines[start_line]; + struct line *last_line = &text->lines[end_line]; + + if (start_col > first_line->nchars) { + return (struct text_chunk){0}; + } + + // handle copying of newlines + if (end_col > last_line->nchars) { + ++end_line; + end_col = 0; + last_line = &text->lines[end_line]; + } + + uint32_t nlines = end_line - start_line + 1; + struct copy_cmd *copy_cmds = calloc(nlines, sizeof(struct copy_cmd)); + + uint32_t total_chars = 0, total_bytes = 0; + for (uint32_t line = start_line; line <= end_line; ++line) { + struct line *l = &text->lines[line]; + total_chars += l->nchars; + total_bytes += l->nbytes; + + struct copy_cmd *cmd = ©_cmds[line - start_line]; + cmd->line = line; + cmd->byteoffset = 0; + cmd->nbytes = l->nbytes; + } + + // correct first line + struct copy_cmd *cmd_first = ©_cmds[0]; + uint32_t byteoff = + utf8_nbytes(first_line->data, first_line->nbytes, start_col); + cmd_first->byteoffset += byteoff; + cmd_first->nbytes -= byteoff; + total_bytes -= byteoff; + total_chars -= start_col; + + // correct last line + struct copy_cmd *cmd_last = ©_cmds[nlines - 1]; + uint32_t byteindex = utf8_nbytes(last_line->data, last_line->nbytes, end_col); + cmd_last->nbytes -= (last_line->nbytes - byteindex); + total_bytes -= (last_line->nbytes - byteindex); + total_chars -= (last_line->nchars - end_col); + + 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->byteoffset, c->nbytes); + curr += c->nbytes; + + if (cmdi != (nlines - 1)) { + data[curr] = '\n'; + ++curr; + ++total_bytes; + ++total_chars; + } + } + + free(copy_cmds); + return (struct text_chunk){ + .text = data, + .line = 0, + .nbytes = total_bytes, + .nchars = total_chars, + .allocated = true, + }; +} + +bool text_line_contains_unicode(struct text *text, uint32_t line) { + return text->lines[line].nbytes != text->lines[line].nchars; +} diff --git a/src/dged/text.h b/src/dged/text.h new file mode 100644 index 0000000..fbee89b --- /dev/null +++ b/src/dged/text.h @@ -0,0 +1,54 @@ +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +// opaque so it is easier to change representation to gap, rope etc. +struct text; + +struct render_command; + +struct text *text_create(uint32_t initial_capacity); +void text_destroy(struct text *text); + +/** + * Clear the text without reclaiming memory + */ +void text_clear(struct text *text); + +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); + +void text_append(struct text *text, uint8_t *bytes, uint32_t nbytes, + uint32_t *lines_added, uint32_t *cols_added); + +void text_delete(struct text *text, uint32_t start_line, uint32_t start_col, + uint32_t end_line, uint32_t end_col); + +uint32_t text_num_lines(struct text *text); +uint32_t text_line_length(struct text *text, uint32_t lineidx); +uint32_t text_line_size(struct text *text, uint32_t lineidx); +uint32_t text_col_to_byteindex(struct text *text, uint32_t line, uint32_t col); +uint32_t text_byteindex_to_col(struct text *text, uint32_t line, + uint32_t byteindex); + +struct text_chunk { + uint8_t *text; + uint32_t nbytes; + uint32_t nchars; + uint32_t line; + bool allocated; +}; + +typedef void (*chunk_cb)(struct text_chunk *chunk, void *userdata); +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + chunk_cb callback, void *userdata); + +void text_for_each_chunk(struct text *text, chunk_cb callback, void *userdata); + +struct text_chunk text_get_line(struct text *text, uint32_t line); +struct text_chunk text_get_region(struct text *text, uint32_t start_line, + uint32_t start_col, uint32_t end_line, + uint32_t end_col); + +bool text_line_contains_unicode(struct text *text, uint32_t line); diff --git a/src/dged/undo.c b/src/dged/undo.c new file mode 100644 index 0000000..8f00f0f --- /dev/null +++ b/src/dged/undo.c @@ -0,0 +1,181 @@ +#include "undo.h" +#include "string.h" +#include "vec.h" + +#include <stdio.h> +#include <stdlib.h> + +void undo_init(struct undo_stack *undo, uint32_t initial_capacity) { + undo->top = INVALID_TOP; + undo->undo_in_progress = false; + VEC_INIT(&undo->records, initial_capacity); +} + +void undo_clear(struct undo_stack *undo) { + undo->top = INVALID_TOP; + VEC_CLEAR(&undo->records); +} + +void undo_destroy(struct undo_stack *undo) { + VEC_FOR_EACH(&undo->records, struct undo_record * rec) { + if (rec->type == Undo_Delete && rec->delete.data != NULL && + rec->delete.nbytes > 0) { + free(rec->delete.data); + } + } + + undo_clear(undo); + + VEC_DESTROY(&undo->records); +} + +uint32_t undo_push_boundary(struct undo_stack *undo, + struct undo_boundary boundary) { + + // we can only have one save point + if (boundary.save_point) { + VEC_FOR_EACH(&undo->records, struct undo_record * rec) { + if (rec->type == Undo_Boundary && rec->boundary.save_point) { + rec->boundary.save_point = false; + } + } + } + + VEC_APPEND(&undo->records, struct undo_record * rec); + rec->type = Undo_Boundary; + rec->boundary = boundary; + + if (!undo->undo_in_progress) { + undo->top = VEC_SIZE(&undo->records) - 1; + } + + return VEC_SIZE(&undo->records) - 1; +} + +bool pos_equal(struct position *a, struct position *b) { + return a->row == b->row && a->col == b->col; +} + +uint32_t undo_push_add(struct undo_stack *undo, struct undo_add add) { + + // "compress" + if (!VEC_EMPTY(&undo->records) && + VEC_BACK(&undo->records)->type == Undo_Add && + pos_equal(&VEC_BACK(&undo->records)->add.end, &add.begin)) { + VEC_BACK(&undo->records)->add.end = add.end; + } else { + VEC_APPEND(&undo->records, struct undo_record * rec); + rec->type = Undo_Add; + rec->add = add; + } + + if (!undo->undo_in_progress) { + undo->top = VEC_SIZE(&undo->records) - 1; + } + + return VEC_SIZE(&undo->records) - 1; +} + +uint32_t undo_push_delete(struct undo_stack *undo, struct undo_delete delete) { + VEC_APPEND(&undo->records, struct undo_record * rec); + rec->type = Undo_Delete; + rec->delete = delete; + + if (!undo->undo_in_progress) { + undo->top = VEC_SIZE(&undo->records) - 1; + } + + return VEC_SIZE(&undo->records) - 1; +} + +void undo_begin(struct undo_stack *undo) { undo->undo_in_progress = true; } + +void undo_next(struct undo_stack *undo, struct undo_record **records_out, + uint32_t *nrecords_out) { + *nrecords_out = 0; + *records_out = NULL; + + if (VEC_EMPTY(&undo->records)) { + return; + } + + if (undo->top == INVALID_TOP) { + // reset back to the top (redo) + undo->top = VEC_SIZE(&undo->records) - 1; + } + + uint32_t nrecords = 1; + struct undo_record *current = &VEC_ENTRIES(&undo->records)[undo->top]; + + // skip any leading boundaries + while (undo->top > 0 && current->type == Undo_Boundary) { + ++nrecords; + --undo->top; + current = &VEC_ENTRIES(&undo->records)[undo->top]; + } + + // find the next boundary + while (undo->top > 0 && current->type != Undo_Boundary) { + ++nrecords; + --undo->top; + current = &VEC_ENTRIES(&undo->records)[undo->top]; + } + + if (nrecords > 0) { + *records_out = calloc(nrecords, sizeof(struct undo_record)); + *nrecords_out = nrecords; + + struct undo_record *dest = *records_out; + + // copy backwards + for (uint32_t reci = undo->top + nrecords, outi = 0; reci > undo->top; + --reci, ++outi) { + dest[outi] = VEC_ENTRIES(&undo->records)[reci - 1]; + } + } + + if (undo->top > 0) { + --undo->top; + } else { + undo->top = INVALID_TOP; + } +} + +void undo_end(struct undo_stack *undo) { undo->undo_in_progress = false; } + +uint32_t undo_size(struct undo_stack *undo) { return VEC_SIZE(&undo->records); } +uint32_t undo_current_position(struct undo_stack *undo) { return undo->top; } + +size_t rec_to_str(struct undo_record *rec, char *buffer, size_t n) { + switch (rec->type) { + case Undo_Add: + return snprintf(buffer, n, "add { begin: (%d, %d) end: (%d, %d)}", + rec->add.begin.row, rec->add.begin.col, rec->add.end.row, + rec->add.end.col); + case Undo_Delete: + return snprintf(buffer, n, "delete { pos: (%d, %d), ptr: 0x%p, nbytes: %d}", + rec->delete.pos.row, rec->delete.pos.col, rec->delete.data, + rec->delete.nbytes); + default: + return snprintf(buffer, n, "boundary { save_point: %s }", + rec->boundary.save_point ? "yes" : "no"); + } +} + +const char *undo_dump(struct undo_stack *undo) { + uint32_t left = 8192; + const char *buf = malloc(left); + char *pos = (char *)buf; + pos[0] = '\0'; + + char rec_buf[256]; + VEC_FOR_EACH_INDEXED(&undo->records, struct undo_record * rec, reci) { + rec_to_str(rec, rec_buf, 256); + uint32_t written = snprintf(pos, left, "%d: [%s]%s\n", reci, rec_buf, + reci == undo->top ? " <- top" : ""); + left = written > left ? 0 : left - written; + pos += written; + } + + return buf; +} diff --git a/src/dged/undo.h b/src/dged/undo.h new file mode 100644 index 0000000..1ce3a8a --- /dev/null +++ b/src/dged/undo.h @@ -0,0 +1,66 @@ +#include "vec.h" +#include <stdbool.h> +#include <stdint.h> + +enum undo_record_type { + Undo_Boundary = 1, + Undo_Add = 2, + Undo_Delete = 3, +}; + +struct position { + uint32_t row; + uint32_t col; +}; + +struct undo_boundary { + bool save_point; +}; + +struct undo_add { + struct position begin; + struct position end; +}; + +struct undo_delete { + struct position pos; + uint8_t *data; + uint32_t nbytes; +}; + +struct undo_record { + enum undo_record_type type; + + union { + struct undo_boundary boundary; + struct undo_add add; + struct undo_delete delete; + }; +}; + +#define INVALID_TOP -1 + +struct undo_stack { + VEC(struct undo_record) records; + uint32_t top; + bool undo_in_progress; +}; + +void undo_init(struct undo_stack *undo, uint32_t initial_capacity); +void undo_clear(struct undo_stack *undo); +void undo_destroy(struct undo_stack *undo); + +uint32_t undo_push_boundary(struct undo_stack *undo, + struct undo_boundary boundary); + +uint32_t undo_push_add(struct undo_stack *undo, struct undo_add add); +uint32_t undo_push_delete(struct undo_stack *undo, struct undo_delete delete); + +void undo_begin(struct undo_stack *undo); +void undo_next(struct undo_stack *undo, struct undo_record **records_out, + uint32_t *nrecords_out); +void undo_end(struct undo_stack *undo); + +uint32_t undo_size(struct undo_stack *undo); +uint32_t undo_current_position(struct undo_stack *undo); +const char *undo_dump(struct undo_stack *undo); diff --git a/src/dged/utf8.c b/src/dged/utf8.c new file mode 100644 index 0000000..abf5ef7 --- /dev/null +++ b/src/dged/utf8.c @@ -0,0 +1,67 @@ +#include "utf8.h" + +#include <stdio.h> + +bool utf8_byte_is_unicode_start(uint8_t byte) { return (byte & 0xc0) == 0xc0; } +bool utf8_byte_is_unicode_continuation(uint8_t byte) { + return utf8_byte_is_unicode(byte) && !utf8_byte_is_unicode_start(byte); +} +bool utf8_byte_is_unicode(uint8_t byte) { return (byte & 0x80) != 0x0; } +bool utf8_byte_is_ascii(uint8_t byte) { return !utf8_byte_is_unicode(byte); } + +uint32_t utf8_nbytes_in_char(uint8_t byte) { + // length of char is the number of leading ones + // flip it and count number of leading zeros + uint8_t invb = ~byte; + return __builtin_clz((uint32_t)invb) - 24; +} + +// TODO: grapheme clusters, this returns the number of unicode code points +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes) { + uint32_t nchars = 0; + uint32_t expected = 0; + for (uint32_t bi = 0; bi < nbytes; ++bi) { + uint8_t byte = bytes[bi]; + if (utf8_byte_is_unicode(byte)) { + if (utf8_byte_is_unicode_start(byte)) { + expected = utf8_nbytes_in_char(byte) - 1; + } else { // continuation byte + --expected; + if (expected == 0) { + ++nchars; + } + } + } else { // ascii + ++nchars; + } + } + return nchars; +} + +// TODO: grapheme clusters, this uses the number of unicode code points +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nbytes, uint32_t nchars) { + + uint32_t bi = 0; + uint32_t chars = 0; + uint32_t expected = 0; + + while (chars < nchars && bi < nbytes) { + uint8_t byte = bytes[bi]; + if (utf8_byte_is_unicode(byte)) { + if (utf8_byte_is_unicode_start(byte)) { + expected = utf8_nbytes_in_char(byte) - 1; + } else { // continuation char + --expected; + if (expected == 0) { + ++chars; + } + } + } else { // ascii + ++chars; + } + + ++bi; + } + + return bi; +} diff --git a/src/dged/utf8.h b/src/dged/utf8.h new file mode 100644 index 0000000..59a959e --- /dev/null +++ b/src/dged/utf8.h @@ -0,0 +1,17 @@ +#include <stdbool.h> +#include <stdint.h> + +/*! + * \brief Return the number of chars the utf-8 sequence pointed at by `bytes` of + * length `nbytes`, represents + */ +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes); + +/* Return the number of bytes used to make up the next `nchars` characters */ +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nbytes, uint32_t nchars); + +/* true if `byte` is a unicode byte sequence start byte */ +bool utf8_byte_is_unicode_start(uint8_t byte); +bool utf8_byte_is_unicode_continuation(uint8_t byte); +bool utf8_byte_is_ascii(uint8_t byte); +bool utf8_byte_is_unicode(uint8_t byte); diff --git a/src/dged/vec.h b/src/dged/vec.h new file mode 100644 index 0000000..073f978 --- /dev/null +++ b/src/dged/vec.h @@ -0,0 +1,63 @@ +#ifndef _VEC_H +#define _VEC_H + +#include <stdlib.h> + +#define VEC(entry) \ + struct { \ + entry *entries; \ + uint32_t nentries; \ + uint32_t capacity; \ + } + +#define VEC_INIT(vec, initial_capacity) \ + (vec)->entries = malloc(sizeof((vec)->entries[0]) * initial_capacity); \ + (vec)->capacity = initial_capacity; \ + (vec)->nentries = 0; + +#define VEC_DESTROY(vec) \ + free((vec)->entries); \ + (vec)->entries = NULL; \ + (vec)->capacity = 0; \ + (vec)->nentries = 0; + +#define VEC_GROW(vec, new_size) \ + if (new_size > (vec)->capacity) { \ + (vec)->capacity = new_size; \ + (vec)->entries = realloc((vec)->entries, \ + (sizeof((vec)->entries[0]) * (vec)->capacity)); \ + } + +#define VEC_PUSH(vec, entry) \ + if ((vec)->nentries + 1 >= (vec)->capacity) { \ + VEC_GROW(vec, (vec)->capacity * 2); \ + } \ + \ + (vec)->entries[(vec)->nentries] = entry; \ + ++(vec)->nentries; + +#define VEC_APPEND(vec, var) \ + if ((vec)->nentries + 1 >= (vec)->capacity) { \ + VEC_GROW(vec, (vec)->capacity * 2); \ + } \ + \ + var = &((vec)->entries[(vec)->nentries]); \ + ++(vec)->nentries; + +#define VEC_FOR_EACH(vec, var) VEC_FOR_EACH_INDEXED(vec, var, i) + +#define VEC_FOR_EACH_INDEXED(vec, var, idx) \ + for (uint32_t keep = 1, idx = 0, size = (vec)->nentries; \ + keep && idx != size; keep = !keep, idx++) \ + for (var = (vec)->entries + idx; keep; keep = !keep) + +#define VEC_SIZE(vec) (vec)->nentries +#define VEC_CAPACITY(vec) (vec)->capacity +#define VEC_ENTRIES(vec) (vec)->entries +#define VEC_EMPTY(vec) ((vec)->nentries == 0) + +#define VEC_CLEAR(vec) (vec)->nentries = 0 +#define VEC_BACK(vec) \ + ((vec)->nentries > 0 ? &((vec)->entries[(vec)->nentries - 1]) : NULL) + +#endif diff --git a/src/dged/window.c b/src/dged/window.c new file mode 100644 index 0000000..f24997c --- /dev/null +++ b/src/dged/window.c @@ -0,0 +1,445 @@ +#include "binding.h" +#include "btree.h" +#include "buffer.h" +#include "command.h" +#include "display.h" +#include "minibuffer.h" + +enum window_type { + Window_Buffer, + Window_HSplit, + Window_VSplit, +}; + +struct window { + uint32_t x; + uint32_t y; + uint32_t width; + uint32_t height; + enum window_type type; + struct buffer_view buffer_view; + struct buffer *prev_buffer; + struct command_list *commands; + uint32_t relline; + uint32_t relcol; +}; + +BINTREE_ENTRY_TYPE(window_node, struct window); + +static struct windows { + BINTREE(window_node) windows; + struct window_node *active; + struct keymap keymap; +} g_windows; + +static struct window g_minibuffer_window; + +void windows_init(uint32_t height, uint32_t width, + struct buffer *initial_buffer, struct buffer *minibuffer) { + BINTREE_INIT(&g_windows.windows); + + g_minibuffer_window = (struct window){ + .buffer_view = buffer_view_create(minibuffer, false, false), + .prev_buffer = NULL, + .x = 0, + .y = height - 1, + .height = 1, + .width = width, + }; + + struct window root_window = (struct window){ + .buffer_view = buffer_view_create(initial_buffer, true, true), + .prev_buffer = NULL, + .height = height - 1, + .width = width, + .x = 0, + .y = 0, + }; + BINTREE_SET_ROOT(&g_windows.windows, root_window); + g_windows.active = BINTREE_ROOT(&g_windows.windows); +} + +static void window_tree_clear_sub(struct window_node *root_node) { + struct window_node *n = root_node; + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + buffer_view_destroy(&w->buffer_view); + } + BINTREE_NEXT(n); + } + BINTREE_FREE_NODES(root_node, window_node); +} + +static void window_tree_clear() { + window_tree_clear_sub(BINTREE_ROOT(&g_windows.windows)); +} + +void windows_destroy() { window_tree_clear(); } + +struct window *root_window() { + return &BINTREE_VALUE(BINTREE_ROOT(&g_windows.windows)); +} + +struct window *minibuffer_window() { + return &g_minibuffer_window; +} + +static void window_tree_resize(struct window_node *root, uint32_t height, + uint32_t width) { + + /* due to the way tree traversal works, we need to disconnect the subtree from + * its potential parent. Otherwise the algorithm will traverse above the root + * of the subtree. */ + struct window_node *orig_parent = BINTREE_PARENT(root); + BINTREE_PARENT(root) = NULL; + + struct window *root_window = &BINTREE_VALUE(root); + uint32_t width_ratio_percent = (width * 100) / (root_window->width); + uint32_t height_ratio_percent = (height * 100) / (root_window->height); + root_window->width = width; + root_window->height = height; + + struct window_node *n = root; + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (BINTREE_PARENT(n) != NULL && n != root) { + if (BINTREE_LEFT(BINTREE_PARENT(n)) == n) { + // if left child, use scale from root + w->width = (width_ratio_percent * w->width) / 100; + w->height = (height_ratio_percent * w->height) / 100; + } else { + // if right child, fill rest of space after left and parent resize + struct window *left_sibling = + &BINTREE_VALUE(BINTREE_LEFT(BINTREE_PARENT(n))); + struct window *parent = &BINTREE_VALUE(BINTREE_PARENT(n)); + + w->width = parent->width; + w->height = parent->height; + if (parent->type == Window_HSplit) { + w->y = parent->y + left_sibling->height; + w->height -= left_sibling->height; + } else { + w->x = parent->x + left_sibling->width; + w->width -= left_sibling->width; + } + } + } + BINTREE_NEXT(n); + } + + BINTREE_PARENT(root) = orig_parent; +} + +void windows_resize(uint32_t height, uint32_t width) { + g_minibuffer_window.width = width; + g_minibuffer_window.y = height - 1; + + window_tree_resize(BINTREE_ROOT(&g_windows.windows), height - 1, width); +} + +void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time) { + struct window_node *n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, + w->y, w->buffer_view.buffer->name); + + buffer_update(&w->buffer_view, w->width, w->height, w->commands, + frame_time, &w->relline, &w->relcol); + } + + BINTREE_NEXT(n); + } + + struct window *w = &g_minibuffer_window; + w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, + w->y, w->buffer_view.buffer->name); + buffer_update(&w->buffer_view, w->width, w->height, w->commands, frame_time, + &w->relline, &w->relcol); +} + +void windows_render(struct display *display) { + struct window_node *n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + display_render(display, w->commands); + } + BINTREE_NEXT(n); + } + + display_render(display, g_minibuffer_window.commands); +} + +struct window_node *find_window(struct window *window) { + struct window_node *n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w == window) { + return n; + } + BINTREE_NEXT(n); + } + + return NULL; +} + +void windows_set_active(struct window *window) { + struct window_node *n = find_window(window); + if (n != NULL) { + g_windows.active = n; + } +} + +struct window *windows_get_active() { + return &BINTREE_VALUE(g_windows.active); +} + +void window_set_buffer(struct window *window, struct buffer *buffer) { + window->prev_buffer = window->buffer_view.buffer; + buffer_view_destroy(&window->buffer_view); + window->buffer_view = buffer_view_create(buffer, true, true); +} + +struct buffer *window_buffer(struct window *window) { + return window->buffer_view.buffer; +} + +struct buffer_view *window_buffer_view(struct window *window) { + return &window->buffer_view; +} + +struct buffer *window_prev_buffer(struct window *window) { + return window->prev_buffer; +} + +bool window_has_prev_buffer(struct window *window) { + return window->prev_buffer != NULL; +} + +struct buffer_location window_cursor_location(struct window *window) { + return (struct buffer_location){ + .col = window->relcol, + .line = window->relline, + }; +} +struct buffer_location window_absolute_cursor_location(struct window *window) { + return (struct buffer_location){ + .col = window->x + window->relcol, + .line = window->y + window->relline, + }; +} + +void window_close(struct window *window) { + // do not want to delete last window + if (window == root_window()) { + return; + } + + struct window_node *to_delete = find_window(window); + if (to_delete == NULL) { + return; + } + + // promote other child to parent + struct window_node *target = BINTREE_PARENT(to_delete); + struct window_node *source = BINTREE_RIGHT(target) == to_delete + ? BINTREE_LEFT(target) + : BINTREE_RIGHT(target); + + buffer_view_destroy(&window->buffer_view); + BINTREE_REMOVE(to_delete); + BINTREE_FREE_NODE(to_delete); + + BINTREE_VALUE(source).x = BINTREE_VALUE(target).x; + BINTREE_VALUE(source).y = BINTREE_VALUE(target).y; + uint32_t target_width = BINTREE_VALUE(target).width; + uint32_t target_height = BINTREE_VALUE(target).height; + + // copy the node value and set it's children as children of the target node + BINTREE_VALUE(target) = BINTREE_VALUE(source); + BINTREE_LEFT(target) = BINTREE_LEFT(source); + BINTREE_RIGHT(target) = BINTREE_RIGHT(source); + + // adopt the children + if (BINTREE_HAS_LEFT(source)) { + BINTREE_PARENT(BINTREE_LEFT(source)) = target; + } + + if (BINTREE_HAS_RIGHT(source)) { + BINTREE_PARENT(BINTREE_RIGHT(source)) = target; + } + + BINTREE_FREE_NODE(source); + + window_tree_resize(target, target_height, target_width); + BINTREE_FIRST(target); + windows_set_active(&BINTREE_VALUE(target)); +} + +void window_close_others(struct window *window) { + struct window_node *root = BINTREE_ROOT(&g_windows.windows); + + // copy window and make it suitable as a root window + struct window new_root = *window; + new_root.x = 0; + new_root.y = 0; + new_root.buffer_view = buffer_view_clone(&window->buffer_view); + new_root.width = BINTREE_VALUE(root).width; + new_root.height = BINTREE_VALUE(root).height; + + window_tree_clear(); + + // create new root window + BINTREE_SET_ROOT(&g_windows.windows, new_root); + windows_set_active(&BINTREE_VALUE(BINTREE_ROOT(&g_windows.windows))); +} + +void window_hsplit(struct window *window, struct window **new_window_a, + struct window **new_window_b) { + struct window_node *n = find_window(window); + if (n != NULL) { + struct window w = BINTREE_VALUE(n); + + if (w.type == Window_Buffer) { + struct window parent = {0}; + parent.type = Window_HSplit; + parent.x = w.x; + parent.y = w.y; + parent.width = w.width; + parent.height = w.height; + BINTREE_VALUE(n) = parent; + + /* Reuse the current window as the 'left' child, halving the height */ + w.height /= 2; + BINTREE_INSERT(n, w); + *new_window_a = &BINTREE_VALUE(BINTREE_LEFT(n)); + windows_set_active(*new_window_a); + + /* Create a new window for the split, showing the same buffer as the + * original window. + */ + struct window new_window = {0}; + new_window.type = Window_Buffer; + new_window.buffer_view = + buffer_view_create(w.buffer_view.buffer, true, true); + buffer_goto(&new_window.buffer_view, w.buffer_view.dot.line, + w.buffer_view.dot.col); + new_window.prev_buffer = w.prev_buffer; + new_window.x = w.x; + new_window.y = w.y + w.height; + new_window.width = w.width; + new_window.height = parent.height - w.height; + BINTREE_INSERT(n, new_window); + *new_window_b = &BINTREE_VALUE(BINTREE_RIGHT(n)); + } + } +} + +void window_vsplit(struct window *window, struct window **new_window_a, + struct window **new_window_b) { + struct window_node *n = find_window(window); + if (n != NULL) { + struct window w = BINTREE_VALUE(n); + + if (w.type == Window_Buffer) { + /* Create a new split container to use as parent */ + struct window parent = {0}; + parent.type = Window_VSplit; + parent.x = w.x; + parent.y = w.y; + parent.width = w.width; + parent.height = w.height; + BINTREE_VALUE(n) = parent; + + /* Reuse the current window as the 'left' child, halving the width */ + w.width /= 2; + BINTREE_INSERT(n, w); + *new_window_a = &BINTREE_VALUE(BINTREE_LEFT(n)); + windows_set_active(*new_window_a); + + /* Create a new window for the split, showing the same buffer as the + * original window. + */ + struct window new_window = {0}; + new_window.type = Window_Buffer; + new_window.buffer_view = + buffer_view_create(w.buffer_view.buffer, true, true); + buffer_goto(&new_window.buffer_view, w.buffer_view.dot.line, + w.buffer_view.dot.col); + new_window.prev_buffer = w.prev_buffer; + new_window.x = w.x + w.width; + new_window.y = w.y; + new_window.width = parent.width - w.width; + new_window.height = w.height; + BINTREE_INSERT(n, new_window); + *new_window_b = &BINTREE_VALUE(BINTREE_RIGHT(n)); + } + } +} + +void window_split(struct window *window, struct window **new_window_a, + struct window **new_window_b) { + /* The height * 2 is a horrible hack, we would need to know how big the font + actually is */ + window->height * 2 > window->width + ? window_hsplit(window, new_window_a, new_window_b) + : window_vsplit(window, new_window_a, new_window_b); +} + +struct window *windows_focus_next() { + struct window *active = windows_get_active(); + struct window_node *n = find_window(active); + BINTREE_NEXT(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + windows_set_active(w); + return w; + } + BINTREE_NEXT(n); + } + + // we have moved around + n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + if (n != NULL) { + windows_set_active(&BINTREE_VALUE(n)); + return &BINTREE_VALUE(n); + } + + // fall back to root + windows_set_active(root_window()); + return root_window(); +} + +struct window *windows_focus(uint32_t id) { + uint32_t curr_id = 0; + + struct window_node *n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + if (curr_id == id) { + windows_set_active(w); + return w; + } + ++curr_id; + } + BINTREE_NEXT(n); + } + + return NULL; +} + +uint32_t window_width(struct window *window) { return window->width; } + +uint32_t window_height(struct window *window) { return window->height; } diff --git a/src/dged/window.h b/src/dged/window.h new file mode 100644 index 0000000..b3284e9 --- /dev/null +++ b/src/dged/window.h @@ -0,0 +1,48 @@ +#include <stdbool.h> +#include <stdint.h> + +#include "btree.h" + +struct command_list; +struct display; +struct keymap; +struct commands; +struct buffer; + +struct window; +struct windows; + +void windows_init(uint32_t height, uint32_t width, + struct buffer *initial_buffer, struct buffer *minibuffer); + +void windows_destroy(); +void windows_resize(uint32_t height, uint32_t width); +void windows_update(void *(*frame_alloc)(size_t), uint64_t frame_time); +void windows_render(struct display *display); + +struct window *root_window(); +struct window *minibuffer_window(); + +void windows_set_active(struct window *window); +struct window *windows_focus(uint32_t id); +struct window *windows_get_active(); +struct window *windows_focus_next(); + +void window_set_buffer(struct window *window, struct buffer *buffer); +struct buffer *window_buffer(struct window *window); +struct buffer_view *window_buffer_view(struct window *window); +struct buffer *window_prev_buffer(struct window *window); +bool window_has_prev_buffer(struct window *window); +struct buffer_location window_cursor_location(struct window *window); +struct buffer_location window_absolute_cursor_location(struct window *window); +uint32_t window_width(struct window *window); +uint32_t window_height(struct window *window); + +void window_close(struct window *window); +void window_close_others(struct window *window); +void window_split(struct window *window, struct window **new_window_a, + struct window **new_window_b); +void window_hsplit(struct window *window, struct window **new_window_a, + struct window **new_window_b); +void window_vsplit(struct window *window, struct window **new_window_a, + struct window **new_window_b); |
