From a123725a12e948d78badb2cb686d38548f1c633b Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Thu, 6 Apr 2023 23:23:46 +0200 Subject: Implement window handling Also implement searching. fix undo boundaries when it checked for other save point, it used && instead of == which caused it to overwrite other types. Fix bytes vs chars bug in text_get_region --- Makefile | 26 +- src/allocator.c | 24 - src/allocator.h | 46 -- src/binding.c | 75 ---- src/binding.h | 136 ------ src/buffer.c | 1125 ---------------------------------------------- src/buffer.h | 201 --------- src/buffers.c | 43 -- src/buffers.h | 17 - src/command.c | 175 -------- src/command.h | 205 --------- src/dged/allocator.c | 24 + src/dged/allocator.h | 46 ++ src/dged/binding.c | 75 ++++ src/dged/binding.h | 136 ++++++ src/dged/btree.h | 113 +++++ src/dged/buffer.c | 1110 +++++++++++++++++++++++++++++++++++++++++++++ src/dged/buffer.h | 219 +++++++++ src/dged/buffers.c | 43 ++ src/dged/buffers.h | 17 + src/dged/command.c | 79 ++++ src/dged/command.h | 179 ++++++++ src/dged/display.c | 386 ++++++++++++++++ src/dged/display.h | 212 +++++++++ src/dged/hash.h | 11 + src/dged/hashmap.h | 86 ++++ src/dged/keyboard.c | 159 +++++++ src/dged/keyboard.h | 145 ++++++ src/dged/lang.c | 160 +++++++ src/dged/lang.h | 46 ++ src/dged/minibuffer.c | 223 +++++++++ src/dged/minibuffer.h | 108 +++++ src/dged/reactor-epoll.c | 76 ++++ src/dged/reactor.h | 17 + src/dged/settings.c | 95 ++++ src/dged/settings.h | 137 ++++++ src/dged/text.c | 496 ++++++++++++++++++++ src/dged/text.h | 54 +++ src/dged/undo.c | 181 ++++++++ src/dged/undo.h | 66 +++ src/dged/utf8.c | 67 +++ src/dged/utf8.h | 17 + src/dged/vec.h | 63 +++ src/dged/window.c | 445 ++++++++++++++++++ src/dged/window.h | 48 ++ src/display.c | 386 ---------------- src/display.h | 212 --------- src/hash.h | 11 - src/hashmap.h | 86 ---- src/keyboard.c | 159 ------- src/keyboard.h | 143 ------ src/lang.c | 160 ------- src/lang.h | 46 -- src/main.c | 364 --------------- src/main/bindings.c | 207 +++++++++ src/main/bindings.h | 15 + src/main/cmds.c | 519 +++++++++++++++++++++ src/main/cmds.h | 10 + src/main/main.c | 294 ++++++++++++ src/minibuffer.c | 184 -------- src/minibuffer.h | 89 ---- src/reactor-epoll.c | 76 ---- src/reactor.h | 17 - src/settings.c | 165 ------- src/settings.h | 119 ----- src/text.c | 492 -------------------- src/text.h | 54 --- src/undo.c | 181 -------- src/undo.h | 66 --- src/utf8.c | 67 --- src/utf8.h | 17 - src/vec.h | 61 --- src/window.c | 14 - src/window.h | 18 - test/allocator.c | 4 +- test/buffer.c | 43 +- test/command.c | 6 +- test/container.c | 102 +++++ test/fake-reactor.h | 3 +- test/keyboard.c | 11 +- test/main.c | 5 +- test/minibuffer.c | 22 +- test/settings.c | 15 +- test/test.h | 1 + test/text.c | 12 +- test/undo.c | 7 +- test/utf8.c | 9 +- 87 files changed, 6572 insertions(+), 5312 deletions(-) delete mode 100644 src/allocator.c delete mode 100644 src/allocator.h delete mode 100644 src/binding.c delete mode 100644 src/binding.h delete mode 100644 src/buffer.c delete mode 100644 src/buffer.h delete mode 100644 src/buffers.c delete mode 100644 src/buffers.h delete mode 100644 src/command.c delete mode 100644 src/command.h create mode 100644 src/dged/allocator.c create mode 100644 src/dged/allocator.h create mode 100644 src/dged/binding.c create mode 100644 src/dged/binding.h create mode 100644 src/dged/btree.h create mode 100644 src/dged/buffer.c create mode 100644 src/dged/buffer.h create mode 100644 src/dged/buffers.c create mode 100644 src/dged/buffers.h create mode 100644 src/dged/command.c create mode 100644 src/dged/command.h create mode 100644 src/dged/display.c create mode 100644 src/dged/display.h create mode 100644 src/dged/hash.h create mode 100644 src/dged/hashmap.h create mode 100644 src/dged/keyboard.c create mode 100644 src/dged/keyboard.h create mode 100644 src/dged/lang.c create mode 100644 src/dged/lang.h create mode 100644 src/dged/minibuffer.c create mode 100644 src/dged/minibuffer.h create mode 100644 src/dged/reactor-epoll.c create mode 100644 src/dged/reactor.h create mode 100644 src/dged/settings.c create mode 100644 src/dged/settings.h create mode 100644 src/dged/text.c create mode 100644 src/dged/text.h create mode 100644 src/dged/undo.c create mode 100644 src/dged/undo.h create mode 100644 src/dged/utf8.c create mode 100644 src/dged/utf8.h create mode 100644 src/dged/vec.h create mode 100644 src/dged/window.c create mode 100644 src/dged/window.h delete mode 100644 src/display.c delete mode 100644 src/display.h delete mode 100644 src/hash.h delete mode 100644 src/hashmap.h delete mode 100644 src/keyboard.c delete mode 100644 src/keyboard.h delete mode 100644 src/lang.c delete mode 100644 src/lang.h delete mode 100644 src/main.c create mode 100644 src/main/bindings.c create mode 100644 src/main/bindings.h create mode 100644 src/main/cmds.c create mode 100644 src/main/cmds.h create mode 100644 src/main/main.c delete mode 100644 src/minibuffer.c delete mode 100644 src/minibuffer.h delete mode 100644 src/reactor-epoll.c delete mode 100644 src/reactor.h delete mode 100644 src/settings.c delete mode 100644 src/settings.h delete mode 100644 src/text.c delete mode 100644 src/text.h delete mode 100644 src/undo.c delete mode 100644 src/undo.h delete mode 100644 src/utf8.c delete mode 100644 src/utf8.h delete mode 100644 src/vec.h delete mode 100644 src/window.c delete mode 100644 src/window.h create mode 100644 test/container.c diff --git a/Makefile b/Makefile index 6ba7cca..7c6a4af 100644 --- a/Makefile +++ b/Makefile @@ -7,15 +7,16 @@ default: dged build: mkdir -p build -SOURCES = src/binding.c src/buffer.c src/command.c src/display.c \ - src/keyboard.c src/minibuffer.c src/text.c \ - src/utf8.c src/buffers.c src/window.c src/allocator.c src/undo.c \ - src/settings.c src/lang.c +SOURCES = src/dged/binding.c src/dged/buffer.c src/dged/command.c src/dged/display.c \ + src/dged/keyboard.c src/dged/minibuffer.c src/dged/text.c \ + src/dged/utf8.c src/dged/buffers.c src/dged/window.c src/dged/allocator.c src/dged/undo.c \ + src/dged/settings.c src/dged/lang.c + +MAIN_SOURCES = src/main/main.c src/main/cmds.c src/main/bindings.c -DGED_SOURCES = $(SOURCES) src/main.c TEST_SOURCES = test/assert.c test/buffer.c test/text.c test/utf8.c test/main.c \ test/command.c test/keyboard.c test/fake-reactor.c test/allocator.c \ - test/minibuffer.c test/undo.c test/settings.c + test/minibuffer.c test/undo.c test/settings.c test/container.c prefix ?= "/usr" @@ -24,14 +25,15 @@ prefix ?= "/usr" UNAME_S != uname -s | tr '[:upper:]' '[:lower:]' -CFLAGS = -Werror -g -std=c99 -I $(.CURDIR)/src +CFLAGS = -Werror -g -std=c99 -I $(.CURDIR)/src -I $(.CURDIR)/src/main DEPS = $(DGED_SOURCES:.c=.d) $(TEST_SOURCES:.c=.d) OBJS = $(SOURCES:.c=.o) +MAIN_OBJS = $(MAIN_SOURCES:.c=.o) TEST_OBJS = $(TEST_SOURCES:.c=.o) -FILES = $(DEPS) $(DGED_SOURCES:.c=.o) dged libdged.a $(TEST_OBJS) +FILES = $(DEPS) $(MAIN_OBJS) $(OBJS) dged libdged.a $(TEST_OBJS) .sinclude "$(UNAME_S).mk" @@ -45,8 +47,8 @@ FILES = $(DEPS) $(DGED_SOURCES:.c=.o) dged libdged.a $(TEST_OBJS) @mkdir -p $(@D) $(CC) $(CFLAGS) -c $< -o $@ -dged: src/main.o libdged.a - $(CC) $(LDFLAGS) src/main.o libdged.a -o dged +dged: $(MAIN_OBJS) libdged.a + $(CC) $(LDFLAGS) $(MAIN_OBJS) libdged.a -o dged libdged.a: $(OBJS) $(PLATFORM_OBJS) $(AR) -rc libdged.a $(OBJS) $(PLATFORM_OBJS) @@ -55,7 +57,7 @@ run-tests: $(TEST_OBJS) $(OBJS) $(CC) $(LDFLAGS) $(TEST_OBJS) $(OBJS) -o run-tests check: run-tests - clang-format --dry-run --Werror $(DGED_SOURCES:%.c=../%.c) $(TEST_SOURCES:%c=../%c) + clang-format --dry-run --Werror $(SOURCES:%.c=../%.c) $(MAIN_SOURCES:%.c=../%.c) $(TEST_SOURCES:%c=../%c) ./run-tests run: dged @@ -68,7 +70,7 @@ debug-tests: run-tests gdb ./run-tests format: - clang-format -i $(DGED_SOURCES) $(TEST_SOURCES) + clang-format -i $(SOURCES:%.c=../%.c) $(MAIN_SOURCES:%.c=../%.c) $(TEST_SOURCES:%c=../%c) clean: rm -f $(FILES) diff --git a/src/allocator.c b/src/allocator.c deleted file mode 100644 index 308b97c..0000000 --- a/src/allocator.c +++ /dev/null @@ -1,24 +0,0 @@ -#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/allocator.h b/src/allocator.h deleted file mode 100644 index 49e3aec..0000000 --- a/src/allocator.h +++ /dev/null @@ -1,46 +0,0 @@ -#include -#include -#include - -/** - * 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/binding.c b/src/binding.c deleted file mode 100644 index a0946ee..0000000 --- a/src/binding.c +++ /dev/null @@ -1,75 +0,0 @@ -#include "binding.h" -#include "command.h" - -#include -#include - -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 = 0; bi < keymap->nbindings; ++bi) { - struct binding *binding = &keymap->bindings[bi]; - 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/binding.h b/src/binding.h deleted file mode 100644 index f2a531d..0000000 --- a/src/binding.h +++ /dev/null @@ -1,136 +0,0 @@ -#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/buffer.c b/src/buffer.c deleted file mode 100644 index 23a8ab1..0000000 --- a/src/buffer.c +++ /dev/null @@ -1,1125 +0,0 @@ -#include "buffer.h" -#include "binding.h" -#include "display.h" -#include "errno.h" -#include "lang.h" -#include "minibuffer.h" -#include "reactor.h" -#include "settings.h" -#include "utf8.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct modeline { - uint8_t *buffer; -}; - -#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}; - -struct update_hook_result buffer_linenum_hook(struct buffer *buffer, - 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 *buffer, - struct command_list *commands, - uint32_t width, uint32_t height, - uint64_t frame_time, - void *userdata); - -struct buffer buffer_create(char *name, bool modeline) { - struct buffer b = (struct buffer){ - .filename = NULL, - .name = strdup(name), - .text = text_create(10), - .dot = {0}, - .mark = {0}, - .mark_set = false, - .modified = false, - .readonly = false, - .modeline = NULL, - .keymaps = calloc(10, sizeof(struct keymap)), - .nkeymaps = 1, - .scroll = {0}, - .update_hooks = {0}, - .nkeymaps_max = 10, - .lang = lang_from_id("fnd"), - }; - - undo_init(&b.undo, 100); - - b.keymaps[0] = keymap_create("buffer-default", 128); - struct binding bindings[] = { - BINDING(Ctrl, 'B', "backward-char"), - BINDING(LEFT, "backward-char"), - BINDING(Ctrl, 'F', "forward-char"), - BINDING(RIGHT, "forward-char"), - - BINDING(Ctrl, 'P', "backward-line"), - BINDING(UP, "backward-line"), - BINDING(Ctrl, 'N', "forward-line"), - BINDING(DOWN, "forward-line"), - - BINDING(Meta, 'f', "forward-word"), - BINDING(Meta, 'b', "backward-word"), - - BINDING(Ctrl, 'A', "beginning-of-line"), - BINDING(Ctrl, 'E', "end-of-line"), - - BINDING(Meta, '<', "goto-beginning"), - BINDING(Meta, '>', "goto-end"), - - BINDING(ENTER, "newline"), - BINDING(TAB, "indent"), - - BINDING(Ctrl, 'K', "kill-line"), - BINDING(DELETE, "delete-char"), - BINDING(Ctrl, 'D', "delete-char"), - BINDING(BACKSPACE, "backward-delete-char"), - - BINDING(Ctrl, '@', "set-mark"), - - BINDING(Ctrl, 'W', "cut"), - BINDING(Ctrl, 'Y', "paste"), - BINDING(Meta, 'y', "paste-older"), - BINDING(Meta, 'w', "copy"), - - BINDING(Ctrl, '_', "undo"), - }; - keymap_bind_keys(&b.keymaps[0], bindings, - sizeof(bindings) / sizeof(bindings[0])); - - if (modeline) { - b.modeline = calloc(1, sizeof(struct modeline)); - b.modeline->buffer = malloc(1024); - b.modeline->buffer[0] = '\0'; - buffer_add_update_hook(&b, buffer_modeline_hook, b.modeline); - } - - if (modeline) { - buffer_add_update_hook(&b, buffer_linenum_hook, NULL); - } - - 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; - - for (uint32_t keymapi = 0; keymapi < buffer->nkeymaps; ++keymapi) { - keymap_destroy(&buffer->keymaps[keymapi]); - } - free(buffer->keymaps); - buffer->keymaps = NULL; - - if (buffer->modeline != NULL) { - free(buffer->modeline->buffer); - free(buffer->modeline); - } - - undo_destroy(&buffer->undo); -} - -void buffer_clear(struct buffer *buffer) { - text_clear(buffer->text); - buffer->dot.col = buffer->dot.line = 0; -} - -#define BUFFER_WRAPCMD(fn) \ - static int32_t fn##_cmd(struct command_ctx ctx, int argc, \ - const char *argv[]) { \ - fn(ctx.active_window->buffer); \ - return 0; \ - } - -// commands -BUFFER_WRAPCMD(buffer_kill_line); -BUFFER_WRAPCMD(buffer_forward_delete_char); -BUFFER_WRAPCMD(buffer_backward_delete_char); -BUFFER_WRAPCMD(buffer_backward_char); -BUFFER_WRAPCMD(buffer_backward_word); -BUFFER_WRAPCMD(buffer_forward_char); -BUFFER_WRAPCMD(buffer_forward_word); -BUFFER_WRAPCMD(buffer_backward_line); -BUFFER_WRAPCMD(buffer_forward_line); -BUFFER_WRAPCMD(buffer_end_of_line); -BUFFER_WRAPCMD(buffer_beginning_of_line); -BUFFER_WRAPCMD(buffer_newline); -BUFFER_WRAPCMD(buffer_indent); -BUFFER_WRAPCMD(buffer_to_file); -BUFFER_WRAPCMD(buffer_set_mark); -BUFFER_WRAPCMD(buffer_clear_mark); -BUFFER_WRAPCMD(buffer_copy); -BUFFER_WRAPCMD(buffer_cut); -BUFFER_WRAPCMD(buffer_paste); -BUFFER_WRAPCMD(buffer_paste_older); -BUFFER_WRAPCMD(buffer_goto_beginning); -BUFFER_WRAPCMD(buffer_goto_end); -BUFFER_WRAPCMD(buffer_undo); - -void buffer_static_init(struct commands *commands) { - 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}); - - static struct command buffer_commands[] = { - {.name = "kill-line", .fn = buffer_kill_line_cmd}, - {.name = "delete-char", .fn = buffer_forward_delete_char_cmd}, - {.name = "backward-delete-char", .fn = buffer_backward_delete_char_cmd}, - {.name = "backward-char", .fn = buffer_backward_char_cmd}, - {.name = "backward-word", .fn = buffer_backward_word_cmd}, - {.name = "forward-char", .fn = buffer_forward_char_cmd}, - {.name = "forward-word", .fn = buffer_forward_word_cmd}, - {.name = "backward-line", .fn = buffer_backward_line_cmd}, - {.name = "forward-line", .fn = buffer_forward_line_cmd}, - {.name = "end-of-line", .fn = buffer_end_of_line_cmd}, - {.name = "beginning-of-line", .fn = buffer_beginning_of_line_cmd}, - {.name = "newline", .fn = buffer_newline_cmd}, - {.name = "indent", .fn = buffer_indent_cmd}, - {.name = "buffer-write-to-file", .fn = buffer_to_file_cmd}, - {.name = "set-mark", .fn = buffer_set_mark_cmd}, - {.name = "clear-mark", .fn = buffer_clear_mark_cmd}, - {.name = "copy", .fn = buffer_copy_cmd}, - {.name = "cut", .fn = buffer_cut_cmd}, - {.name = "paste", .fn = buffer_paste_cmd}, - {.name = "paste-older", .fn = buffer_paste_older_cmd}, - {.name = "goto-beginning", .fn = buffer_goto_beginning_cmd}, - {.name = "goto-end", .fn = buffer_goto_end_cmd}, - {.name = "undo", .fn = buffer_undo_cmd}, - }; - - register_commands(commands, buffer_commands, - sizeof(buffer_commands) / sizeof(buffer_commands[0])); -} - -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; -} - -uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out) { - *keymaps_out = buffer->keymaps; - return buffer->nkeymaps; -} - -void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap) { - if (buffer->nkeymaps == buffer->nkeymaps_max) { - buffer->nkeymaps_max *= 2; - buffer->keymaps = - realloc(buffer->keymaps, sizeof(struct keymap) * buffer->nkeymaps_max); - } - buffer->keymaps[buffer->nkeymaps] = *keymap; - ++buffer->nkeymaps; -} - -void buffer_goto_beginning(struct buffer *buffer) { - buffer->dot.col = 0; - buffer->dot.line = 0; -} - -void buffer_goto_end(struct buffer *buffer) { - buffer->dot.line = text_num_lines(buffer->text); - buffer->dot.col = 0; -} - -bool movev(struct buffer *buffer, int rowdelta) { - int64_t new_line = (int64_t)buffer->dot.line + rowdelta; - - if (new_line < 0) { - buffer->dot.line = 0; - return false; - } else if (new_line > text_num_lines(buffer->text)) { - buffer->dot.line = text_num_lines(buffer->text); - return false; - } else { - buffer->dot.line = (uint32_t)new_line; - - // make sure column stays on the line - uint32_t linelen = text_line_length(buffer->text, buffer->dot.line); - buffer->dot.col = buffer->dot.col > linelen ? linelen : buffer->dot.col; - return true; - } -} - -// move dot `coldelta` chars -bool moveh(struct buffer *buffer, int coldelta) { - int64_t new_col = (int64_t)buffer->dot.col + coldelta; - - if (new_col > (int64_t)text_line_length(buffer->text, buffer->dot.line)) { - if (movev(buffer, 1)) { - buffer->dot.col = 0; - } - } else if (new_col < 0) { - if (movev(buffer, -1)) { - buffer->dot.col = text_line_length(buffer->text, buffer->dot.line); - } else { - return false; - } - } else { - buffer->dot.col = new_col; - } - - return true; -} - -void buffer_goto(struct buffer *buffer, uint32_t line, uint32_t col) { - int64_t linedelta = (int64_t)line - (int64_t)buffer->dot.line; - movev(buffer, linedelta); - - int64_t coldelta = (int64_t)col - (int64_t)buffer->dot.col; - moveh(buffer, 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 *buffer) { - return to_region(buffer->dot, buffer->mark); -} - -bool buffer_region_has_size(struct buffer *buffer) { - return buffer->mark_set && - (labs((int64_t)buffer->mark.line - (int64_t)buffer->dot.line) + - labs((int64_t)buffer->mark.col - (int64_t)buffer->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 *buffer) { - if (buffer_region_has_size(buffer)) { - struct region reg = buffer_get_region(buffer); - struct text_chunk *curr = copy_region(buffer, reg); - buffer_clear_mark(buffer); - } -} - -void paste(struct buffer *buffer, 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 = buffer->mark_set ? buffer->mark : buffer->dot; - buffer_add_text(buffer, curr->text, curr->nbytes); - g_kill_ring.paste_up_to_date = true; - } - } -} - -void buffer_paste(struct buffer *buffer) { - g_kill_ring.paste_idx = g_kill_ring.curr_idx; - paste(buffer, g_kill_ring.curr_idx); -} - -void buffer_paste_older(struct buffer *buffer) { - if (g_kill_ring.paste_up_to_date) { - - // remove previous paste - struct text_chunk *curr = &g_kill_ring.buffer[g_kill_ring.curr_idx]; - delete_with_undo(buffer, g_kill_ring.last_paste, buffer->dot); - - // place ourselves right - buffer->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(buffer, g_kill_ring.paste_idx); - - } else { - buffer_paste(buffer); - } -} - -void buffer_cut(struct buffer *buffer) { - if (buffer_region_has_size(buffer)) { - struct region reg = buffer_get_region(buffer); - copy_region(buffer, reg); - delete_with_undo(buffer, reg.begin, reg.end); - buffer_clear_mark(buffer); - buffer->dot = reg.begin; - } -} - -bool maybe_delete_region(struct buffer *buffer) { - if (buffer_region_has_size(buffer)) { - struct region reg = buffer_get_region(buffer); - delete_with_undo(buffer, reg.begin, reg.end); - buffer_clear_mark(buffer); - buffer->dot = reg.begin; - return true; - } - - return false; -} - -void buffer_kill_line(struct buffer *buffer) { - if (text_num_lines(buffer->text) == 0) { - return; - } - - uint32_t nchars = - text_line_length(buffer->text, buffer->dot.line) - buffer->dot.col; - if (nchars == 0) { - nchars = 1; - } - - struct region reg = { - .begin = buffer->dot, - .end = - { - .line = buffer->dot.line, - .col = buffer->dot.col + nchars, - }, - }; - copy_region(buffer, reg); - delete_with_undo(buffer, buffer->dot, - (struct buffer_location){ - .line = buffer->dot.line, - .col = buffer->dot.col + nchars, - }); -} - -void buffer_forward_delete_char(struct buffer *buffer) { - if (maybe_delete_region(buffer)) { - return; - } - - delete_with_undo(buffer, buffer->dot, - (struct buffer_location){ - .line = buffer->dot.line, - .col = buffer->dot.col + 1, - }); -} - -void buffer_backward_delete_char(struct buffer *buffer) { - if (maybe_delete_region(buffer)) { - return; - } - - if (moveh(buffer, -1)) { - buffer_forward_delete_char(buffer); - } -} - -void buffer_backward_char(struct buffer *buffer) { moveh(buffer, -1); } -void buffer_forward_char(struct buffer *buffer) { moveh(buffer, 1); } - -struct buffer_location find_next(struct buffer *buffer, uint8_t chars[], - uint32_t nchars, int direction) { - struct text_chunk line = text_get_line(buffer->text, buffer->dot.line); - int64_t bytei = - text_col_to_byteindex(buffer->text, buffer->dot.line, buffer->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(buffer->text, buffer->dot.line, bytei); - return (struct buffer_location){.line = buffer->dot.line, .col = target_col}; -} - -void buffer_forward_word(struct buffer *buffer) { - moveh(buffer, 1); - uint8_t chars[] = {' ', '.'}; - buffer->dot = find_next(buffer, chars, 2, 1); -} - -void buffer_backward_word(struct buffer *buffer) { - moveh(buffer, -1); - uint8_t chars[] = {' ', '.'}; - buffer->dot = find_next(buffer, chars, 2, -1); -} - -void buffer_backward_line(struct buffer *buffer) { movev(buffer, -1); } -void buffer_forward_line(struct buffer *buffer) { movev(buffer, 1); } - -void buffer_end_of_line(struct buffer *buffer) { - buffer->dot.col = text_line_length(buffer->text, buffer->dot.line); -} - -void buffer_beginning_of_line(struct buffer *buffer) { buffer->dot.col = 0; } - -struct buffer buffer_from_file(char *filename) { - struct buffer b = buffer_create(basename((char *)filename), true); - 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); -} - -int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) { - if (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(buffer); - - struct buffer_location initial = buffer->dot; - - uint32_t lines_added, cols_added; - text_insert_at(buffer->text, initial.line, initial.col, text, nbytes, - &lines_added, &cols_added); - - // move to after inserted text - movev(buffer, lines_added); - if (lines_added > 0) { - // does not make sense to use position from another line - buffer->dot.col = 0; - } - moveh(buffer, cols_added); - - struct buffer_location final = buffer->dot; - undo_push_add( - &buffer->undo, - (struct undo_add){.begin = {.row = initial.line, .col = initial.col}, - .end = {.row = final.line, .col = final.col}}); - - if (lines_added > 0) { - undo_push_boundary(&buffer->undo, - (struct undo_boundary){.save_point = false}); - } - - buffer->modified = true; - return lines_added; -} - -void buffer_newline(struct buffer *buffer) { - buffer_add_text(buffer, (uint8_t *)"\n", 1); -} - -void buffer_indent(struct buffer *buffer) { - uint32_t tab_width = buffer->lang.tab_width; - buffer_add_text(buffer, (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 *buffer) { - buffer->mark_set - ? buffer_clear_mark(buffer) - : buffer_set_mark_at(buffer, buffer->dot.line, buffer->dot.col); -} - -void buffer_clear_mark(struct buffer *buffer) { - buffer->mark_set = false; - minibuffer_echo_timeout(2, "mark cleared"); -} - -void buffer_set_mark_at(struct buffer *buffer, uint32_t line, uint32_t col) { - buffer->mark_set = true; - buffer->mark.line = line; - buffer->mark.col = col; - minibuffer_echo_timeout(2, "mark set"); -} - -void buffer_undo(struct buffer *buffer) { - struct undo_stack *undo = &buffer->undo; - undo_begin(undo); - - // fetch and handle records - struct undo_record *records = NULL; - uint32_t nrecords = 0; - - if (undo_current_position(undo) == INVALID_TOP) { - minibuffer_echo_timeout(4, - "no more undo information, starting from top..."); - } - - undo_next(undo, &records, &nrecords); - - undo_push_boundary(undo, (struct undo_boundary){.save_point = false}); - for (uint32_t reci = 0; reci < nrecords; ++reci) { - struct undo_record *rec = &records[reci]; - switch (rec->type) { - case Undo_Boundary: { - struct undo_boundary *b = &rec->boundary; - if (b->save_point) { - buffer->modified = false; - } - break; - } - case Undo_Add: { - struct undo_add *add = &rec->add; - - delete_with_undo(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(buffer, add->begin.row, add->begin.col); - break; - } - case Undo_Delete: { - struct undo_delete *del = &rec->delete; - buffer_goto(buffer, del->pos.row, del->pos.col); - buffer_add_text(buffer, 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); - uint8_t *text = line->text + scroll_bytes; - uint32_t text_nbytes = - scroll_bytes > line->nbytes ? 0 : line->nbytes - scroll_bytes; - uint32_t text_nchars = - scroll_bytes > line->nchars ? 0 : line->nchars - cmdbuf->scroll.col; - - 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); - - uint32_t col = text_nchars + cmdbuf->left_margin; - for (uint32_t bytei = scroll_bytes; bytei < line->nbytes; ++bytei) { - if (line->text[bytei] == '\t') { - 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) { - col += wcwidth(wc) - 1; - } - } - } - - if (col < cmdbuf->width) { - command_list_draw_repeated(cmdbuf->cmds, col, visual_line, ' ', - cmdbuf->width - col); - } -} - -void scroll(struct buffer *buffer, int line_delta, int col_delta) { - uint32_t nlines = text_num_lines(buffer->text); - int64_t new_line = (int64_t)buffer->scroll.line + line_delta; - if (new_line >= 0 && new_line < nlines) { - buffer->scroll.line = (uint32_t)new_line; - } else if (new_line < 0) { - buffer->scroll.line = 0; - } - - int64_t new_col = (int64_t)buffer->scroll.col + col_delta; - if (new_col >= 0 && - new_col < text_line_length(buffer->text, buffer->dot.line)) { - buffer->scroll.col = (uint32_t)new_col; - } else if (new_col < 0) { - buffer->scroll.col = 0; - } -} - -void to_relative(struct buffer *buffer, uint32_t line, uint32_t col, - int64_t *rel_line, int64_t *rel_col) { - *rel_col = (int64_t)col - (int64_t)buffer->scroll.col; - *rel_line = (int64_t)line - (int64_t)buffer->scroll.line; -} - -uint32_t visual_dot_col(struct buffer *buffer, uint32_t dot_col) { - uint32_t visual_dot_col = dot_col; - struct text_chunk line = text_get_line(buffer->text, buffer->dot.line); - for (uint32_t bytei = 0; - bytei < - text_col_to_byteindex(buffer->text, buffer->dot.line, buffer->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; -} - -struct update_hook_result buffer_modeline_hook(struct buffer *buffer, - struct command_list *commands, - uint32_t width, uint32_t height, - uint64_t frame_time, - void *userdata) { - 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)", - buffer->modified ? '*' : '-', buffer->readonly ? '%' : '-', - buffer->name, buffer->dot.line + 1, - visual_dot_col(buffer, buffer->dot.col), 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); - - struct modeline *modeline = (struct modeline *)userdata; - if (strcmp(buf, (char *)modeline->buffer) != 0) { - modeline->buffer = realloc(modeline->buffer, 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 update_hook_result res = {0}; - res.margins.bottom = 1; - return res; -} - -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); -} - -struct update_hook_result buffer_linenum_hook(struct buffer *buffer, - struct command_list *commands, - uint32_t width, uint32_t height, - uint64_t frame_time, - void *userdata) { - 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; - } - - linenum_data.longest_nchars = longest_nchars; - linenum_data.dot_line = buffer->dot.line; - struct update_hook_result res = {0}; - res.margins.left = longest_nchars + 2; - res.line_render_hook.callback = linenum_render_hook; - res.line_render_hook.empty_callback = clear_empty_linenum_lines; - res.line_render_hook.userdata = &linenum_data; - - return res; -} - -void buffer_update(struct buffer *buffer, 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]; - uint32_t nlinehooks = 0; - for (uint32_t hooki = 0; hooki < buffer->update_hooks.nhooks; ++hooki) { - struct update_hook *h = &buffer->update_hooks.hooks[hooki]; - struct update_hook_result res = - h->callback(buffer, commands, width, height, frame_time, h->userdata); - - struct margin margins = res.margins; - - if (res.line_render_hook.callback != NULL) { - line_hooks[nlinehooks] = res.line_render_hook; - ++nlinehooks; - } - - total_margins.left += margins.left; - total_margins.right += margins.right; - total_margins.top += margins.top; - total_margins.bottom += margins.bottom; - - height -= margins.top + margins.bottom; - width -= margins.left + margins.right; - } - - int64_t rel_line, rel_col; - to_relative(buffer, buffer->dot.line, buffer->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(buffer, line_delta, col_delta); - - struct setting *show_ws = settings_get("editor.show-whitespace"); - - struct cmdbuf cmdbuf = (struct cmdbuf){ - .cmds = commands, - .scroll = buffer->scroll, - .left_margin = total_margins.left, - .width = total_width, - .line_offset = total_margins.top, - .line_render_hooks = line_hooks, - .nlinerender_hooks = nlinehooks, - .mark_set = buffer->mark_set, - .region = to_region(buffer->dot, buffer->mark), - .show_ws = show_ws != NULL ? show_ws->value.bool_value : true, - }; - text_for_each_line(buffer->text, buffer->scroll.line, height, render_line, - &cmdbuf); - - // draw empty lines - uint32_t nlines = text_num_lines(buffer->text); - for (uint32_t linei = nlines - buffer->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(buffer, buffer->dot.line, buffer->dot.col, &rel_line, &rel_col); - uint32_t visual_col = visual_dot_col(buffer, buffer->dot.col); - to_relative(buffer, buffer->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); -} diff --git a/src/buffer.h b/src/buffer.h deleted file mode 100644 index 85518f5..0000000 --- a/src/buffer.h +++ /dev/null @@ -1,201 +0,0 @@ -#include -#include -#include - -#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 *buffer, 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; -}; - -/** - * 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; -}; - -/** - * 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; - - /** Location of dot (cursor) */ - struct buffer_location dot; - - /** Location of mark (where a selection starts) */ - struct buffer_location mark; - - /** True if the start of a selection has been set */ - bool mark_set; - - /** Buffer-local keymaps in reverse priority order */ - struct keymap *keymaps; - - /** Number of buffer-local keymaps */ - uint32_t nkeymaps; - - /** Maximum number of keymaps */ - uint32_t nkeymaps_max; - - /** Current buffer scroll position */ - struct buffer_location scroll; - - /** 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; - - /** Modeline buffer (may be NULL) */ - struct modeline *modeline; - - /** Buffer programming language */ - struct language lang; -}; - -struct buffer buffer_create(char *name, bool modeline); -void buffer_destroy(struct buffer *buffer); - -void buffer_static_init(struct commands *commands); -void buffer_static_teardown(); - -uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out); -void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap); - -int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes); -void buffer_clear(struct buffer *buffer); -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 *buffer); -void buffer_forward_delete_char(struct buffer *buffer); -void buffer_backward_delete_char(struct buffer *buffer); -void buffer_backward_char(struct buffer *buffer); -void buffer_backward_word(struct buffer *buffer); -void buffer_forward_char(struct buffer *buffer); -void buffer_forward_word(struct buffer *buffer); -void buffer_backward_line(struct buffer *buffer); -void buffer_forward_line(struct buffer *buffer); -void buffer_end_of_line(struct buffer *buffer); -void buffer_beginning_of_line(struct buffer *buffer); -void buffer_newline(struct buffer *buffer); -void buffer_indent(struct buffer *buffer); - -void buffer_undo(struct buffer *buffer); - -void buffer_goto_beginning(struct buffer *buffer); -void buffer_goto_end(struct buffer *buffer); -void buffer_goto(struct buffer *buffer, uint32_t line, uint32_t col); - -void buffer_set_mark(struct buffer *buffer); -void buffer_clear_mark(struct buffer *buffer); -void buffer_set_mark_at(struct buffer *buffer, uint32_t line, uint32_t col); - -void buffer_copy(struct buffer *buffer); -void buffer_paste(struct buffer *buffer); -void buffer_paste_older(struct buffer *buffer); -void buffer_cut(struct buffer *buffer); - -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); - -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 *buffer, uint32_t width, uint32_t height, - struct command_list *commands, uint64_t frame_time, - uint32_t *relline, uint32_t *relcol); diff --git a/src/buffers.c b/src/buffers.c deleted file mode 100644 index 38b51b7..0000000 --- a/src/buffers.c +++ /dev/null @@ -1,43 +0,0 @@ -#include "buffers.h" -#include "buffer.h" - -#include -#include - -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/buffers.h b/src/buffers.h deleted file mode 100644 index edf772c..0000000 --- a/src/buffers.h +++ /dev/null @@ -1,17 +0,0 @@ -#include - -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/command.c b/src/command.c deleted file mode 100644 index 65543a0..0000000 --- a/src/command.c +++ /dev/null @@ -1,175 +0,0 @@ -#include "command.h" -#include "buffer.h" -#include "buffers.h" -#include "hash.h" -#include "hashmap.h" -#include "minibuffer.h" - -#include -#include -#include -#include - -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; -} - -int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]) { - const char *pth = NULL; - if (argc == 0) { - return minibuffer_prompt(ctx, "find file: "); - } - - pth = argv[0]; - struct stat sb; - if (stat(pth, &sb) < 0 && errno != ENOENT) { - minibuffer_echo("stat on %s failed: %s", pth, strerror(errno)); - return 1; - } - - if (S_ISDIR(sb.st_mode) && errno != ENOENT) { - minibuffer_echo("TODO: implement dired!"); - return 1; - } - - window_set_buffer(ctx.active_window, - buffers_add(ctx.buffers, buffer_from_file((char *)pth))); - minibuffer_echo_timeout(4, "buffer \"%s\" loaded", - ctx.active_window->buffer->name); - - return 0; -} - -int32_t write_file(struct command_ctx ctx, int argc, const char *argv[]) { - const char *pth = NULL; - if (argc == 0) { - return minibuffer_prompt(ctx, "write to file: "); - } - - pth = argv[0]; - buffer_write_to(ctx.active_window->buffer, pth); - - return 0; -} - -int32_t run_interactive(struct command_ctx ctx, int argc, const char *argv[]) { - if (argc == 0) { - return minibuffer_prompt(ctx, "execute: "); - } - - struct command *cmd = lookup_command(ctx.commands, argv[0]); - if (cmd != NULL) { - return execute_command(cmd, ctx.commands, ctx.active_window, ctx.buffers, - argc - 1, argv + 1); - } else { - minibuffer_echo_timeout(4, "command %s not found", argv[0]); - return 11; - } -} - -int32_t do_switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { - const char *bufname = argv[0]; - if (argc == 0) { - // switch back to prev buffer - if (ctx.active_window->prev_buffer != NULL) { - bufname = ctx.active_window->prev_buffer->name; - } else { - return 0; - } - } - - struct buffer *buf = buffers_find(ctx.buffers, bufname); - - if (buf == NULL) { - minibuffer_echo_timeout(4, "buffer %s not found", bufname); - return 1; - } else { - window_set_buffer(ctx.active_window, buf); - return 0; - } -} - -static struct command do_switch_buffer_cmd = {.fn = do_switch_buffer, - .name = "do-switch-buffer"}; - -int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { - if (argc == 0) { - ctx.self = &do_switch_buffer_cmd; - if (ctx.active_window->prev_buffer != NULL) { - return minibuffer_prompt( - ctx, "buffer (default %s): ", ctx.active_window->prev_buffer->name); - } else { - return minibuffer_prompt(ctx, "buffer: "); - } - } - - return execute_command(&do_switch_buffer_cmd, ctx.commands, ctx.active_window, - ctx.buffers, argc, argv); -} diff --git a/src/command.h b/src/command.h deleted file mode 100644 index 7ece486..0000000 --- a/src/command.h +++ /dev/null @@ -1,205 +0,0 @@ -#ifndef _COMMAND_H -#define _COMMAND_H - -/** @file command.h - * Commands and command registries - */ -#include "hashmap.h" -#include - -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); - -/** - * @defgroup common-commands Implementation of common commands - * @{ - */ - -/** - * Find and visit a file in the current window. - */ -int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]); - -/** - * Write the active buffer to a file - */ -int32_t write_file(struct command_ctx ctx, int argc, const char *argv[]); - -/** - * Run a command interactively from the minibuffer. - */ -int32_t run_interactive(struct command_ctx ctx, int argc, const char *argv[]); - -/** - * Switch to another buffer in the currently active window - */ -int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]); - -/**@}*/ -#endif 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 +#include +#include + +/** + * 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 +#include + +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 + +#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 +#include +#include +#include +#include +#include +#include +#include +#include + +struct modeline { + uint8_t *buffer; + uint32_t sz; +}; + +#define KILL_RING_SZ 64 +static struct kill_ring { + struct text_chunk buffer[KILL_RING_SZ]; + struct 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 +#include +#include + +#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 +#include + +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 + +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 + +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 + +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 +#include +#include +#include +#include +#include +#include + +#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 +#include +#include + +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 + +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 + +#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 +#include +#include +#include +#include +#include + +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 +#include +#include + +/** + * 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 +#include +#include + +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 +#include + +/** + * 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 +#include +#include +#include + +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 +#include +#include +#include + +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 +#include +#include + +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 +#include + +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 +#include +#include + +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 +#include + +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 ... + * @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 ... + * @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 ... + * @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 +#include +#include +#include + +#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 +#include +#include + +// 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 +#include + +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 +#include + +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 + +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 +#include + +/*! + * \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 + +#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 +#include + +#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); diff --git a/src/display.c b/src/display.c deleted file mode 100644 index 77b5b32..0000000 --- a/src/display.c +++ /dev/null @@ -1,386 +0,0 @@ -#define _DEFAULT_SOURCE -#include "display.h" - -#include "buffer.h" - -#include -#include -#include -#include -#include -#include -#include - -#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 + list->xoffset; - cmd->row = row + list->yoffset; - 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/display.h b/src/display.h deleted file mode 100644 index 14dd246..0000000 --- a/src/display.h +++ /dev/null @@ -1,212 +0,0 @@ -#include -#include -#include - -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/hash.h b/src/hash.h deleted file mode 100644 index 0fd689b..0000000 --- a/src/hash.h +++ /dev/null @@ -1,11 +0,0 @@ -#include - -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/hashmap.h b/src/hashmap.h deleted file mode 100644 index 405c193..0000000 --- a/src/hashmap.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef _HASHMAP_H -#define _HASHMAP_H - -#include "vec.h" -#include - -#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/keyboard.c b/src/keyboard.c deleted file mode 100644 index 14bb9dd..0000000 --- a/src/keyboard.c +++ /dev/null @@ -1,159 +0,0 @@ -#define _DEFAULT_SOURCE -#include "keyboard.h" -#include "reactor.h" -#include "stdio.h" -#include "utf8.h" - -#include -#include -#include -#include -#include -#include - -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); -} - -void 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/keyboard.h b/src/keyboard.h deleted file mode 100644 index 09a71be..0000000 --- a/src/keyboard.h +++ /dev/null @@ -1,143 +0,0 @@ -#include -#include -#include - -/** - * 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. - */ -void key_name(struct key *key, char *buf, size_t capacity); diff --git a/src/lang.c b/src/lang.c deleted file mode 100644 index 6919780..0000000 --- a/src/lang.c +++ /dev/null @@ -1,160 +0,0 @@ -#include "lang.h" -#include "minibuffer.h" -#include "settings.h" - -#include -#include -#include - -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/lang.h b/src/lang.h deleted file mode 100644 index 984e207..0000000 --- a/src/lang.h +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef _LANG_H -#define _LANG_H - -#include -#include - -/** - * 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/main.c b/src/main.c deleted file mode 100644 index c0b6b0c..0000000 --- a/src/main.c +++ /dev/null @@ -1,364 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include "allocator.h" -#include "binding.h" -#include "bits/getopt_core.h" -#include "bits/getopt_ext.h" -#include "buffer.h" -#include "buffers.h" -#include "display.h" -#include "lang.h" -#include "minibuffer.h" -#include "reactor.h" -#include "settings.h" - -struct frame_allocator frame_allocator; - -void *frame_alloc(size_t sz) { - return frame_allocator_alloc(&frame_allocator, sz); -} - -bool running = true; - -void terminate() { running = false; } - -static struct display *display = NULL; -static bool display_resized = false; -void resized() { - if (display != NULL) { - display_resize(display); - } - display_resized = true; - - signal(SIGWINCH, resized); -} - -int32_t _abort(struct command_ctx ctx, int argc, const char *argv[]) { - minibuffer_abort_prompt(); - minibuffer_echo_timeout(4, "💣 aborted"); - return 0; -} - -int32_t unimplemented_command(struct command_ctx ctx, int argc, - const char *argv[]) { - minibuffer_echo("TODO: %s is not implemented", (const char *)ctx.userdata); - return 0; -} - -int32_t exit_editor(struct command_ctx ctx, int argc, const char *argv[]) { - terminate(); - return 0; -} - -static struct command GLOBAL_COMMANDS[] = { - {.name = "find-file", .fn = find_file}, - {.name = "write-file", .fn = write_file}, - {.name = "run-command-interactive", .fn = run_interactive}, - {.name = "switch-buffer", .fn = switch_buffer}, - {.name = "abort", .fn = _abort}, - {.name = "exit", .fn = exit_editor}}; - -uint64_t calc_frame_time_ns(struct timespec *timers, uint32_t num_timer_pairs) { - uint64_t total = 0; - for (uint32_t ti = 0; ti < num_timer_pairs * 2; ti += 2) { - struct timespec *start_timer = &timers[ti]; - struct timespec *end_timer = &timers[ti + 1]; - - total += - ((uint64_t)end_timer->tv_sec * 1e9 + (uint64_t)end_timer->tv_nsec) - - ((uint64_t)start_timer->tv_sec * 1e9 + (uint64_t)start_timer->tv_nsec); - } - - return total; -} - -void usage() { printf("TODO: print usage\n"); } - -int main(int argc, char *argv[]) { - - static struct option longopts[] = {{"line", required_argument, NULL, 'l'}, - {"end", no_argument, NULL, 'e'}, - {NULL, 0, NULL, 0}}; - - char *filename = NULL; - uint32_t jumpline = 1; - bool goto_end = false; - char ch; - while ((ch = getopt_long(argc, argv, "el:", longopts, NULL)) != -1) { - switch (ch) { - case 'l': - jumpline = atoi(optarg); - break; - case 'e': - goto_end = true; - break; - default: - usage(); - return 1; - } - } - argc -= optind; - argv += optind; - - if (argc > 1) { - fprintf(stderr, "More than one file to open is not supported\n"); - return 2; - } else if (argc == 1) { - filename = strdup(argv[0]); - } - - setlocale(LC_ALL, ""); - - signal(SIGTERM, terminate); - - struct commands commands = command_registry_create(32); - settings_init(64, &commands); - languages_init(true); - buffer_static_init(&commands); - - frame_allocator = frame_allocator_create(16 * 1024 * 1024); - - // create reactor - struct reactor *reactor = reactor_create(); - - // initialize display - display = display_create(); - display_clear(display); - signal(SIGWINCH, resized); - - // init keyboard - struct keyboard kbd = keyboard_create(reactor); - - // global commands, TODO: move these, they should exist even if main does not - register_commands(&commands, GLOBAL_COMMANDS, - sizeof(GLOBAL_COMMANDS) / sizeof(GLOBAL_COMMANDS[0])); - - // keymaps - struct keymap *current_keymap = NULL; - struct keymap global_keymap = keymap_create("global", 32); - struct keymap ctrlx_map = keymap_create("c-x", 32); - struct binding global_binds[] = { - PREFIX(Ctrl, 'X', &ctrlx_map), - BINDING(Ctrl, 'G', "abort"), - BINDING(Meta, 'x', "run-command-interactive"), - }; - struct binding ctrlx_bindings[] = { - BINDING(Ctrl, 'C', "exit"), - BINDING(Ctrl, 'S', "buffer-write-to-file"), - BINDING(Ctrl, 'F', "find-file"), - BINDING(Ctrl, 'W', "write-file"), - BINDING(None, 'b', "switch-buffer"), - }; - keymap_bind_keys(&global_keymap, global_binds, - sizeof(global_binds) / sizeof(global_binds[0])); - keymap_bind_keys(&ctrlx_map, ctrlx_bindings, - sizeof(ctrlx_bindings) / sizeof(ctrlx_bindings[0])); - - struct buffers buflist = {0}; - buffers_init(&buflist, 32); - struct buffer initial_buffer = buffer_create("welcome", true); - if (filename != NULL) { - buffer_destroy(&initial_buffer); - initial_buffer = buffer_from_file(filename); - if (goto_end) { - buffer_goto_end(&initial_buffer); - } else - buffer_goto(&initial_buffer, jumpline > 0 ? jumpline - 1 : 0, 0); - } else { - const char *welcome_txt = "Welcome to the editor for datagubbar 👴\n"; - buffer_add_text(&initial_buffer, (uint8_t *)welcome_txt, - strlen(welcome_txt)); - } - - // one main window - struct window main_window = (struct window){ - .buffer = buffers_add(&buflist, initial_buffer), - .prev_buffer = NULL, - .height = display_height(display) - 1, - .width = display_width(display), - .x = 0, - .y = 0, - }; - - // and one for the minibuffer - struct buffer minibuffer = buffer_create("minibuffer", false); - - minibuffer_init(&minibuffer); - struct window minibuffer_window = (struct window){ - .buffer = &minibuffer, - .prev_buffer = NULL, - .x = 0, - .y = display_height(display) - 1, - .height = 1, - .width = display_width(display), - }; - - struct timespec buffer_begin, buffer_end, display_begin, display_end, - keyboard_begin, keyboard_end; - - uint64_t frame_time = 0; - - struct window *windows[2] = { - &minibuffer_window, - &main_window, - }; - - struct command_list *command_lists[2] = {0}; - - // TODO: not always - struct window *active_window = &main_window; - - while (running) { - - clock_gettime(CLOCK_MONOTONIC, &buffer_begin); - - if (display_resized) { - minibuffer_window.width = display_width(display); - minibuffer_window.y = display_height(display) - 1; - - main_window.height = display_height(display) - 1; - main_window.width = display_width(display); - - display_resized = false; - } - - // update windows - uint32_t dot_line = 0, dot_col = 0; - for (uint32_t windowi = 0; windowi < sizeof(windows) / sizeof(windows[0]); - ++windowi) { - struct window *win = windows[windowi]; - // TODO: better capacity - command_lists[windowi] = - command_list_create(win->height * win->width, frame_alloc, win->x, - win->y, win->buffer->name); - - uint32_t relline, relcol; - window_update_buffer(win, command_lists[windowi], frame_time, &relline, - &relcol); - - if (win == active_window) { - dot_line = relline; - dot_col = relcol; - } - } - - clock_gettime(CLOCK_MONOTONIC, &buffer_end); - - // update screen - clock_gettime(CLOCK_MONOTONIC, &display_begin); - uint32_t relline, relcol; - - display_begin_render(display); - for (uint32_t windowi = 0; windowi < sizeof(windows) / sizeof(windows[0]); - ++windowi) { - display_render(display, command_lists[windowi]); - } - display_move_cursor(display, dot_line + active_window->y, - dot_col + active_window->x); - display_end_render(display); - clock_gettime(CLOCK_MONOTONIC, &display_end); - - // this blocks for events, so if nothing has happened we block here. - reactor_update(reactor); - - clock_gettime(CLOCK_MONOTONIC, &keyboard_begin); - struct keymap *local_keymaps = NULL; - uint32_t nbuffer_keymaps = - buffer_keymaps(active_window->buffer, &local_keymaps); - struct keyboard_update kbd_upd = - keyboard_update(&kbd, reactor, frame_alloc); - - uint32_t input_data_idx = 0; - for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) { - struct key *k = &kbd_upd.keys[ki]; - - struct lookup_result res = {.found = false}; - if (current_keymap != NULL) { - res = lookup_key(current_keymap, 1, k, &commands); - } else { - // check first the global keymap, then the buffer ones - res = lookup_key(&global_keymap, 1, k, &commands); - if (!res.found) { - res = lookup_key(local_keymaps, nbuffer_keymaps, k, &commands); - } - } - - if (res.found) { - switch (res.type) { - case BindingType_Command: { - if (res.command == NULL) { - minibuffer_echo_timeout( - 4, "binding found for key %s but not command", k); - } else { - int32_t ec = execute_command(res.command, &commands, active_window, - &buflist, 0, NULL); - if (ec != 0 && !minibuffer_displaying()) { - minibuffer_echo_timeout(4, "command %s failed with exit code %d", - res.command->name, ec); - } - } - current_keymap = NULL; - break; - } - case BindingType_Keymap: { - char keyname[16]; - key_name(k, keyname, 16); - current_keymap = res.keymap; - minibuffer_echo("%s", current_keymap->name); - break; - } - } - } else if (k->mod == 0) { - buffer_add_text(active_window->buffer, &kbd_upd.raw[k->start], - k->end - k->start); - } else { - char keyname[16]; - key_name(k, keyname, 16); - if (current_keymap == NULL) { - minibuffer_echo_timeout(4, "key \"%s\" is not bound!", keyname); - } else { - minibuffer_echo_timeout(4, "key \"%s %s\" is not bound!", - current_keymap->name, keyname); - } - current_keymap = NULL; - } - } - clock_gettime(CLOCK_MONOTONIC, &keyboard_end); - - // calculate frame time - struct timespec timers[] = {buffer_begin, buffer_end, display_begin, - display_end, keyboard_begin, keyboard_end}; - frame_time = calc_frame_time_ns(timers, 3); - - if (minibuffer_focused()) { - active_window = &minibuffer_window; - } else { - // TODO: not this - active_window = &main_window; - } - - frame_allocator_clear(&frame_allocator); - } - - minibuffer_destroy(); - buffer_destroy(&minibuffer); - buffers_destroy(&buflist); - display_clear(display); - display_destroy(display); - keymap_destroy(&global_keymap); - keymap_destroy(&ctrlx_map); - command_registry_destroy(&commands); - reactor_destroy(reactor); - frame_allocator_destroy(&frame_allocator); - buffer_static_teardown(); - settings_destroy(); - - return 0; -} diff --git a/src/main/bindings.c b/src/main/bindings.c new file mode 100644 index 0000000..10436d6 --- /dev/null +++ b/src/main/bindings.c @@ -0,0 +1,207 @@ +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/minibuffer.h" +#include "dged/vec.h" + +static struct keymap g_global_keymap, g_ctrlx_map, g_windows_keymap, + g_buffer_default_keymap; + +struct buffer_keymap { + struct buffer *buffer; + bool active; + struct keymap keymap; +}; + +static VEC(struct buffer_keymap) g_buffer_keymaps; + +void set_default_buffer_bindings(struct keymap *keymap) { + struct binding buffer_bindings[] = { + BINDING(Ctrl, 'B', "backward-char"), + BINDING(LEFT, "backward-char"), + BINDING(Ctrl, 'F', "forward-char"), + BINDING(RIGHT, "forward-char"), + + BINDING(Ctrl, 'P', "backward-line"), + BINDING(UP, "backward-line"), + BINDING(Ctrl, 'N', "forward-line"), + BINDING(DOWN, "forward-line"), + + BINDING(Meta, 'f', "forward-word"), + BINDING(Meta, 'b', "backward-word"), + + BINDING(Ctrl, 'A', "beginning-of-line"), + BINDING(Ctrl, 'E', "end-of-line"), + + BINDING(Ctrl, 'S', "find-next"), + BINDING(Ctrl, 'R', "find-prev"), + + BINDING(Meta, '<', "goto-beginning"), + BINDING(Meta, '>', "goto-end"), + + BINDING(Ctrl, 'V', "scroll-down"), + BINDING(Meta, 'v', "scroll-up"), + + BINDING(ENTER, "newline"), + BINDING(TAB, "indent"), + + BINDING(Ctrl, 'K', "kill-line"), + BINDING(DELETE, "delete-char"), + BINDING(Ctrl, 'D', "delete-char"), + BINDING(BACKSPACE, "backward-delete-char"), + + BINDING(Ctrl, '@', "set-mark"), + + BINDING(Ctrl, 'W', "cut"), + BINDING(Ctrl, 'Y', "paste"), + BINDING(Meta, 'y', "paste-older"), + BINDING(Meta, 'w', "copy"), + + BINDING(Ctrl, '_', "undo"), + }; + + keymap_bind_keys(keymap, buffer_bindings, + sizeof(buffer_bindings) / sizeof(buffer_bindings[0])); +} + +struct keymap *register_bindings() { + g_global_keymap = keymap_create("global", 32); + g_ctrlx_map = keymap_create("c-x", 32); + g_windows_keymap = keymap_create("c-x w", 32); + + struct binding global_binds[] = { + PREFIX(Ctrl, 'X', &g_ctrlx_map), + BINDING(Ctrl, 'G', "abort"), + BINDING(Meta, 'x', "run-command-interactive"), + }; + + struct binding ctrlx_bindings[] = { + BINDING(Ctrl, 'C', "exit"), + BINDING(Ctrl, 'S', "buffer-write-to-file"), + BINDING(Ctrl, 'F', "find-file"), + BINDING(Ctrl, 'W', "write-file"), + BINDING(None, 'b', "switch-buffer"), + + BINDING(None, '0', "window-close"), + BINDING(None, '1', "window-close-others"), + BINDING(None, '2', "window-split-horizontal"), + BINDING(None, '3', "window-split-vertical"), + BINDING(None, 'o', "window-focus-next"), + + PREFIX(None, 'w', &g_windows_keymap), + }; + + // windows + struct binding window_subbinds[] = { + BINDING(None, '0', "window-focus-0"), + BINDING(None, '1', "window-focus-1"), + BINDING(None, '2', "window-focus-2"), + BINDING(None, '3', "window-focus-3"), + BINDING(None, '4', "window-focus-4"), + BINDING(None, '5', "window-focus-5"), + BINDING(None, '6', "window-focus-6"), + BINDING(None, '7', "window-focus-7"), + BINDING(None, '8', "window-focus-8"), + BINDING(None, '9', "window-focus-9"), + }; + + // buffers + g_buffer_default_keymap = keymap_create("buffer-default", 128); + set_default_buffer_bindings(&g_buffer_default_keymap); + + keymap_bind_keys(&g_windows_keymap, window_subbinds, + sizeof(window_subbinds) / sizeof(window_subbinds[0])); + keymap_bind_keys(&g_global_keymap, global_binds, + sizeof(global_binds) / sizeof(global_binds[0])); + keymap_bind_keys(&g_ctrlx_map, ctrlx_bindings, + sizeof(ctrlx_bindings) / sizeof(ctrlx_bindings[0])); + + VEC_INIT(&g_buffer_keymaps, 32); + + return &g_global_keymap; +} + +struct keymap *buffer_default_bindings() { + return &g_buffer_default_keymap; +} + +int32_t execute(struct command_ctx ctx, int argc, const char *argv[]) { + // TODO: this should be more lib-like + return minibuffer_execute(); +} + +static struct command execute_minibuffer_command = { + .fn = execute, + .name = "minibuffer-execute", + .userdata = NULL, +}; + +void buffer_bind_keys(struct buffer *buffer, struct binding *bindings, + uint32_t nbindings) { + struct buffer_keymap *target = NULL; + VEC_FOR_EACH(&g_buffer_keymaps, struct buffer_keymap * km) { + if (buffer == km->buffer) { + target = km; + } + } + + if (target == NULL) { + struct buffer_keymap new = (struct buffer_keymap){ + .buffer = buffer, + .active = false, + }; + VEC_PUSH(&g_buffer_keymaps, new); + target = VEC_BACK(&g_buffer_keymaps); + } + + if (!target->active) { + target->keymap = keymap_create("buffer-overlay-keys", 32); + target->active = true; + set_default_buffer_bindings(&target->keymap); + } + + keymap_bind_keys(&target->keymap, bindings, nbindings); +} + +// TODO: do something better +void reset_buffer_keys(struct buffer *buffer) { + VEC_FOR_EACH(&g_buffer_keymaps, struct buffer_keymap * km) { + if (buffer == km->buffer) { + keymap_destroy(&km->keymap); + km->active = false; + } + } +} + +struct keymap *buffer_keymap(struct buffer *buffer) { + VEC_FOR_EACH(&g_buffer_keymaps, struct buffer_keymap * km) { + if (buffer == km->buffer && km->active) { + return &km->keymap; + } + } + + return &g_buffer_default_keymap; +} + +void reset_minibuffer_keys(struct buffer *minibuffer) { + reset_buffer_keys(minibuffer); + struct binding bindings[] = { + ANONYMOUS_BINDING(ENTER, &execute_minibuffer_command), + }; + + buffer_bind_keys(minibuffer, bindings, + sizeof(bindings) / sizeof(bindings[0])); +} + +void destroy_keymaps() { + keymap_destroy(&g_windows_keymap); + keymap_destroy(&g_global_keymap); + keymap_destroy(&g_ctrlx_map); + keymap_destroy(&g_buffer_default_keymap); + + VEC_FOR_EACH(&g_buffer_keymaps, struct buffer_keymap * km) { + if (km->active) { + keymap_destroy(&km->keymap); + km->active = false; + } + } +} diff --git a/src/main/bindings.h b/src/main/bindings.h new file mode 100644 index 0000000..d0ba27c --- /dev/null +++ b/src/main/bindings.h @@ -0,0 +1,15 @@ +#include + +struct keymap; +struct buffer; +struct binding; + +struct keymap *register_bindings(); + +void buffer_bind_keys(struct buffer *buffer, struct binding *bindings, + uint32_t nbindings); +void reset_buffer_keys(struct buffer *buffer); +void reset_minibuffer_keys(struct buffer *minibuffer); +struct keymap *buffer_keymap(struct buffer *buffer); + +void destroy_keymaps(); diff --git a/src/main/cmds.c b/src/main/cmds.c new file mode 100644 index 0000000..2041cba --- /dev/null +++ b/src/main/cmds.c @@ -0,0 +1,519 @@ +#include +#include +#include +#include + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffers.h" +#include "dged/command.h" +#include "dged/minibuffer.h" +#include "dged/settings.h" + +#include "bindings.h" + +int32_t _abort(struct command_ctx ctx, int argc, const char *argv[]) { + minibuffer_abort_prompt(); + minibuffer_echo_timeout(4, "💣 aborted"); + return 0; +} + +int32_t unimplemented_command(struct command_ctx ctx, int argc, + const char *argv[]) { + minibuffer_echo("TODO: %s is not implemented", (const char *)ctx.userdata); + return 0; +} + +int32_t exit_editor(struct command_ctx ctx, int argc, const char *argv[]) { + ((void (*)())ctx.userdata)(); + return 0; +} + +int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]) { + const char *pth = NULL; + if (argc == 0) { + return minibuffer_prompt(ctx, "find file: "); + } + + pth = argv[0]; + struct stat sb = {0}; + if (stat(pth, &sb) < 0 && errno != ENOENT) { + minibuffer_echo("stat on %s failed: %s", pth, strerror(errno)); + return 1; + } + + if (S_ISDIR(sb.st_mode) && errno != ENOENT) { + minibuffer_echo("TODO: implement dired!"); + return 1; + } + + window_set_buffer(ctx.active_window, + buffers_add(ctx.buffers, buffer_from_file((char *)pth))); + minibuffer_echo_timeout(4, "buffer \"%s\" loaded", + window_buffer(ctx.active_window)->name); + + return 0; +} + +int32_t write_file(struct command_ctx ctx, int argc, const char *argv[]) { + const char *pth = NULL; + if (argc == 0) { + return minibuffer_prompt(ctx, "write to file: "); + } + + pth = argv[0]; + buffer_write_to(window_buffer(ctx.active_window), pth); + + return 0; +} + +int32_t run_interactive(struct command_ctx ctx, int argc, const char *argv[]) { + if (argc == 0) { + return minibuffer_prompt(ctx, "execute: "); + } + + struct command *cmd = lookup_command(ctx.commands, argv[0]); + if (cmd != NULL) { + return execute_command(cmd, ctx.commands, ctx.active_window, ctx.buffers, + argc - 1, argv + 1); + } else { + minibuffer_echo_timeout(4, "command %s not found", argv[0]); + return 11; + } +} + +int32_t do_switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { + const char *bufname = argv[0]; + if (argc == 0) { + // switch back to prev buffer + if (window_has_prev_buffer(ctx.active_window)) { + bufname = window_prev_buffer(ctx.active_window)->name; + } else { + return 0; + } + } + + struct buffer *buf = buffers_find(ctx.buffers, bufname); + + if (buf == NULL) { + minibuffer_echo_timeout(4, "buffer %s not found", bufname); + return 1; + } else { + window_set_buffer(ctx.active_window, buf); + return 0; + } +} + +static struct command do_switch_buffer_cmd = {.fn = do_switch_buffer, + .name = "do-switch-buffer"}; + +int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { + if (argc == 0) { + ctx.self = &do_switch_buffer_cmd; + if (window_has_prev_buffer(ctx.active_window)) { + return minibuffer_prompt(ctx, "buffer (default %s): ", + window_prev_buffer(ctx.active_window)->name); + } else { + return minibuffer_prompt(ctx, "buffer: "); + } + } + + return execute_command(&do_switch_buffer_cmd, ctx.commands, ctx.active_window, + ctx.buffers, argc, argv); +} + +static char *g_last_search = NULL; + +int64_t matchdist(struct match *match, struct buffer_location loc) { + struct buffer_location begin = match->begin; + + int64_t linedist = (int64_t)begin.line - (int64_t)loc.line; + int64_t coldist = (int64_t)begin.col - (int64_t)loc.col; + + return linedist * linedist + coldist * coldist; +} + +int buffer_loc_cmp(struct buffer_location loc1, struct buffer_location loc2) { + if (loc1.line < loc2.line) { + return -1; + } else if (loc1.line > loc2.line) { + return 1; + } else { + if (loc1.col < loc2.col) { + return -1; + } else if (loc1.col > loc2.col) { + return 1; + } else { + return 0; + } + } +} + +const char *search_prompt(bool reverse) { + const char *txt = "search (down): "; + if (reverse) { + txt = "search (up): "; + } + + return txt; +} + +void do_search(struct buffer_view *view, const char *pattern, bool reverse) { + struct match *matches = NULL; + uint32_t nmatches = 0; + + g_last_search = strdup(pattern); + + struct buffer_view *buffer_view = window_buffer_view(windows_get_active()); + buffer_find(buffer_view->buffer, pattern, &matches, &nmatches); + + // find the "nearest" match + if (nmatches > 0) { + struct match *closest = reverse ? &matches[nmatches - 1] : &matches[0]; + int64_t closest_dist = INT64_MAX; + for (uint32_t matchi = 0; matchi < nmatches; ++matchi) { + struct match *m = &matches[matchi]; + int res = buffer_loc_cmp(m->begin, view->dot); + int64_t dist = matchdist(m, view->dot); + if (((res < 0 && reverse) || (res > 0 && !reverse)) && + dist < closest_dist) { + closest_dist = dist; + closest = m; + } + } + buffer_goto(buffer_view, closest->begin.line, closest->begin.col); + } +} + +int32_t search_interactive(struct command_ctx ctx, int argc, + const char *argv[]) { + const char *pattern = NULL; + if (minibuffer_content().nbytes == 0) { + // recall the last search, if any + if (g_last_search != NULL) { + struct buffer_view *view = window_buffer_view(minibuffer_window()); + buffer_clear(view); + buffer_add_text(view, (uint8_t *)g_last_search, strlen(g_last_search)); + pattern = g_last_search; + } + } else { + struct text_chunk content = minibuffer_content(); + char *p = malloc(content.nbytes + 1); + memcpy(p, content.text, content.nbytes); + p[content.nbytes] = '\0'; + pattern = p; + } + + minibuffer_set_prompt(search_prompt(*(bool *)ctx.userdata)); + + if (pattern != NULL) { + // ctx.active_window would be the minibuffer window + do_search(window_buffer_view(windows_get_active()), pattern, + *(bool *)ctx.userdata); + } + return 0; +} + +static bool search_dir_backward = true; +static bool search_dir_forward = false; +static struct command search_forward_command = { + .fn = search_interactive, + .name = "search-forward", + .userdata = &search_dir_forward, +}; + +static struct command search_backward_command = { + .fn = search_interactive, + .name = "search-backward", + .userdata = &search_dir_backward, +}; + +int32_t find(struct command_ctx ctx, int argc, const char *argv[]) { + bool reverse = strcmp((char *)ctx.userdata, "backward") == 0; + if (argc == 0) { + struct binding bindings[] = { + ANONYMOUS_BINDING(Ctrl, 'S', &search_forward_command), + ANONYMOUS_BINDING(Ctrl, 'R', &search_backward_command), + }; + buffer_bind_keys(minibuffer_buffer(), bindings, + sizeof(bindings) / sizeof(bindings[0])); + return minibuffer_prompt(ctx, search_prompt(reverse)); + } + + reset_minibuffer_keys(minibuffer_buffer()); + do_search(window_buffer_view(ctx.active_window), argv[0], reverse); + + return 0; +} + +int32_t timers(struct command_ctx ctx, int argc, const char *argv[]) { + + struct buffer *b = buffers_add(ctx.buffers, buffer_create("timers")); + buffer_set_readonly(b, true); + struct window *new_window_a, *new_window_b; + window_split(ctx.active_window, &new_window_a, &new_window_b); + + const char *txt = + "TODO: this is not real values!\ntimer 1: 1ms\ntimer 2: 2ms\n"; + buffer_set_text(b, (uint8_t *)txt, strlen(txt)); + + window_set_buffer(new_window_b, b); + return 0; +} + +void register_global_commands(struct commands *commands, + void (*terminate_cb)()) { + + struct command global_commands[] = { + {.name = "find-file", .fn = find_file}, + {.name = "write-file", .fn = write_file}, + {.name = "run-command-interactive", .fn = run_interactive}, + {.name = "switch-buffer", .fn = switch_buffer}, + {.name = "abort", .fn = _abort}, + {.name = "find-next", .fn = find, .userdata = "forward"}, + {.name = "find-prev", .fn = find, .userdata = "backward"}, + {.name = "timers", .fn = timers}, + {.name = "exit", .fn = exit_editor, .userdata = terminate_cb}}; + + register_commands(commands, global_commands, + sizeof(global_commands) / sizeof(global_commands[0])); +} + +#define BUFFER_WRAPCMD_POS(fn) \ + static int32_t fn##_cmd(struct command_ctx ctx, int argc, \ + const char *argv[]) { \ + fn(window_buffer_view(ctx.active_window)); \ + return 0; \ + } + +#define BUFFER_WRAPCMD(fn) \ + static int32_t fn##_cmd(struct command_ctx ctx, int argc, \ + const char *argv[]) { \ + fn(window_buffer(ctx.active_window)); \ + return 0; \ + } + +BUFFER_WRAPCMD_POS(buffer_kill_line); +BUFFER_WRAPCMD_POS(buffer_forward_delete_char); +BUFFER_WRAPCMD_POS(buffer_backward_delete_char); +BUFFER_WRAPCMD_POS(buffer_backward_char); +BUFFER_WRAPCMD_POS(buffer_backward_word); +BUFFER_WRAPCMD_POS(buffer_forward_char); +BUFFER_WRAPCMD_POS(buffer_forward_word); +BUFFER_WRAPCMD_POS(buffer_backward_line); +BUFFER_WRAPCMD_POS(buffer_forward_line); +BUFFER_WRAPCMD_POS(buffer_end_of_line); +BUFFER_WRAPCMD_POS(buffer_beginning_of_line); +BUFFER_WRAPCMD_POS(buffer_newline); +BUFFER_WRAPCMD_POS(buffer_indent); +BUFFER_WRAPCMD(buffer_to_file); +BUFFER_WRAPCMD_POS(buffer_set_mark); +BUFFER_WRAPCMD_POS(buffer_clear_mark); +BUFFER_WRAPCMD_POS(buffer_copy); +BUFFER_WRAPCMD_POS(buffer_cut); +BUFFER_WRAPCMD_POS(buffer_paste); +BUFFER_WRAPCMD_POS(buffer_paste_older); +BUFFER_WRAPCMD_POS(buffer_goto_beginning); +BUFFER_WRAPCMD_POS(buffer_goto_end); +BUFFER_WRAPCMD_POS(buffer_undo); +static int32_t buffer_view_scroll_up_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + buffer_view_scroll_up(window_buffer_view(ctx.active_window), + window_height(ctx.active_window)); + return 0; +}; +static int32_t buffer_view_scroll_down_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + buffer_view_scroll_down(window_buffer_view(ctx.active_window), + window_height(ctx.active_window)); + return 0; +}; + +void register_buffer_commands(struct commands *commands) { + + static struct command buffer_commands[] = { + {.name = "kill-line", .fn = buffer_kill_line_cmd}, + {.name = "delete-char", .fn = buffer_forward_delete_char_cmd}, + {.name = "backward-delete-char", .fn = buffer_backward_delete_char_cmd}, + {.name = "backward-char", .fn = buffer_backward_char_cmd}, + {.name = "backward-word", .fn = buffer_backward_word_cmd}, + {.name = "forward-char", .fn = buffer_forward_char_cmd}, + {.name = "forward-word", .fn = buffer_forward_word_cmd}, + {.name = "backward-line", .fn = buffer_backward_line_cmd}, + {.name = "forward-line", .fn = buffer_forward_line_cmd}, + {.name = "end-of-line", .fn = buffer_end_of_line_cmd}, + {.name = "beginning-of-line", .fn = buffer_beginning_of_line_cmd}, + {.name = "newline", .fn = buffer_newline_cmd}, + {.name = "indent", .fn = buffer_indent_cmd}, + {.name = "buffer-write-to-file", .fn = buffer_to_file_cmd}, + {.name = "set-mark", .fn = buffer_set_mark_cmd}, + {.name = "clear-mark", .fn = buffer_clear_mark_cmd}, + {.name = "copy", .fn = buffer_copy_cmd}, + {.name = "cut", .fn = buffer_cut_cmd}, + {.name = "paste", .fn = buffer_paste_cmd}, + {.name = "paste-older", .fn = buffer_paste_older_cmd}, + {.name = "goto-beginning", .fn = buffer_goto_beginning_cmd}, + {.name = "goto-end", .fn = buffer_goto_end_cmd}, + {.name = "undo", .fn = buffer_undo_cmd}, + {.name = "scroll-down", .fn = buffer_view_scroll_down_cmd}, + {.name = "scroll-up", .fn = buffer_view_scroll_up_cmd}, + }; + + register_commands(commands, buffer_commands, + sizeof(buffer_commands) / sizeof(buffer_commands[0])); +} + +static int32_t window_close_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + window_close(ctx.active_window); + return 0; +} + +static int32_t window_split_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + struct window *resa, *resb; + window_split(ctx.active_window, &resa, &resb); + return 0; +} + +static int32_t window_hsplit_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + struct window *resa, *resb; + window_hsplit(ctx.active_window, &resa, &resb); + return 0; +} + +static int32_t window_vsplit_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + struct window *resa, *resb; + window_vsplit(ctx.active_window, &resa, &resb); + return 0; +} + +static int32_t window_close_others_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + window_close_others(ctx.active_window); + return 0; +} + +static int32_t window_focus_next_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + windows_focus_next(); + return 0; +} + +static int32_t window_focus_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + if (argc == 0) { + return minibuffer_prompt(ctx, "window id: "); + } + + if (argc == 1) { + uint32_t req_id = atoi(argv[0]); + windows_focus(req_id); + } + + return 0; +} + +static int32_t window_focus_n_cmd(struct command_ctx ctx, int argc, + const char *argv[]) { + char *window_id = (char *)ctx.userdata; + const char *argv_[] = {window_id}; + return window_focus_cmd(ctx, 1, argv_); +} + +void register_window_commands(struct commands *commands) { + static struct command window_commands[] = { + {.name = "window-close", .fn = window_close_cmd}, + {.name = "window-close-others", .fn = window_close_others_cmd}, + {.name = "window-split", .fn = window_split_cmd}, + {.name = "window-split-vertical", .fn = window_vsplit_cmd}, + {.name = "window-split-horizontal", .fn = window_hsplit_cmd}, + {.name = "window-focus-next", .fn = window_focus_next_cmd}, + {.name = "window-focus", .fn = window_focus_cmd}, + {.name = "window-focus-0", .fn = window_focus_n_cmd, .userdata = "0"}, + {.name = "window-focus-1", .fn = window_focus_n_cmd, .userdata = "1"}, + {.name = "window-focus-2", .fn = window_focus_n_cmd, .userdata = "2"}, + {.name = "window-focus-3", .fn = window_focus_n_cmd, .userdata = "3"}, + {.name = "window-focus-4", .fn = window_focus_n_cmd, .userdata = "4"}, + {.name = "window-focus-5", .fn = window_focus_n_cmd, .userdata = "5"}, + {.name = "window-focus-6", .fn = window_focus_n_cmd, .userdata = "6"}, + {.name = "window-focus-7", .fn = window_focus_n_cmd, .userdata = "7"}, + {.name = "window-focus-8", .fn = window_focus_n_cmd, .userdata = "8"}, + {.name = "window-focus-9", .fn = window_focus_n_cmd, .userdata = "9"}, + }; + + register_commands(commands, window_commands, + sizeof(window_commands) / sizeof(window_commands[0])); +} + +int32_t settings_set_cmd(struct command_ctx ctx, int argc, const char *argv[]) { + if (argc == 0) { + return minibuffer_prompt(ctx, "setting: "); + } else if (argc == 1) { + // validate setting here as well for a better experience + struct setting *setting = settings_get(argv[0]); + if (setting == NULL) { + minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); + return 1; + } + + command_ctx_push_arg(&ctx, argv[0]); + return minibuffer_prompt(ctx, "value: "); + } + + struct setting *setting = settings_get(argv[0]); + if (setting == NULL) { + minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); + return 1; + } else { + const char *value = argv[1]; + struct setting_value new_value = {.type = setting->value.type}; + switch (setting->value.type) { + case Setting_Bool: + new_value.bool_value = strncmp("true", value, 4) == 0 || + strncmp("yes", value, 3) == 0 || + strncmp("on", value, 2) == 0; + break; + case Setting_Number: + new_value.number_value = atol(value); + break; + case Setting_String: + new_value.string_value = (char *)value; + break; + } + + setting_set_value(setting, new_value); + } + + return 0; +} + +int32_t settings_get_cmd(struct command_ctx ctx, int argc, const char *argv[]) { + if (argc == 0) { + return minibuffer_prompt(ctx, "setting: "); + } + + struct setting *setting = settings_get(argv[0]); + if (setting == NULL) { + minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); + return 1; + } else { + char buf[128]; + setting_to_string(setting, buf, 128); + minibuffer_echo("%s = %s", argv[0], buf); + } + + return 0; +} + +void register_settings_commands(struct commands *commands) { + static struct command settings_commands[] = { + {.name = "set", .fn = settings_set_cmd}, + {.name = "get", .fn = settings_get_cmd}, + }; + + register_commands(commands, settings_commands, + sizeof(settings_commands) / sizeof(settings_commands[0])); +} diff --git a/src/main/cmds.h b/src/main/cmds.h new file mode 100644 index 0000000..a392e06 --- /dev/null +++ b/src/main/cmds.h @@ -0,0 +1,10 @@ +struct commands; + +void register_global_commands(struct commands *commands, + void (*terminate_cb)()); + +void register_buffer_commands(struct commands *commands); + +void register_window_commands(struct commands *commands); + +void register_settings_commands(struct commands *commands); diff --git a/src/main/main.c b/src/main/main.c new file mode 100644 index 0000000..f13e77e --- /dev/null +++ b/src/main/main.c @@ -0,0 +1,294 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dged/allocator.h" +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffers.h" +#include "dged/display.h" +#include "dged/lang.h" +#include "dged/minibuffer.h" +#include "dged/reactor.h" +#include "dged/settings.h" + +#include "bindings.h" +#include "cmds.h" + +struct frame_allocator frame_allocator; + +void *frame_alloc(size_t sz) { + return frame_allocator_alloc(&frame_allocator, sz); +} + +bool running = true; + +void terminate() { running = false; } + +static struct display *display = NULL; +static bool display_resized = false; +void resized() { + if (display != NULL) { + display_resize(display); + } + display_resized = true; + + signal(SIGWINCH, resized); +} + +uint64_t calc_frame_time_ns(struct timespec *timers, uint32_t num_timer_pairs) { + uint64_t total = 0; + for (uint32_t ti = 0; ti < num_timer_pairs * 2; ti += 2) { + struct timespec *start_timer = &timers[ti]; + struct timespec *end_timer = &timers[ti + 1]; + + total += + ((uint64_t)end_timer->tv_sec * 1e9 + (uint64_t)end_timer->tv_nsec) - + ((uint64_t)start_timer->tv_sec * 1e9 + (uint64_t)start_timer->tv_nsec); + } + + return total; +} + +#define DECLARE_TIMER(timer) struct timespec timer##_begin, timer##_end +#define TIMED_SCOPE_BEGIN(timer) clock_gettime(CLOCK_MONOTONIC, &timer##_begin) +#define TIMED_SCOPE_END(timer) clock_gettime(CLOCK_MONOTONIC, &timer##_end) + +void usage() { printf("TODO: print usage\n"); } + +int main(int argc, char *argv[]) { + + static struct option longopts[] = {{"line", required_argument, NULL, 'l'}, + {"end", no_argument, NULL, 'e'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0}}; + + char *filename = NULL; + uint32_t jumpline = 1; + bool goto_end = false; + char ch; + while ((ch = getopt_long(argc, argv, "hel:", longopts, NULL)) != -1) { + switch (ch) { + case 'l': + jumpline = atoi(optarg); + break; + case 'e': + goto_end = true; + break; + case 'h': + usage(); + return 0; + break; + default: + usage(); + return 1; + } + } + argc -= optind; + argv += optind; + + if (argc > 1) { + fprintf(stderr, "More than one file to open is not supported\n"); + return 2; + } else if (argc == 1) { + filename = strdup(argv[0]); + } + + setlocale(LC_ALL, ""); + + signal(SIGTERM, terminate); + + struct commands commands = command_registry_create(32); + + settings_init(64); + languages_init(true); + buffer_static_init(); + + frame_allocator = frame_allocator_create(16 * 1024 * 1024); + + struct reactor *reactor = reactor_create(); + + display = display_create(); + display_clear(display); + signal(SIGWINCH, resized); + + register_global_commands(&commands, terminate); + register_buffer_commands(&commands); + register_window_commands(&commands); + register_settings_commands(&commands); + + struct keyboard kbd = keyboard_create(reactor); + + struct keymap *current_keymap = NULL; + struct keymap *global_keymap = register_bindings(); + + struct buffers buflist = {0}; + buffers_init(&buflist, 32); + struct buffer initial_buffer = buffer_create("welcome"); + if (filename != NULL) { + buffer_destroy(&initial_buffer); + initial_buffer = buffer_from_file(filename); + } else { + const char *welcome_txt = "Welcome to the editor for datagubbar 👴\n"; + buffer_set_text(&initial_buffer, (uint8_t *)welcome_txt, + strlen(welcome_txt)); + } + + struct buffer *ib = buffers_add(&buflist, initial_buffer); + struct buffer minibuffer = buffer_create("minibuffer"); + minibuffer_init(&minibuffer); + reset_minibuffer_keys(&minibuffer); + + windows_init(display_height(display), display_width(display), ib, + &minibuffer); + struct window *active = windows_get_active(); + if (goto_end) { + buffer_goto_end(window_buffer_view(active)); + } else { + buffer_goto(window_buffer_view(active), jumpline > 0 ? jumpline - 1 : 0, 0); + } + + DECLARE_TIMER(buffer); + DECLARE_TIMER(display); + DECLARE_TIMER(keyboard); + + uint64_t frame_time = 0; + static char keyname[64] = {0}; + static uint32_t nkeychars = 0; + + while (running) { + + if (display_resized) { + windows_resize(display_height(display), display_width(display)); + display_resized = false; + } + + /* Update all windows together with the buffers in them. */ + TIMED_SCOPE_BEGIN(buffer); + windows_update(frame_alloc, frame_time); + TIMED_SCOPE_END(buffer); + + struct window *active_window = windows_get_active(); + if (minibuffer_focused()) { + active_window = minibuffer_window(); + } + + /* Update the screen by flushing command lists collected from updating the + * buffers. + */ + TIMED_SCOPE_BEGIN(display); + display_begin_render(display); + windows_render(display); + struct buffer_location cursor = + window_absolute_cursor_location(active_window); + display_move_cursor(display, cursor.line, cursor.col); + display_end_render(display); + TIMED_SCOPE_END(display); + + /* This blocks for events, so if nothing has happened we block here and let + * the CPU do something more useful than updating this narcissistic editor. + * This is also the reason that there is no timed scope around this, it + * simply makes no sense. + */ + reactor_update(reactor); + + TIMED_SCOPE_BEGIN(keyboard); + struct keyboard_update kbd_upd = + keyboard_update(&kbd, reactor, frame_alloc); + + uint32_t input_data_idx = 0; + for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) { + struct key *k = &kbd_upd.keys[ki]; + + struct lookup_result res = {.found = false}; + if (current_keymap != NULL) { + res = lookup_key(current_keymap, 1, k, &commands); + } else { + // check the global keymap first, then the buffer one + res = lookup_key(global_keymap, 1, k, &commands); + if (!res.found) { + res = lookup_key(buffer_keymap(window_buffer(active_window)), 1, k, + &commands); + } + } + + if (res.found) { + switch (res.type) { + case BindingType_Command: { + if (res.command == NULL) { + minibuffer_echo_timeout( + 4, "binding found for key %s but not command", k); + } else { + int32_t ec = execute_command(res.command, &commands, active_window, + &buflist, 0, NULL); + if (ec != 0 && !minibuffer_displaying()) { + minibuffer_echo_timeout(4, "command %s failed with exit code %d", + res.command->name, ec); + } + } + current_keymap = NULL; + nkeychars = 0; + keyname[0] = '\0'; + break; + } + case BindingType_Keymap: { + if (nkeychars > 0 && nkeychars < 64) { + keyname[nkeychars] = '-'; + ++nkeychars; + } + + if (nkeychars < 64) { + nkeychars += key_name(k, keyname + nkeychars, 64 - nkeychars); + minibuffer_echo("%s", keyname); + } + + current_keymap = res.keymap; + break; + } + } + } else if (k->mod == 0) { + buffer_add_text(window_buffer_view(active_window), + &kbd_upd.raw[k->start], k->end - k->start); + } else { + char keyname[16]; + key_name(k, keyname, 16); + if (current_keymap == NULL) { + minibuffer_echo_timeout(4, "key \"%s\" is not bound!", keyname); + } else { + minibuffer_echo_timeout(4, "key \"%s %s\" is not bound!", + current_keymap->name, keyname); + } + current_keymap = NULL; + nkeychars = 0; + keyname[0] = '\0'; + } + } + TIMED_SCOPE_END(keyboard); + + // calculate frame time + struct timespec timers[] = {buffer_begin, buffer_end, display_begin, + display_end, keyboard_begin, keyboard_end}; + frame_time = calc_frame_time_ns(timers, 3); + frame_allocator_clear(&frame_allocator); + } + + windows_destroy(); + minibuffer_destroy(); + buffer_destroy(&minibuffer); + buffers_destroy(&buflist); + display_clear(display); + display_destroy(display); + destroy_keymaps(); + command_registry_destroy(&commands); + reactor_destroy(reactor); + frame_allocator_destroy(&frame_allocator); + buffer_static_teardown(); + settings_destroy(); + + return 0; +} diff --git a/src/minibuffer.c b/src/minibuffer.c deleted file mode 100644 index 3fa311c..0000000 --- a/src/minibuffer.c +++ /dev/null @@ -1,184 +0,0 @@ -#include "minibuffer.h" -#include "binding.h" -#include "buffer.h" -#include "command.h" -#include "display.h" - -#include -#include -#include -#include - -static struct minibuffer { - struct buffer *buffer; - struct timespec expires; - - char prompt[128]; - struct command_ctx prompt_command_ctx; - bool prompt_active; - - struct keymap keymap; -} 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 execute(struct command_ctx ctx, int argc, const char *argv[]) { - if (g_minibuffer.prompt_active) { - struct command_ctx *c = &g_minibuffer.prompt_command_ctx; - - struct text_chunk line = buffer_get_line(g_minibuffer.buffer, 0); - 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 command execute_minibuffer_command = { - .fn = execute, - .name = "minibuffer-execute", - .userdata = NULL, -}; - -struct update_hook_result update(struct buffer *buffer, - 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) { - buffer_clear(buffer); - } - - struct update_hook_result res = {0}; - if (mb->prompt_active) { - res.margins.left = strlen(mb->prompt); - draw_prompt(commands, NULL); - } - - return res; -} - -void minibuffer_init(struct buffer *buffer) { - if (g_minibuffer.buffer != NULL) { - return; - } - - g_minibuffer.buffer = buffer; - g_minibuffer.keymap = keymap_create("minibuffer", 10); - - struct binding bindings[] = { - ANONYMOUS_BINDING(Ctrl, 'M', &execute_minibuffer_command), - }; - keymap_bind_keys(&g_minibuffer.keymap, bindings, - sizeof(bindings) / sizeof(bindings[0])); - buffer_add_keymap(g_minibuffer.buffer, &g_minibuffer.keymap); - - 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_clear(g_minibuffer.buffer); - buffer_add_text(g_minibuffer.buffer, (uint8_t *)buff, - nbytes > 2048 ? 2048 : nbytes); - - clock_gettime(CLOCK_MONOTONIC, &g_minibuffer.expires); - g_minibuffer.expires.tv_sec += timeout; -} - -void minibuffer_destroy() { - command_ctx_free(&g_minibuffer.prompt_command_ctx); -} - -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); -} - -int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, - ...) { - 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; - - va_list args; - va_start(args, fmt); - vsnprintf(g_minibuffer.prompt, sizeof(g_minibuffer.prompt), fmt, args); - va_end(args); - - return 0; -} - -void minibuffer_abort_prompt() { - minibuffer_clear(); - g_minibuffer.prompt_active = false; -} - -bool minibuffer_displaying() { - return g_minibuffer.buffer != NULL && !buffer_is_empty(g_minibuffer.buffer); -} - -void minibuffer_clear() { - if (g_minibuffer.buffer != NULL) { - buffer_clear(g_minibuffer.buffer); - } -} - -bool minibuffer_focused() { return g_minibuffer.prompt_active; } diff --git a/src/minibuffer.h b/src/minibuffer.h deleted file mode 100644 index 6845b07..0000000 --- a/src/minibuffer.h +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include -#include -#include - -struct buffer; -struct command_ctx; - -/** - * 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(); - -/** - * 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 a return code suitable to return from a command to signal more input - * is needed. - */ -int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...); - -/** - * 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(); - -/** - * 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/reactor-epoll.c b/src/reactor-epoll.c deleted file mode 100644 index e488fef..0000000 --- a/src/reactor-epoll.c +++ /dev/null @@ -1,76 +0,0 @@ -#include "reactor.h" - -#include -#include -#include - -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/reactor.h b/src/reactor.h deleted file mode 100644 index e54afda..0000000 --- a/src/reactor.h +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include - -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/settings.c b/src/settings.c deleted file mode 100644 index 7d3fcf1..0000000 --- a/src/settings.c +++ /dev/null @@ -1,165 +0,0 @@ -#include "settings.h" -#include "command.h" -#include "hash.h" -#include "hashmap.h" -#include "minibuffer.h" -#include "vec.h" - -#include -#include -#include - -static struct settings g_settings = {0}; - -int32_t settings_get_cmd(struct command_ctx ctx, int argc, const char *argv[]); -int32_t settings_set_cmd(struct command_ctx ctx, int argc, const char *argv[]); - -void settings_init(uint32_t initial_capacity, struct commands *commands) { - HASHMAP_INIT(&g_settings.settings, initial_capacity, hash_name); - static struct command settings_commands[] = { - {.name = "set", .fn = settings_set_cmd}, - {.name = "get", .fn = settings_get_cmd}, - }; - - register_commands(commands, settings_commands, - sizeof(settings_commands) / sizeof(settings_commands[0])); -} - -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; - } -} - -int32_t settings_get_cmd(struct command_ctx ctx, int argc, const char *argv[]) { - if (argc == 0) { - return minibuffer_prompt(ctx, "setting: "); - } - - struct setting *setting = settings_get(argv[0]); - if (setting == NULL) { - minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); - return 1; - } else { - char buf[128]; - setting_to_string(setting, buf, 128); - minibuffer_echo("%s = %s", argv[0], buf); - } - - return 0; -} - -int32_t settings_set_cmd(struct command_ctx ctx, int argc, const char *argv[]) { - if (argc == 0) { - return minibuffer_prompt(ctx, "setting: "); - } else if (argc == 1) { - // validate setting here as well for a better experience - struct setting *setting = settings_get(argv[0]); - if (setting == NULL) { - minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); - return 1; - } - - command_ctx_push_arg(&ctx, argv[0]); - return minibuffer_prompt(ctx, "value: "); - } - - struct setting *setting = settings_get(argv[0]); - if (setting == NULL) { - minibuffer_echo_timeout(4, "no such setting \"%s\"", argv[0]); - return 1; - } else { - const char *value = argv[1]; - struct setting_value new_value = {.type = setting->value.type}; - switch (setting->value.type) { - case Setting_Bool: - new_value.bool_value = strncmp("true", value, 4) == 0 || - strncmp("yes", value, 3) == 0 || - strncmp("on", value, 2) == 0; - break; - case Setting_Number: - new_value.number_value = atol(value); - break; - case Setting_String: - new_value.string_value = (char *)value; - break; - } - - setting_set_value(setting, new_value); - } - - return 0; -} diff --git a/src/settings.h b/src/settings.h deleted file mode 100644 index 20cca24..0000000 --- a/src/settings.h +++ /dev/null @@ -1,119 +0,0 @@ -#include "hashmap.h" - -#include -#include - -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, struct commands *commands); - -/** - * Destroy the global collection of settings. - */ -void settings_destroy(); - -/** - * Register a new setting. - * - * @param path The path of the new setting on - * the form ... - * @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 ... - * @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 ... - * @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); diff --git a/src/text.c b/src/text.c deleted file mode 100644 index cdbb796..0000000 --- a/src/text.c +++ /dev/null @@ -1,492 +0,0 @@ -#include "text.h" - -#include -#include -#include -#include - -#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) { - - 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->nchars - end_col); - 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/text.h b/src/text.h deleted file mode 100644 index fbee89b..0000000 --- a/src/text.h +++ /dev/null @@ -1,54 +0,0 @@ -#include -#include -#include - -// 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/undo.c b/src/undo.c deleted file mode 100644 index 2780557..0000000 --- a/src/undo.c +++ /dev/null @@ -1,181 +0,0 @@ -#include "undo.h" -#include "string.h" -#include "vec.h" - -#include -#include - -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) { - - VEC_APPEND(&undo->records, struct undo_record * rec); - rec->type = Undo_Boundary; - rec->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; - } - } - } - - 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/undo.h b/src/undo.h deleted file mode 100644 index 1ce3a8a..0000000 --- a/src/undo.h +++ /dev/null @@ -1,66 +0,0 @@ -#include "vec.h" -#include -#include - -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/utf8.c b/src/utf8.c deleted file mode 100644 index abf5ef7..0000000 --- a/src/utf8.c +++ /dev/null @@ -1,67 +0,0 @@ -#include "utf8.h" - -#include - -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/utf8.h b/src/utf8.h deleted file mode 100644 index 59a959e..0000000 --- a/src/utf8.h +++ /dev/null @@ -1,17 +0,0 @@ -#include -#include - -/*! - * \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/vec.h b/src/vec.h deleted file mode 100644 index 2d5bd32..0000000 --- a/src/vec.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef _VEC_H -#define _VEC_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/window.c b/src/window.c deleted file mode 100644 index 10ded5e..0000000 --- a/src/window.c +++ /dev/null @@ -1,14 +0,0 @@ -#include "buffer.h" -#include "display.h" - -void window_update_buffer(struct window *window, struct command_list *commands, - uint64_t frame_time, uint32_t *relline, - uint32_t *relcol) { - buffer_update(window->buffer, window->width, window->height, commands, - frame_time, relline, relcol); -} - -void window_set_buffer(struct window *window, struct buffer *buffer) { - window->prev_buffer = window->buffer; - window->buffer = buffer; -} diff --git a/src/window.h b/src/window.h deleted file mode 100644 index 14e041e..0000000 --- a/src/window.h +++ /dev/null @@ -1,18 +0,0 @@ -#include - -struct command_list; - -struct window { - uint32_t x; - uint32_t y; - uint32_t width; - uint32_t height; - struct buffer *buffer; - struct buffer *prev_buffer; -}; - -void window_update_buffer(struct window *window, struct command_list *commands, - uint64_t frame_time, uint32_t *relline, - uint32_t *relcol); - -void window_set_buffer(struct window *window, struct buffer *buffer); diff --git a/test/allocator.c b/test/allocator.c index ca1a7a0..fe01334 100644 --- a/test/allocator.c +++ b/test/allocator.c @@ -1,8 +1,8 @@ +#include "dged/allocator.h" + #include "assert.h" #include "test.h" -#include "allocator.h" - void test_frame_allocator() { struct frame_allocator fa = frame_allocator_create(128); diff --git a/test/buffer.c b/test/buffer.c index 38ce468..a4ac754 100644 --- a/test/buffer.c +++ b/test/buffer.c @@ -1,45 +1,46 @@ -#include "assert.h" -#include "test.h" +#include -#include "buffer.h" +#include "dged/buffer.h" -#include +#include "assert.h" +#include "test.h" void test_move() { - struct buffer b = buffer_create("test-buffer", false); - ASSERT(b.dot.col == 0 && b.dot.line == 0, + struct buffer b = buffer_create("test-buffer"); + struct buffer_view v = buffer_view_create(&b, false, false); + ASSERT(v.dot.col == 0 && v.dot.line == 0, "Expected dot to be at buffer start"); // make sure we cannot move now - buffer_backward_char(&b); - buffer_backward_line(&b); - ASSERT(b.dot.col == 0 && b.dot.line == 0, + buffer_backward_char(&v); + buffer_backward_line(&v); + ASSERT(v.dot.col == 0 && v.dot.line == 0, "Expected to not be able to move backward in empty buffer"); - buffer_forward_char(&b); - buffer_forward_line(&b); - ASSERT(b.dot.col == 0 && b.dot.line == 0, + buffer_forward_char(&v); + buffer_forward_line(&v); + ASSERT(v.dot.col == 0 && v.dot.line == 0, "Expected to not be able to move forward in empty buffer"); // add some text and try again const char *txt = "testing movement"; - int lineindex = buffer_add_text(&b, (uint8_t *)txt, strlen(txt)); + int lineindex = buffer_add_text(&v, (uint8_t *)txt, strlen(txt)); ASSERT(lineindex + 1 == 1, "Expected buffer to have one line"); - buffer_beginning_of_line(&b); - buffer_forward_char(&b); - ASSERT(b.dot.col == 1 && b.dot.line == 0, + buffer_beginning_of_line(&v); + buffer_forward_char(&v); + ASSERT(v.dot.col == 1 && v.dot.line == 0, "Expected to be able to move forward by one char"); // now we have two lines const char *txt2 = "\n"; - int lineindex2 = buffer_add_text(&b, (uint8_t *)txt2, strlen(txt2)); + int lineindex2 = buffer_add_text(&v, (uint8_t *)txt2, strlen(txt2)); ASSERT(lineindex2 + 1 == 2, "Expected buffer to have two lines"); - buffer_backward_line(&b); - buffer_beginning_of_line(&b); - buffer_backward_char(&b); + buffer_backward_line(&v); + buffer_beginning_of_line(&v); + buffer_backward_char(&v); ASSERT( - b.dot.col == 0 && b.dot.line == 0, + v.dot.col == 0 && v.dot.line == 0, "Expected to not be able to move backwards when at beginning of buffer"); buffer_destroy(&b); diff --git a/test/command.c b/test/command.c index e09cf35..8db02e0 100644 --- a/test/command.c +++ b/test/command.c @@ -1,9 +1,9 @@ #include "assert.h" #include "test.h" -#include "command.h" -#include "hash.h" -#include "hashmap.h" +#include "dged/command.h" +#include "dged/hash.h" +#include "dged/hashmap.h" void test_command_registry_create() { struct commands cmds = command_registry_create(10); diff --git a/test/container.c b/test/container.c new file mode 100644 index 0000000..8be7fc9 --- /dev/null +++ b/test/container.c @@ -0,0 +1,102 @@ +#include + +#include "dged/btree.h" + +#include "assert.h" +#include "test.h" + +void test_empty_bintree() { + BINTREE_ENTRY_TYPE(node, int); + BINTREE(node) tree; + + BINTREE_INIT(&tree); + + ASSERT(BINTREE_ROOT(&tree) == NULL, + "Expected root of tree to be NULL initially"); + struct node *res = BINTREE_ROOT(&tree); + BINTREE_NEXT(res); + ASSERT(res == NULL, "Expected there to be no \"next\" initially"); + struct node *n = BINTREE_ROOT(&tree); + BINTREE_FIRST(n); + bool loop_empty = true; + while (n != NULL) { + loop_empty = false; + BINTREE_NEXT(n); + } + + ASSERT(loop_empty, "Expected an empty tree to yield an empty loop"); + + BINTREE_FREE_NODES(BINTREE_ROOT(&tree), node); +} + +void test_bintree_iter() { + BINTREE_ENTRY_TYPE(node, char); + BINTREE(node) tree; + BINTREE_INIT(&tree); + + // insert at the root + BINTREE_SET_ROOT(&tree, 'a'); + + struct node *root = BINTREE_ROOT(&tree); + ASSERT(BINTREE_VALUE(root) == 'a', "Expected root to have its value"); + + // insert first child (left) + BINTREE_INSERT(root, 'b'); + ASSERT(BINTREE_VALUE(root->left) == 'b', + "Expected first child to have its value"); + + // insert second child + BINTREE_INSERT(root, 'c'); + ASSERT(BINTREE_VALUE(root->right) == 'c', + "Expected second child to have its value"); + + // insert first child of second child + struct node *right = BINTREE_RIGHT(root); + BINTREE_INSERT(right, 'd'); + + // iterate + char chars[4] = {0}; + uint32_t nchars = 0; + struct node *n = BINTREE_ROOT(&tree); + BINTREE_FIRST(n); + while (n != NULL) { + chars[nchars] = BINTREE_VALUE(n); + ++nchars; + BINTREE_NEXT(n); + } + + ASSERT(nchars == 4, "Expected tree to have 4 nodes after insertions"); + ASSERT(chars[0] == 'b' && chars[1] == 'a' && chars[2] == 'd' && + chars[3] == 'c', + "Expected tree to have been traversed correctly"); + + // find + struct node *res = NULL; + BINTREE_FIND(&tree, 'b', res); + ASSERT(res != NULL && res == BINTREE_LEFT(root), + "Expected existing value to be found"); + ASSERT(BINTREE_VALUE(res) == 'b', + "Expected found node to contain the searched for value"); + + // remove + struct node *to_remove = BINTREE_LEFT(right); + BINTREE_REMOVE(to_remove); + + nchars = 0; + n = BINTREE_ROOT(&tree); + BINTREE_FIRST(n); + while (n != NULL) { + ++nchars; + BINTREE_NEXT(n); + } + + ASSERT(nchars == 3, "Expected three nodes to remain after removing one"); + BINTREE_FREE_NODES(to_remove, node); + + BINTREE_FREE_NODES(root, node); +} + +void run_container_tests() { + run_test(test_empty_bintree); + run_test(test_bintree_iter); +} diff --git a/test/fake-reactor.h b/test/fake-reactor.h index 04d8306..33d1fbc 100644 --- a/test/fake-reactor.h +++ b/test/fake-reactor.h @@ -1,7 +1,8 @@ -#include "reactor.h" #include #include +#include "dged/reactor.h" + struct fake_reactor_impl { bool (*poll_event)(void *userdata, uint32_t ev_id); uint32_t (*register_interest)(void *userdata, int fd, enum interest interest); diff --git a/test/keyboard.c b/test/keyboard.c index a76d2a7..1ddbba5 100644 --- a/test/keyboard.c +++ b/test/keyboard.c @@ -1,12 +1,13 @@ +#include +#include +#include + +#include "dged/keyboard.h" + #include "assert.h" #include "fake-reactor.h" #include "test.h" -#include "keyboard.h" -#include "unistd.h" -#include -#include - struct call_count { uint32_t poll; uint32_t reg; diff --git a/test/main.c b/test/main.c index 01b2645..4c241b3 100644 --- a/test/main.c +++ b/test/main.c @@ -4,8 +4,6 @@ #include #include -#include "lang.h" -#include "settings.h" #include "test.h" void handle_abort() { exit(1); } @@ -44,6 +42,9 @@ int main() { printf("\n 📓 \x1b[1;36mRunning settings tests...\x1b[0m\n"); run_settings_tests(); + printf("\n 🎁 \x1b[1;36mRunning container tests...\x1b[0m\n"); + run_container_tests(); + struct timespec elapsed; clock_gettime(CLOCK_MONOTONIC, &elapsed); uint64_t elapsed_nanos = diff --git a/test/minibuffer.c b/test/minibuffer.c index 4e2b7f1..a6c7f96 100644 --- a/test/minibuffer.c +++ b/test/minibuffer.c @@ -2,12 +2,12 @@ #include "stdlib.h" #include "test.h" -#include "allocator.h" -#include "buffer.h" -#include "command.h" -#include "display.h" -#include "minibuffer.h" -#include "settings.h" +#include "dged/allocator.h" +#include "dged/buffer.h" +#include "dged/command.h" +#include "dged/display.h" +#include "dged/minibuffer.h" +#include "dged/settings.h" static struct buffer b = {0}; @@ -17,9 +17,8 @@ void *alloc_fn(size_t sz) { return frame_allocator_alloc(g_alloc, sz); } void init() { if (b.name == NULL) { - struct commands commands = command_registry_create(10); - settings_init(10, &commands); - b = buffer_create("minibuffer", false); + settings_init(10); + b = buffer_create("minibuffer"); } minibuffer_init(&b); @@ -34,6 +33,7 @@ void destroy() { void test_minibuffer_echo() { uint32_t relline, relcol; + struct buffer_view view = buffer_view_create(&b, false, false); // TODO: how to clear this? struct frame_allocator alloc = frame_allocator_create(1024); @@ -50,11 +50,13 @@ void test_minibuffer_echo() { ASSERT(minibuffer_displaying(), "Minibuffer should now have text to display"); minibuffer_clear(); + buffer_update(&view, 100, 1, list, 0, &relline, &relcol); ASSERT(!minibuffer_displaying(), "Minibuffer should have nothing to display after clearing"); minibuffer_echo_timeout(0, "You will not see me"); - buffer_update(&b, 100, 1, list, 0, &relline, &relcol); + + buffer_update(&view, 100, 1, list, 0, &relline, &relcol); ASSERT(!minibuffer_displaying(), "A zero timeout echo should be cleared after first update"); diff --git a/test/settings.c b/test/settings.c index dc448a5..b1fdc9a 100644 --- a/test/settings.c +++ b/test/settings.c @@ -1,14 +1,12 @@ -#include "assert.h" -#include "test.h" +#include -#include "command.h" -#include "settings.h" +#include "dged/settings.h" -#include +#include "assert.h" +#include "test.h" void test_get() { - struct commands commands = command_registry_create(10); - settings_init(10, &commands); + settings_init(10); settings_register_setting( "my.setting", (struct setting_value){.type = Setting_Bool, .bool_value = false}); @@ -40,8 +38,7 @@ void test_get() { } void test_set() { - struct commands commands = command_registry_create(10); - settings_init(10, &commands); + settings_init(10); settings_register_setting( "my.setting", (struct setting_value){.type = Setting_Bool, .bool_value = false}); diff --git a/test/test.h b/test/test.h index 047bbfb..b01fde4 100644 --- a/test/test.h +++ b/test/test.h @@ -18,5 +18,6 @@ void run_keyboard_tests(); void run_allocator_tests(); void run_minibuffer_tests(); void run_settings_tests(); +void run_container_tests(); #endif diff --git a/test/text.c b/test/text.c index cb18df5..3092dcc 100644 --- a/test/text.c +++ b/test/text.c @@ -1,13 +1,13 @@ -#include "assert.h" -#include "stdio.h" -#include "test.h" - -#include "text.h" - #include #include #include +#include "dged/text.h" + +#include "assert.h" +#include "stdio.h" +#include "test.h" + void assert_line_eq(struct text_chunk line, const char *txt, const char *msg) { ASSERT(strncmp((const char *)line.text, txt, line.nbytes) == 0, msg); } diff --git a/test/undo.c b/test/undo.c index d0e2fdb..a4b6ad0 100644 --- a/test/undo.c +++ b/test/undo.c @@ -1,9 +1,10 @@ +#include + +#include "dged/undo.h" + #include "assert.h" #include "test.h" -#include "undo.h" -#include - void test_undo_insert() { struct undo_stack undo; diff --git a/test/utf8.c b/test/utf8.c index 88e6a8c..d67c409 100644 --- a/test/utf8.c +++ b/test/utf8.c @@ -1,9 +1,10 @@ -#include "utf8.h" +#include +#include + +#include "dged/utf8.h" + #include "assert.h" #include "test.h" -#include "wchar.h" - -#include void test_nchars_nbytes() { ASSERT(utf8_nchars((uint8_t *)"👴", strlen("👴")) == 1, -- cgit v1.2.3