diff options
84 files changed, 8515 insertions, 1232 deletions
@@ -22,15 +22,19 @@ HEADERS = src/dged/settings.h src/dged/minibuffer.h src/dged/keyboard.h src/dged src/dged/vec.h src/dged/window.h src/dged/hash.h src/dged/undo.h src/dged/lang.h \ src/dged/settings-parse.h src/dged/utf8.h src/main/cmds.h src/main/bindings.h \ src/main/search-replace.h src/dged/location.h src/dged/buffer_view.h src/main/completion.h \ - src/dged/timers.h src/dged/s8.h src/main/version.h src/config.h src/dged/process.h + src/dged/timers.h src/dged/s8.h src/main/version.h src/config.h src/dged/process.h src/dged/bufread.h \ + src/dged/hook.h src/main/frame-hooks.h src/main/completion/buffer.h src/main/completion/command.h \ + src/main/completion/path.h 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 src/dged/settings-parse.c src/dged/location.c \ - src/dged/buffer_view.c src/dged/timers.c src/dged/s8.c src/dged/path.c src/dged/hash.c + src/dged/buffer_view.c src/dged/timers.c src/dged/s8.c src/dged/path.c src/dged/hash.c src/dged/bufread.c -MAIN_SOURCES = src/main/main.c src/main/cmds.c src/main/bindings.c src/main/search-replace.c src/main/completion.c +MAIN_SOURCES = src/main/main.c src/main/cmds.c src/main/bindings.c src/main/search-replace.c src/main/completion.c \ + src/main/frame-hooks.c src/main/completion/buffer.c src/main/completion/command.c \ + src/main/completion/path.c # HACK: added to MAIN_SOURCES to not be picked up in tests # since they have their own implementation @@ -47,7 +51,7 @@ MAIN_SOURCES = src/main/main.c src/main/cmds.c src/main/bindings.c src/main/sear 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/container.c \ - test/buflist.c + test/buflist.c test/bufread.c prefix ?= /usr/local DESTDIR ?= $(prefix) @@ -57,11 +61,11 @@ datadir = share/dged .SUFFIXES: .c .o .d CFLAGS ?= -g -O2 - CFLAGS += -Werror -Wall -Wextra -std=c99\ -I $(.CURDIR)/src\ -I $(.CURDIR)/src/main\ - -DDATADIR="$(prefix)/$(datadir)" + -DDATADIR="$(prefix)/$(datadir)"\ + -DTEST_ROOT="$(.CURDIR)/test" ASAN ?= false @@ -82,9 +86,14 @@ ASAN ?= false .endif .if $(LSP_ENABLE) == true - HEADERS += src/dged/lsp.h src/main/lsp.h src/dged/json.h - SOURCES += src/dged/lsp.c src/dged/json.c - MAIN_SOURCES += src/main/lsp.c + HEADERS += src/dged/lsp.h src/main/lsp.h src/dged/json.h src/dged/jsonrpc.h src/main/lsp/types.h \ + src/main/lsp/choice-buffer.h src/main/lsp/actions.h src/main/lsp/diagnostics.h \ + src/main/lsp/goto.h src/main/lsp/format.h src/main/lsp/completion.h src/main/lsp/rename.h \ + src/main/lsp/help.h src/main/lsp/references.h + SOURCES += src/dged/lsp.c src/dged/json.c src/dged/jsonrpc.c + MAIN_SOURCES += src/main/lsp.c src/main/lsp/types.c src/main/lsp/choice-buffer.c src/main/lsp/actions.c \ + src/main/lsp/diagnostics.c src/main/lsp/goto.c src/main/lsp/format.c src/main/lsp/completion.c \ + src/main/lsp/rename.c src/main/lsp/help.c src/main/lsp/references.c TEST_SOURCES += test/json.c CFLAGS += -DLSP_ENABLED .endif @@ -21,7 +21,7 @@ expected pre-1.0.0. - [x] Terminal only - [x] Mouse-free editing - [x] Naive and incorrect unicode handling -- [ ] LSP Client implementation (in progress) +- [x] LSP Client implementation ## Contributing @@ -4,14 +4,14 @@ _usage="./configure -- configure the DGED build. Options: --[enable|disable]-syntax Enable or disable syntax highlighting support. Default: enabled. - --[enable|disable]-lsp Enable or disable Language Server Protocol support. Default: disabled (experimental). + --[enable|disable]-lsp Enable or disable Language Server Protocol support. Default: enabled. --enable-asan Build DGED with address sanitizer enabled. Default: disabled. --prefix=<PREFIX> Set the build prefix path to <PREFIX>. Default: /usr/local. -h/--help Show this help text. " enable_syntax=1 -enable_lsp=0 +enable_lsp=1 enable_asan=0 prefix= while [ "$#" -gt 0 ]; do @@ -244,10 +244,95 @@ more languages, add something like .Bd -literal [languages.mylang] name = "My Language" # a descriptive name +pattern "^.*\.mylang$" # a regex pattern to identify files for this language grammar = "mylang" # name of the treesitter grammar to use (if syntax is enabled) .Ed to the configuration file. +.Sh LANGUAGE SERVER PROTOCOL +When +.Nm +is built with language server protocol support (short: LSP, enabled by default), +it acts as a language server protocol client. To configure the language server +for a language (some common languages are already configured out of the box, +look for a message in *messages* about needing to configure the +language server). Configuration is done in +.Pa dged.toml +and looks like +.Bd -literal +[languages.mylang] +name = "My Language" +pattern "^.*\.mylang$" # see above +grammar = "mylang" # see above + +[languages.mylang.language-server] +command = "mylang-lsp" # path/name of language server process to start +language-server.format-on-save = true # true to format the language when saving +.Ed + +format-on-save can also be set globally with +.Bd -literal +[editor] +format-on-save = true +.Ed + +.Ss COMMANDS +A set of commands are also associated with the language server + +.Bl -tag -width xxxx +.It lsp-goto-definition +Go to the definition of the symbol under the cursor. +.It lsp-goto-declaration +Go to the declaration of the symbol under the cursor. +.It lsp-goto-implementation +Go to the implementation of the symbol under the cursor. +.It lsp-goto +Interactively go to the definition, declaration or implementation +of the symbol under the cursor. +.It lsp-goto-previous +Go back to where the cursor was before jumping with above commands. +.It lsp-references +Find all references for the symbol under the cursor. +.It lsp-restart +Restart the LSP server process. +.It lsp-diagnostics +Open a list of the diagnostics for the current buffer. +.It lsp-next-diagnostic +Goto the next LSP diagnostic in the file. +.It lsp-prev-diagnostic +Goto the previous LSP diagnostic in the file. +.It lsp-code-actions +Get a list of and apply code actions for the symbol under the cursor. +.It lsp-format +Format the buffer or region using the LSP. +.It lsp-rename +Rename the symbol under the cursor. Note that this may modify more +than the open buffer. +.It lsp-help +Display help text for the symbol under the cursor. +.El + +.Ss BINDINGS +For some of the above commands there are also key bindings: + +.Bl -tag -width xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -compact +.It M-. +lsp-goto-definition +.It M-/ +lsp-goto +.It M-[ +lsp-prev-diagnostic +.It M-] +lsp-next-diagnostic +.It M-a +lsp-code-actions +.It M-= +lsp-format +.It M-r +lsp-rename +.It M-h +lsp-help + .Sh FILES .Bl -tag -width XX .It Pa ~/.config/dged/dged.toml @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,16 +20,16 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725930920, - "narHash": "sha256-RVhD9hnlTT2nJzPHlAqrWqCkA7T6CYrP41IoVRkciZM=", + "lastModified": 1744168086, + "narHash": "sha256-S9M4HddBCxbbX1CKSyDYgZ8NCVyHcbKnBfoUXeRu2jQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658", + "rev": "60e405b241edb6f0573f3d9f944617fe33ac4a73", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-24.05", + "ref": "nixos-24.11", "type": "indirect" } }, @@ -1,7 +1,7 @@ { description = "An editor for datagubbar"; - inputs.nixpkgs.url = "nixpkgs/nixos-24.05"; + inputs.nixpkgs.url = "nixpkgs/nixos-24.11"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: diff --git a/src/dged/allocator.c b/src/dged/allocator.c index 308b97c..a1f8cfb 100644 --- a/src/dged/allocator.c +++ b/src/dged/allocator.c @@ -1,5 +1,7 @@ #include "allocator.h" +#include <stdlib.h> + struct frame_allocator frame_allocator_create(size_t capacity) { return (struct frame_allocator){ .capacity = capacity, .offset = 0, .buf = (uint8_t *)malloc(capacity)}; diff --git a/src/dged/allocator.h b/src/dged/allocator.h index 49e3aec..16ab796 100644 --- a/src/dged/allocator.h +++ b/src/dged/allocator.h @@ -1,6 +1,5 @@ #include <stddef.h> #include <stdint.h> -#include <stdlib.h> /** * Simple bump allocator that can be used for diff --git a/src/dged/binding.h b/src/dged/binding.h index 93de02d..6f8719a 100644 --- a/src/dged/binding.h +++ b/src/dged/binding.h @@ -69,7 +69,8 @@ enum binding_type { #define PREFIX(...) PREFIX_INNER(__VA_ARGS__) /** - * Define an anonymous binding, i.e. a binding directly to a function. + * Define an anonymous binding, i.e. a binding directly to a non- + * registered command that has no name. * * Note the function that this key binds to cannot usually be * executed dynamically (with M-x). diff --git a/src/dged/buffer.c b/src/dged/buffer.c index b833a78..dcaa42c 100644 --- a/src/dged/buffer.c +++ b/src/dged/buffer.c @@ -36,62 +36,16 @@ static struct kill_ring { .paste_idx = 0, .paste_up_to_date = false}; -#define DECLARE_HOOK(name, callback_type, vec_type) \ - struct name##_hook { \ - uint32_t id; \ - callback_type callback; \ - void *userdata; \ - }; \ - \ - static uint32_t insert_##name##_hook( \ - vec_type *hooks, uint32_t *id, callback_type callback, void *userdata) { \ - uint32_t iid = ++(*id); \ - struct name##_hook hook = (struct name##_hook){ \ - .id = iid, \ - .callback = callback, \ - .userdata = userdata, \ - }; \ - VEC_PUSH(hooks, hook); \ - \ - return iid; \ - } \ - \ - static void remove_##name##_hook(vec_type *hooks, uint32_t id, \ - remove_hook_cb callback) { \ - uint64_t found_at = (uint64_t)-1; \ - VEC_FOR_EACH_INDEXED(hooks, struct name##_hook *h, idx) { \ - if (h->id == id) { \ - if (callback != NULL) { \ - callback(h->userdata); \ - } \ - found_at = idx; \ - break; \ - } \ - } \ - if (found_at != (uint64_t)-1) { \ - if (found_at < VEC_SIZE(hooks) - 1) { \ - VEC_SWAP(hooks, found_at, VEC_SIZE(hooks) - 1); \ - } \ - VEC_POP(hooks, struct name##_hook removed); \ - (void)removed; \ - } \ - } - -typedef VEC(struct create_hook) create_hook_vec; -typedef VEC(struct destroy_hook) destroy_hook_vec; -typedef VEC(struct insert_hook) insert_hook_vec; -typedef VEC(struct update_hook) update_hook_vec; -typedef VEC(struct reload_hook) reload_hook_vec; -typedef VEC(struct delete_hook) delete_hook_vec; -typedef VEC(struct render_hook) render_hook_vec; - -DECLARE_HOOK(create, create_hook_cb, create_hook_vec) -DECLARE_HOOK(destroy, destroy_hook_cb, destroy_hook_vec) -DECLARE_HOOK(insert, insert_hook_cb, insert_hook_vec) -DECLARE_HOOK(update, update_hook_cb, update_hook_vec) -DECLARE_HOOK(reload, reload_hook_cb, reload_hook_vec) -DECLARE_HOOK(render, render_hook_cb, render_hook_vec) -DECLARE_HOOK(delete, delete_hook_cb, delete_hook_vec) +HOOK_IMPL(create, create_hook_cb); +HOOK_IMPL(destroy, destroy_hook_cb); +HOOK_IMPL(insert, insert_hook_cb); +HOOK_IMPL(update, update_hook_cb); +HOOK_IMPL(reload, reload_hook_cb); +HOOK_IMPL(render, render_hook_cb); +HOOK_IMPL(delete, delete_hook_cb); +HOOK_IMPL(pre_delete, delete_hook_cb); +HOOK_IMPL(pre_save, pre_save_cb); +HOOK_IMPL(post_save, post_save_cb); static create_hook_vec g_create_hooks; uint32_t g_create_hook_id; @@ -114,6 +68,15 @@ struct hooks { delete_hook_vec delete_hooks; uint32_t delete_hook_id; + + pre_delete_hook_vec pre_delete_hooks; + uint32_t pre_delete_hook_id; + + pre_save_hook_vec pre_save_hooks; + uint32_t pre_save_hook_id; + + post_save_hook_vec post_save_hooks; + uint32_t post_save_hook_id; }; uint32_t buffer_add_create_hook(create_hook_cb callback, void *userdata) { @@ -202,9 +165,12 @@ static struct buffer create_internal(const char *name, char *filename) { .modified = false, .readonly = false, .lazy_row_add = true, + .retain_properties = false, .lang = filename != NULL ? lang_from_filename(filename) : lang_from_id("fnd"), .last_write = {0}, + .version = 0, + .needs_render = false, }; b.hooks = calloc(1, sizeof(struct hooks)); @@ -213,7 +179,10 @@ static struct buffer create_internal(const char *name, char *filename) { VEC_INIT(&b.hooks->reload_hooks, 8); VEC_INIT(&b.hooks->render_hooks, 8); VEC_INIT(&b.hooks->delete_hooks, 8); + VEC_INIT(&b.hooks->pre_delete_hooks, 8); VEC_INIT(&b.hooks->destroy_hooks, 8); + VEC_INIT(&b.hooks->pre_save_hooks, 8); + VEC_INIT(&b.hooks->post_save_hooks, 8); undo_init(&b.undo, 100); @@ -280,7 +249,7 @@ static bool is_word_break(const struct codepoint *codepoint) { uint32_t c = codepoint->codepoint; return c == ' ' || c == '.' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == ';' || c == '<' || c == '>' || c == ':' || - c == '"'; + c == '"' || c == '=' || c == ','; } static bool is_word_char(const struct codepoint *c) { @@ -334,8 +303,7 @@ find_prev_in_line(struct buffer *buffer, struct location start, } return (struct match_result){ - .at = - (struct location){.line = start.line, .col = found ? found_at : coli}, + .at = (struct location){.line = start.line, .col = found ? found_at : 0}, .found = found}; } @@ -400,9 +368,7 @@ struct buffer buffer_create(const char *name) { struct buffer b = create_internal(name, NULL); - VEC_FOR_EACH(&g_create_hooks, struct create_hook * h) { - h->callback(&b, h->userdata); - } + dispatch_hook(&g_create_hooks, struct create_hook, &b); return b; } @@ -412,9 +378,7 @@ struct buffer buffer_from_file(const char *path) { struct buffer b = create_internal(basename((char *)path), full_path); buffer_read_from_file(&b); - VEC_FOR_EACH(&g_create_hooks, struct create_hook * h) { - h->callback(&b, h->userdata); - } + dispatch_hook(&g_create_hooks, struct create_hook, &b); return b; } @@ -440,6 +404,8 @@ void buffer_to_file(struct buffer *buffer) { return; } + dispatch_hook(&buffer->hooks->pre_save_hooks, struct pre_save_hook, buffer); + uint32_t nlines = text_num_lines(buffer->text); uint32_t nlines_to_write = nlines; if (nlines > 0) { @@ -452,13 +418,19 @@ void buffer_to_file(struct buffer *buffer) { buffer->filename); fclose(file); - clock_gettime(CLOCK_REALTIME, &buffer->last_write); buffer->modified = false; undo_push_boundary(&buffer->undo, (struct undo_boundary){.save_point = true}); + + struct stat sb; + stat(buffer->filename, &sb); + buffer->last_write = sb.st_mtim; + + dispatch_hook(&buffer->hooks->post_save_hooks, struct post_save_hook, buffer); } void buffer_set_filename(struct buffer *buffer, const char *filename) { buffer->filename = to_abspath(filename); + ++buffer->version; buffer->modified = true; } @@ -478,16 +450,12 @@ void buffer_reload(struct buffer *buffer) { sb.st_mtim.tv_nsec != buffer->last_write.tv_nsec) { text_clear(buffer->text); buffer_read_from_file(buffer); - VEC_FOR_EACH(&buffer->hooks->reload_hooks, struct reload_hook * h) { - h->callback(buffer, h->userdata); - } + dispatch_hook(&buffer->hooks->reload_hooks, struct reload_hook, buffer); } } void buffer_destroy(struct buffer *buffer) { - VEC_FOR_EACH(&buffer->hooks->destroy_hooks, struct destroy_hook * h) { - h->callback(buffer, h->userdata); - } + dispatch_hook(&buffer->hooks->destroy_hooks, struct destroy_hook, buffer); lang_destroy(&buffer->lang); @@ -506,6 +474,9 @@ void buffer_destroy(struct buffer *buffer) { VEC_DESTROY(&buffer->hooks->insert_hooks); VEC_DESTROY(&buffer->hooks->destroy_hooks); VEC_DESTROY(&buffer->hooks->delete_hooks); + VEC_DESTROY(&buffer->hooks->pre_delete_hooks); + VEC_DESTROY(&buffer->hooks->pre_save_hooks); + VEC_DESTROY(&buffer->hooks->post_save_hooks); free(buffer->hooks); undo_destroy(&buffer->undo); @@ -518,6 +489,8 @@ struct location buffer_add(struct buffer *buffer, struct location at, return at; } + buffer->needs_render = true; + // invalidate last paste g_kill_ring.paste_up_to_date = false; @@ -553,26 +526,20 @@ struct location buffer_add(struct buffer *buffer, struct location at, (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->version; + buffer->modified = true; uint32_t begin_idx = to_global_offset(buffer, at_bytes); uint32_t end_idx = to_global_offset(buffer, final_bytes); - VEC_FOR_EACH(&buffer->hooks->insert_hooks, struct insert_hook * h) { - h->callback(buffer, + dispatch_hook(&buffer->hooks->insert_hooks, struct insert_hook, buffer, (struct edit_location){ .coordinates = region_new(initial, final), .bytes = region_new(at_bytes, final_bytes), .global_byte_begin = begin_idx, .global_byte_end = end_idx, - }, - h->userdata); - } + }); - buffer->modified = true; return final; } @@ -636,8 +603,8 @@ struct location buffer_previous_word(struct buffer *buffer, struct location dot) { struct match_result res = find_prev_in_line(buffer, dot, is_word_break); - if (!res.found && res.at.col == dot.col) { - return buffer_previous_char(buffer, res.at); + if (!res.found) { + return (struct location){.line = dot.line, .col = 0}; } // check if we got here from the middle of a word or not @@ -647,7 +614,7 @@ struct location buffer_previous_word(struct buffer *buffer, if (traveled <= 1) { res = find_prev_in_line(buffer, res.at, is_word_char); if (!res.found) { - return buffer_previous_char(buffer, res.at); + return (struct location){.line = dot.line, .col = 0}; } // at this point, we are at the end of the previous word @@ -657,7 +624,7 @@ struct location buffer_previous_word(struct buffer *buffer, } else { res.at = buffer_next_char(buffer, res.at); } - } else { + } else if (res.at.col > 0) { res.at = buffer_next_char(buffer, res.at); } @@ -823,6 +790,11 @@ struct location buffer_indent_alt(struct buffer *buffer, struct location at) { return do_indent(buffer, at, get_tab_width(buffer), !use_tabs(buffer)); } +void buffer_push_undo_boundary(struct buffer *buffer) { + undo_push_boundary(&buffer->undo, + (struct undo_boundary){.save_point = false}); +} + struct location buffer_undo(struct buffer *buffer, struct location dot) { struct undo_stack *undo = &buffer->undo; undo_begin(undo); @@ -958,10 +930,17 @@ struct location buffer_delete(struct buffer *buffer, struct region region) { return region.begin; } + buffer->needs_render = true; + if (!region_has_size(region)) { return region.begin; } + region.begin = buffer_clamp(buffer, (int64_t)region.begin.line, + (int64_t)region.begin.col); + region.end = + buffer_clamp(buffer, (int64_t)region.end.line, (int64_t)region.end.col); + struct location begin_bytes = buffer_location_to_byte_coords(buffer, region.begin); struct location end_bytes = @@ -971,34 +950,37 @@ struct location buffer_delete(struct buffer *buffer, struct region region) { text_get_region(buffer->text, begin_bytes.line, begin_bytes.col, end_bytes.line, end_bytes.col); - undo_push_boundary(&buffer->undo, - (struct undo_boundary){.save_point = false}); - undo_push_delete(&buffer->undo, (struct undo_delete){.data = txt.text, .nbytes = txt.nbytes, .pos = {.row = region.begin.line, .col = region.begin.col}}); - undo_push_boundary(&buffer->undo, - (struct undo_boundary){.save_point = false}); uint64_t begin_idx = to_global_offset(buffer, begin_bytes); uint64_t end_idx = to_global_offset(buffer, end_bytes); + ++buffer->version; + buffer->modified = true; + + dispatch_hook(&buffer->hooks->pre_delete_hooks, struct pre_delete_hook, + buffer, + (struct edit_location){ + .coordinates = region, + .bytes = region_new(begin_bytes, end_bytes), + .global_byte_begin = begin_idx, + .global_byte_end = end_idx, + }); + text_delete(buffer->text, begin_bytes.line, begin_bytes.col, end_bytes.line, end_bytes.col); - buffer->modified = true; - VEC_FOR_EACH(&buffer->hooks->delete_hooks, struct delete_hook * h) { - h->callback(buffer, + dispatch_hook(&buffer->hooks->delete_hooks, struct delete_hook, buffer, (struct edit_location){ .coordinates = region, .bytes = region_new(begin_bytes, end_bytes), .global_byte_begin = begin_idx, .global_byte_end = end_idx, - }, - h->userdata); - } + }); return region.begin; } @@ -1047,8 +1029,13 @@ struct text_chunk buffer_line(struct buffer *buffer, uint32_t line) { } struct text_chunk buffer_region(struct buffer *buffer, struct region region) { - return text_get_region(buffer->text, region.begin.line, region.begin.col, - region.end.line, region.end.col); + struct location begin_bytes = + buffer_location_to_byte_coords(buffer, region.begin); + struct location end_bytes = + buffer_location_to_byte_coords(buffer, region.end); + + return text_get_region(buffer->text, begin_bytes.line, begin_bytes.col, + end_bytes.line, end_bytes.col); } uint32_t buffer_add_insert_hook(struct buffer *buffer, insert_hook_cb hook, @@ -1073,6 +1060,18 @@ void buffer_remove_delete_hook(struct buffer *buffer, uint32_t hook_id, remove_delete_hook(&buffer->hooks->delete_hooks, hook_id, callback); } +uint32_t buffer_add_pre_delete_hook(struct buffer *buffer, delete_hook_cb hook, + void *userdata) { + return insert_pre_delete_hook(&buffer->hooks->pre_delete_hooks, + &buffer->hooks->pre_delete_hook_id, hook, + userdata); +} + +void buffer_remove_pre_delete_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback) { + remove_pre_delete_hook(&buffer->hooks->pre_delete_hooks, hook_id, callback); +} + uint32_t buffer_add_update_hook(struct buffer *buffer, update_hook_cb hook, void *userdata) { return insert_update_hook(&buffer->hooks->update_hooks, @@ -1106,6 +1105,30 @@ void buffer_remove_reload_hook(struct buffer *buffer, uint32_t hook_id, remove_reload_hook(&buffer->hooks->reload_hooks, hook_id, callback); } +uint32_t buffer_add_pre_save_hook(struct buffer *buffer, pre_save_cb callback, + void *userdata) { + return insert_pre_save_hook(&buffer->hooks->pre_save_hooks, + &buffer->hooks->pre_save_hook_id, callback, + userdata); +} + +void buffer_remove_pre_save_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback) { + remove_pre_save_hook(&buffer->hooks->pre_save_hooks, hook_id, callback); +} + +uint32_t buffer_add_post_save_hook(struct buffer *buffer, post_save_cb callback, + void *userdata) { + return insert_post_save_hook(&buffer->hooks->post_save_hooks, + &buffer->hooks->post_save_hook_id, callback, + userdata); +} + +void buffer_remove_post_save_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback) { + remove_post_save_hook(&buffer->hooks->post_save_hooks, hook_id, callback); +} + struct cmdbuf { struct command_list *cmds; struct location origin; @@ -1133,6 +1156,15 @@ static void apply_properties(struct command_list *cmds, if (colors->set_fg) { command_list_set_index_color_fg(cmds, colors->fg); } + + if (colors->underline) { + command_list_set_underline(cmds); + } + + if (colors->inverted) { + command_list_set_inverted_colors(cmds); + } + break; } case TextProperty_Data: @@ -1215,7 +1247,6 @@ void render_line(struct text_chunk *line, void *userdata) { command_list_reset_color(cmdbuf->cmds); command_list_set_show_whitespace(cmdbuf->cmds, false); - // TODO: considering the whole screen is cleared, is this really needed? if (drawn_coli < cmdbuf->width) { command_list_draw_repeated(cmdbuf->cmds, drawn_coli, visual_line, ' ', cmdbuf->width - drawn_coli); @@ -1223,9 +1254,7 @@ void render_line(struct text_chunk *line, void *userdata) { } void buffer_update(struct buffer *buffer) { - VEC_FOR_EACH(&buffer->hooks->update_hooks, struct update_hook * h) { - h->callback(buffer, h->userdata); - } + dispatch_hook(&buffer->hooks->update_hooks, struct update_hook, buffer); } void buffer_render(struct buffer *buffer, struct buffer_render_params *params) { @@ -1233,10 +1262,8 @@ void buffer_render(struct buffer *buffer, struct buffer_render_params *params) { return; } - VEC_FOR_EACH(&buffer->hooks->render_hooks, struct render_hook * h) { - h->callback(buffer, h->userdata, params->origin, params->width, - params->height); - } + dispatch_hook(&buffer->hooks->render_hooks, struct render_hook, buffer, + params->origin, params->width, params->height); struct setting *show_ws = settings_get("editor.show-whitespace"); @@ -1258,17 +1285,40 @@ void buffer_render(struct buffer *buffer, struct buffer_render_params *params) { ++linei) { command_list_draw_repeated(params->commands, 0, linei, ' ', params->width); } + + buffer->needs_render = false; } void buffer_add_text_property(struct buffer *buffer, struct location start, struct location end, struct text_property property) { + buffer->needs_render = true; struct location bytestart = buffer_location_to_byte_coords(buffer, start); struct location byteend = buffer_location_to_byte_coords(buffer, end); text_add_property(buffer->text, bytestart.line, bytestart.col, byteend.line, byteend.col, property); } +void buffer_add_text_property_to_layer(struct buffer *buffer, + struct location start, + struct location end, + struct text_property property, + layer_id layer) { + buffer->needs_render = true; + struct location bytestart = buffer_location_to_byte_coords(buffer, start); + struct location byteend = buffer_location_to_byte_coords(buffer, end); + text_add_property_to_layer(buffer->text, bytestart.line, bytestart.col, + byteend.line, byteend.col, property, layer); +} + +layer_id buffer_add_text_property_layer(struct buffer *buffer) { + return text_add_property_layer(buffer->text); +} + +void buffer_remove_property_layer(struct buffer *buffer, layer_id layer) { + text_remove_property_layer(buffer->text, layer); +} + void buffer_get_text_properties(struct buffer *buffer, struct location location, struct text_property **properties, uint32_t max_nproperties, @@ -1278,10 +1328,25 @@ void buffer_get_text_properties(struct buffer *buffer, struct location location, max_nproperties, nproperties); } +void buffer_get_text_properties_filtered(struct buffer *buffer, + struct location location, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties, + layer_id layer) { + struct location bytecoords = buffer_location_to_byte_coords(buffer, location); + text_get_properties_filtered(buffer->text, bytecoords.line, bytecoords.col, + properties, max_nproperties, nproperties, layer); +} + void buffer_clear_text_properties(struct buffer *buffer) { text_clear_properties(buffer->text); } +void buffer_clear_text_property_layer(struct buffer *buffer, layer_id layer) { + text_clear_property_layer(buffer->text, layer); +} + static int compare_lines(const void *l1, const void *l2) { return s8cmp(*(const struct s8 *)l1, *(const struct s8 *)l2); } diff --git a/src/dged/buffer.h b/src/dged/buffer.h index 0e45b98..25cc42b 100644 --- a/src/dged/buffer.h +++ b/src/dged/buffer.h @@ -7,6 +7,7 @@ #include <time.h> #include "command.h" +#include "hook.h" #include "lang.h" #include "location.h" #include "text.h" @@ -63,6 +64,17 @@ struct buffer { /** If true, force whitespace indication off for this buffer */ bool force_show_ws_off; + + /** If true, text properties are not immediate */ + bool retain_properties; + + bool needs_render; + + /** + * Version that increases with each edit (including undo). + * Can be used to check if a buffer has changed. + */ + uint64_t version; }; void buffer_static_init(void); @@ -342,6 +354,8 @@ struct location buffer_indent_alt(struct buffer *buffer, struct location at); */ struct location buffer_undo(struct buffer *buffer, struct location dot); +void buffer_push_undo_boundary(struct buffer *buffer); + /** * Search for a substring in the buffer. * @@ -433,8 +447,46 @@ void buffer_add_text_property(struct buffer *buffer, struct location start, struct text_property property); /** + * Add a text property to a region of the buffer and a specified property layer. + * + * @param buffer The buffer to add a text property to. + * @param start The start of the region to set the property for. + * @param end The end of the region to set the property for. + * @param property The text property to set. + * @param layer Id of the layer to add the text property to. + */ +void buffer_add_text_property_to_layer(struct buffer *buffer, + struct location start, + struct location end, + struct text_property property, + layer_id layer); + +/** + * Add a new layer for holding properties. + * + * Note that only the default layer is cleared automatically + * when @ref retain_properties is false. Any other layer + * needs to be cleared manually when needed. + * + * @param [in] buffer The buffer to add the property layer to. + * + * @returns The id of the added layer, -1 on error. + */ +layer_id buffer_add_text_property_layer(struct buffer *buffer); + +/** + * Remove a property layer. + * + * @param [in] buffer The buffer to remove the property layer from + * @param [in] layer The layer id of the layer to remove. + */ +void buffer_remove_property_layer(struct buffer *buffer, layer_id layer); + +/** * Get active text properties at @p location in @p buffer. * + * This will retrieve properties from all property layers. + * * @param buffer The buffer to get properties for. * @param location The location to get properties at. * @param properties Caller-provided array of properties set by this function. @@ -447,14 +499,35 @@ void buffer_get_text_properties(struct buffer *buffer, struct location location, uint32_t *nproperties); /** - * Clear any text properties for @p buffer. + * Get active text properties at @p location in @p buffer for the layer @layer. + * + * @param buffer The buffer to get properties for. + * @param location The location to get properties at. + * @param properties Caller-provided array of properties set by this function. + * @param max_nproperties Max num properties to put in @p properties. + * @param nproperties Number of properties that got stored in @p properties. + * @param layer Id of the layer to fetch properties for. + */ +void buffer_get_text_properties_filtered(struct buffer *buffer, + struct location location, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties, layer_id layer); + +/** + * Clear any text properties from the default property layer for @p buffer. * * @param buffer The buffer to clear properties for. */ void buffer_clear_text_properties(struct buffer *buffer); -/** Callback when removing hooks to clean up userdata */ -typedef void (*remove_hook_cb)(void *userdata); +/** + * Clear text properties from layer @ref layer. + * + * @param buffer The buffer to clear properties for. + * @param layer The layer to clear. + */ +void buffer_clear_text_property_layer(struct buffer *buffer, layer_id layer); /** * Buffer update hook callback function. @@ -496,9 +569,8 @@ void buffer_remove_update_hook(struct buffer *buffer, uint32_t hook_id, * @param width The width of the rendered region. * @param height The height of the rendered region. */ -typedef void (*render_hook_cb)(struct buffer *buffer, void *userdata, - struct location origin, uint32_t width, - uint32_t height); +typedef void (*render_hook_cb)(struct buffer *buffer, struct location origin, + uint32_t width, uint32_t height, void *userdata); /** * Add a buffer render hook. @@ -567,9 +639,6 @@ struct edit_location { * * @param buffer The buffer. * @param inserted The position in the @p buffer where text was inserted. - * @param begin_idx The global byte offset to the start of where text was - * inserted. - * @param end_idx The global byte offset to the end of where text was inserted. * @param userdata The userdata as sent in to @ref buffer_add_insert_hook. */ typedef void (*insert_hook_cb)(struct buffer *buffer, @@ -602,8 +671,6 @@ void buffer_remove_insert_hook(struct buffer *buffer, uint32_t hook_id, * * @param buffer The buffer. * @param removed The region that was removed from the @p buffer. - * @param begin_idx The global byte offset to the start of the removed text. - * @param end_idx The global byte offset to the end of the removed text. * @param userdata The userdata as sent in to @ref buffer_add_delete_hook. */ typedef void (*delete_hook_cb)(struct buffer *buffer, @@ -632,6 +699,29 @@ void buffer_remove_delete_hook(struct buffer *buffer, uint32_t hook_id, remove_hook_cb callback); /** + * Add a pre-delete hook, called when text is about to be removed from the @p + * buffer. + * + * @param buffer The buffer to add a delete hook to. + * @param callback The function to call when text is removed from @p buffer. + * @param userdata Data that is passed unmodified to the delete hook. + * @returns The hook id. + */ +uint32_t buffer_add_pre_delete_hook(struct buffer *buffer, + delete_hook_cb callback, void *userdata); + +/** + * Remove a buffer pre-delete hook. + * + * @param [in] buffer The buffer to remove the hook from. + * @param [in] hook_id The hook id as returned from @ref buffer_add_delete_hook. + * @param [in] callback A function called with the userdata pointer to do + * cleanup. + */ +void buffer_remove_pre_delete_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback); + +/** * Buffer destroy hook callback function. * * @param buffer The buffer. @@ -690,6 +780,68 @@ uint32_t buffer_add_create_hook(create_hook_cb callback, void *userdata); void buffer_remove_create_hook(uint32_t hook_id, remove_hook_cb callback); /** + * Buffer pre-save callback function + * + * @param buffer The buffer about to be saved. + * @param userdata The userdata as sent in to @ref buffer_add_pre_save_hook. + */ +typedef void (*pre_save_cb)(struct buffer *buffer, void *userdata); + +/** + * Add a pre-save hook, called when @p buffer is about to be saved. + * + * @param buffer The buffer to add a pre-save hook to. + * @param callback The function to call @p buffer is about to be saved. + * @param userdata Data that is passed unmodified to the pre-save hook. + * @returns The hook id. + */ +uint32_t buffer_add_pre_save_hook(struct buffer *buffer, pre_save_cb callback, + void *userdata); + +/** + * Remove a buffer pre-save hook. + * + * @param [in] buffer The buffer to remove the hook from. + * @param [in] hook_id The hook id as returned from @ref + * buffer_add_pre_save_hook. + * @param [in] callback A function called with the userdata pointer to do + * cleanup. + */ +void buffer_remove_pre_save_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback); + +/** + * Buffer post-save callback function + * + * @param buffer The buffer that was saved. + * @param userdata The userdata as sent in to @ref buffer_add_post_save_hook. + */ +typedef void (*post_save_cb)(struct buffer *buffer, void *userdata); + +/** + * Add a post-save hook, called when @p buffer has been saved. + * + * @param buffer The buffer to add a post-save hook to. + * @param callback The function to call @p buffer is saved. + * @param userdata Data that is passed unmodified to the post-save hook. + * @returns The hook id. + */ +uint32_t buffer_add_post_save_hook(struct buffer *buffer, post_save_cb callback, + void *userdata); + +/** + * Remove a buffer post-save hook. + * + * @param [in] buffer The buffer to remove the hook from. + * @param [in] hook_id The hook id as returned from @ref + * buffer_add_post_save_hook. + * @param [in] callback A function called with the userdata pointer to do + * cleanup. + */ +void buffer_remove_post_save_hook(struct buffer *buffer, uint32_t hook_id, + remove_hook_cb callback); + +/** * Parameters for rendering a buffer. */ struct buffer_render_params { diff --git a/src/dged/buffer_view.c b/src/dged/buffer_view.c index a9bbe19..9d998fe 100644 --- a/src/dged/buffer_view.c +++ b/src/dged/buffer_view.c @@ -7,10 +7,10 @@ #include "timers.h" #include "utf8.h" -struct modeline { - uint8_t *buffer; - uint32_t sz; -}; +HOOK_IMPL(modeline, modeline_hook_cb); + +static modeline_hook_vec g_modeline_hooks = {0}; +static uint32_t g_modeline_hook_id = 0; static bool maybe_delete_region(struct buffer_view *view) { struct region reg = region_new(view->dot, view->mark); @@ -32,18 +32,11 @@ struct buffer_view buffer_view_create(struct buffer *buffer, bool modeline, .mark_set = false, .scroll = (struct location){.line = 0, .col = 0}, .buffer = buffer, - .modeline = NULL, + .modeline = modeline, .line_numbers = line_numbers, .fringe_width = 0, }; - if (modeline) { - v.modeline = calloc(1, sizeof(struct modeline)); - v.modeline->buffer = malloc(1024); - v.modeline->sz = 1024; - v.modeline->buffer[0] = '\0'; - } - return v; } @@ -54,32 +47,22 @@ struct buffer_view buffer_view_clone(const struct buffer_view *view) { .mark_set = view->mark_set, .scroll = view->scroll, .buffer = view->buffer, - .modeline = NULL, + .modeline = view->modeline, .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); - view->modeline = NULL; - } - - view->buffer = NULL; -} +void buffer_view_destroy(struct buffer_view *view) { view->buffer = NULL; } void buffer_view_add(struct buffer_view *view, uint8_t *txt, uint32_t nbytes) { maybe_delete_region(view); + struct location before = view->dot; view->dot = buffer_add(view->buffer, view->dot, txt, nbytes); + if (view->dot.line > before.line) { + buffer_push_undo_boundary(view->buffer); + } } void buffer_view_goto_beginning(struct buffer_view *view) { @@ -107,7 +90,11 @@ void buffer_view_forward_word(struct buffer_view *view) { } void buffer_view_backward_word(struct buffer_view *view) { + struct location before = view->dot; view->dot = buffer_previous_word(view->buffer, view->dot); + if (before.col == 0 && view->dot.col == 0) { + buffer_view_backward_char(view); + } } void buffer_view_forward_line(struct buffer_view *view) { @@ -138,6 +125,7 @@ void buffer_view_goto_beginning_of_line(struct buffer_view *view) { void buffer_view_newline(struct buffer_view *view) { view->dot = buffer_newline(view->buffer, view->dot); + buffer_push_undo_boundary(view->buffer); } void buffer_view_indent(struct buffer_view *view) { @@ -202,6 +190,7 @@ void buffer_view_paste_older(struct buffer_view *view) { } void buffer_view_forward_delete_char(struct buffer_view *view) { + buffer_push_undo_boundary(view->buffer); if (maybe_delete_region(view)) { return; } @@ -209,9 +198,11 @@ void buffer_view_forward_delete_char(struct buffer_view *view) { view->dot = buffer_delete( view->buffer, region_new(view->dot, buffer_next_char(view->buffer, view->dot))); + buffer_push_undo_boundary(view->buffer); } void buffer_view_backward_delete_char(struct buffer_view *view) { + buffer_push_undo_boundary(view->buffer); if (maybe_delete_region(view)) { return; } @@ -219,9 +210,11 @@ void buffer_view_backward_delete_char(struct buffer_view *view) { view->dot = buffer_delete( view->buffer, region_new(buffer_previous_char(view->buffer, view->dot), view->dot)); + buffer_push_undo_boundary(view->buffer); } void buffer_view_delete_word(struct buffer_view *view) { + buffer_push_undo_boundary(view->buffer); if (maybe_delete_region(view)) { return; } @@ -232,9 +225,11 @@ void buffer_view_delete_word(struct buffer_view *view) { buffer_delete(view->buffer, word); view->dot = word.begin; } + buffer_push_undo_boundary(view->buffer); } void buffer_view_kill_line(struct buffer_view *view) { + buffer_push_undo_boundary(view->buffer); uint32_t ncols = buffer_line_length(view->buffer, view->dot.line) - view->dot.col; @@ -254,6 +249,7 @@ void buffer_view_kill_line(struct buffer_view *view) { }); buffer_cut(view->buffer, reg); + buffer_push_undo_boundary(view->buffer); } void buffer_view_sort_lines(struct buffer_view *view) { @@ -354,52 +350,87 @@ static uint32_t render_line_numbers(struct buffer_view *view, return longest_nchars + 2; } -static void render_modeline(struct modeline *modeline, struct buffer_view *view, +static void render_modeline(struct buffer_view *view, struct command_list *commands, uint32_t window_id, uint32_t width, uint32_t height, float frame_time) { - char buf[width * 4]; - memset(buf, 0, width * 4); - time_t now = time(NULL); struct tm *lt = localtime(&now); - static char left[128] = {0}; - static char right[128] = {0}; - - snprintf(left, 128, " %c%c %d:%-16s (%d, %d) (%s)", - view->buffer->modified ? '*' : '-', - view->buffer->readonly ? '%' : '-', window_id, view->buffer->name, - view->dot.line + 1, 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; - - uint32_t len = strlen(buf); - len = (len + 1) > modeline->sz ? modeline->sz - 1 : len; - memcpy(modeline->buffer, buf, len); - modeline->buffer[len] = '\0'; + + char left[1024] = {}; + char right[1024] = {}; + + size_t left_len = snprintf(left, 1024, " %c%c %d:%-16s (%d, %d) (%s) ", + view->buffer->modified ? '*' : '-', + view->buffer->readonly ? '%' : '-', window_id, + view->buffer->name, view->dot.line + 1, + view->dot.col, view->buffer->lang.name); + + /* insert hook content on the left */ + VEC_FOR_EACH(&g_modeline_hooks, struct modeline_hook * hook) { + struct s8 content = hook->callback(view, hook->userdata); + if (content.l > 0) { + left_len += snprintf(left + left_len, 1024 - left_len, "[%.*s] ", + content.l, content.s); + s8delete(content); + } + } + + size_t right_len = snprintf(right, 1024, " (%.2f ms) %02d:%02d", + frame_time / 1e6, lt->tm_hour, lt->tm_min); + + /* clamp all the widths with priority: + * 1. left + * 2. right + * 3. mid + */ + left_len = left_len > width ? width : left_len; + right_len = left_len + right_len > width ? width - left_len : right_len; + size_t mid_len = + left_len + right_len < width ? width - left_len - right_len : 0; + + char mid[mid_len + 1] = {}; + if (mid_len > 0) { + memset(mid, '-', mid_len); + mid[0] = ' '; + mid[mid_len - 1] = ' '; + mid[mid_len] = '\0'; + } + + if (left_len > 0) { + command_list_set_index_color_bg(commands, Color_BrightBlack); + command_list_set_index_color_fg(commands, Color_White); + command_list_draw_text_copy(commands, 0, height - 1, (uint8_t *)left, + left_len); + } + + if (mid_len > 0) { + command_list_set_index_color_bg(commands, Color_BrightBlack); + command_list_set_index_color_fg(commands, Color_White); + command_list_draw_text_copy(commands, left_len, height - 1, (uint8_t *)mid, + mid_len); + } + + if (right_len > 0) { + command_list_set_index_color_bg(commands, Color_BrightBlack); + command_list_set_index_color_fg(commands, Color_White); + command_list_draw_text_copy(commands, left_len + mid_len, height - 1, + (uint8_t *)right, right_len); } - command_list_set_index_color_bg(commands, Color_BrightBlack); - command_list_set_index_color_fg(commands, Color_White); - command_list_draw_text(commands, 0, height - 1, modeline->buffer, - strlen((char *)modeline->buffer)); command_list_reset_color(commands); } -void buffer_view_update(struct buffer_view *view, +bool buffer_view_update(struct buffer_view *view, struct buffer_view_update_params *params) { + bool needs_render = false; struct timer *buffer_update_timer = timer_start("update-windows.buffer-update"); buffer_update(view->buffer); timer_stop(buffer_update_timer); + needs_render |= view->buffer->needs_render; + uint32_t height = params->height; uint32_t width = params->width; @@ -412,10 +443,10 @@ void buffer_view_update(struct buffer_view *view, struct timer *render_modeline_timer = timer_start("update-windows.modeline-render"); uint32_t modeline_height = 0; - if (view->modeline != NULL) { + if (view->modeline) { modeline_height = 1; - render_modeline(view->modeline, view, params->commands, params->window_id, - params->width, params->height, params->frame_time); + render_modeline(view, params->commands, params->window_id, params->width, + params->height, params->frame_time); } height -= modeline_height; @@ -494,4 +525,21 @@ void buffer_view_update(struct buffer_view *view, // draw buffer commands nested inside this command list command_list_draw_command_list(params->commands, buf_cmds); timer_stop(render_buffer_timer); + + return needs_render; +} + +uint32_t buffer_view_add_modeline_hook(modeline_hook_cb callback, + void *userdata) { + if (VEC_CAPACITY(&g_modeline_hooks) == 0) { + VEC_INIT(&g_modeline_hooks, 8); + } + + return insert_modeline_hook(&g_modeline_hooks, &g_modeline_hook_id, callback, + userdata); +} + +void buffer_view_remove_modeline_hook(uint32_t hook_id, + remove_hook_cb callback) { + remove_modeline_hook(&g_modeline_hooks, hook_id, callback); } diff --git a/src/dged/buffer_view.h b/src/dged/buffer_view.h index 4e23b5d..d1b6b4a 100644 --- a/src/dged/buffer_view.h +++ b/src/dged/buffer_view.h @@ -3,7 +3,9 @@ #include <stddef.h> +#include "hook.h" #include "location.h" +#include "s8.h" struct buffer; @@ -25,8 +27,8 @@ struct buffer_view { /** Pointer to the actual buffer */ struct buffer *buffer; - /** Modeline buffer (may be NULL) */ - struct modeline *modeline; + /** Has modeline? */ + bool modeline; /** Current left fringe size */ uint32_t fringe_width; @@ -86,6 +88,15 @@ void buffer_view_undo(struct buffer_view *view); void buffer_view_sort_lines(struct buffer_view *view); +// hack to prevent s8 from being expanded as a macro +// in the function pointer typedef +typedef struct s8 _string; +typedef _string (*modeline_hook_cb)(struct buffer_view *, void *); +uint32_t buffer_view_add_modeline_hook(modeline_hook_cb callback, + void *userdata); +void buffer_view_remove_modeline_hook(uint32_t hook_id, + remove_hook_cb callback); + struct buffer_view_update_params { struct command_list *commands; void *(*frame_alloc)(size_t); @@ -97,7 +108,7 @@ struct buffer_view_update_params { uint32_t window_y; }; -void buffer_view_update(struct buffer_view *view, +bool buffer_view_update(struct buffer_view *view, struct buffer_view_update_params *params); #endif diff --git a/src/dged/buffers.c b/src/dged/buffers.c index d20be39..f6d197d 100644 --- a/src/dged/buffers.c +++ b/src/dged/buffers.c @@ -1,5 +1,6 @@ #include "buffers.h" #include "buffer.h" +#include "s8.h" #include <stdbool.h> #include <stdlib.h> @@ -112,7 +113,7 @@ struct buffer *buffers_find(struct buffers *buffers, const char *name) { struct buffer *buffers_find_by_filename(struct buffers *buffers, const char *path) { struct buffer_chunk *chunk = buffers->head; - size_t pathlen = strlen(path); + struct s8 needle = s8(path); while (chunk != NULL) { for (uint32_t i = 0; i < buffers->chunk_size; ++i) { if (!chunk->entries[i].occupied) { @@ -124,8 +125,8 @@ struct buffer *buffers_find_by_filename(struct buffers *buffers, continue; } - size_t bnamelen = strlen(b->filename); - if (bnamelen == pathlen && memcmp(path, b->filename, bnamelen) == 0) { + struct s8 bname = s8(b->filename); + if (s8endswith(bname, needle)) { return b; } } diff --git a/src/dged/bufread.c b/src/dged/bufread.c new file mode 100644 index 0000000..68ef839 --- /dev/null +++ b/src/dged/bufread.c @@ -0,0 +1,151 @@ +#include "bufread.h" + +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +struct bufread { + uint8_t *buf; + size_t capacity; + size_t read_pos; + size_t write_pos; + int fd; + bool empty; +}; + +struct bufread *bufread_create(int fd, size_t capacity) { + struct bufread *br = (struct bufread *)calloc(1, sizeof(struct bufread)); + br->buf = calloc(capacity, 1); + br->capacity = capacity; + br->read_pos = 0; + br->write_pos = 0; + br->empty = true; + br->fd = fd; + + return br; +} + +void bufread_destroy(struct bufread *br) { + free(br->buf); + br->buf = NULL; + br->capacity = 0; + br->read_pos = 0; + br->write_pos = 0; + br->empty = true; + br->fd = -1; + + free(br); +} + +static ssize_t fill(struct bufread *br) { + ssize_t rd = 0, ret = 0; + + // special case for empty ring buffer + // in this case, reset read and write pos to beginning. + if (br->empty) { + if ((ret = read(br->fd, br->buf, br->capacity)) < 0) { + return ret; + } + + rd = ret; + br->read_pos = 0; + br->write_pos = ret; + br->empty = false; + + return rd; + } + + size_t space_after = + br->read_pos < br->write_pos ? br->capacity - br->write_pos : 0; + if (space_after > 0) { + if ((ret = read(br->fd, &br->buf[br->write_pos], space_after)) < 0) { + return ret; + } + } + + rd += ret; + + // if we wrapped around, there might be more space + if (br->write_pos == br->capacity) { + br->write_pos = 0; + size_t space_before = br->read_pos; + if (space_before > 0) { + if ((ret = read(br->fd, &br->buf[0], space_before)) < 0) { + return ret; + } + } + + br->write_pos += ret; + rd += ret; + } + + br->empty = rd == 0; + return rd; +} + +static size_t available(struct bufread *br) { + if (br->write_pos > br->read_pos) { + return br->write_pos - br->read_pos; + } else if (br->write_pos < br->read_pos) { + return br->write_pos + (br->capacity - br->read_pos); + } + + /* read == write, either empty or full */ + return br->empty ? 0 : br->capacity; +} + +static void consume(struct bufread *br, size_t amount) { + if (amount >= available(br)) { + br->empty = true; + br->read_pos = br->write_pos; + return; + } + + br->read_pos = (br->read_pos + amount) % br->capacity; +} + +ssize_t bufread_read(struct bufread *br, uint8_t *buf, size_t count) { + if (count == 0) { + return 0; + } + + // for read request larger than the internal buffer + // and an empty internal buffer, just go to the + // underlying source + if (br->empty && count >= br->capacity) { + return read(br->fd, buf, count); + } + + if (available(br) < count && available(br) < br->capacity) { + ssize_t fill_res = 0; + if ((fill_res = fill(br)) <= 0) { + return fill_res; + } + } + + // read (at most) to end + uint8_t *tgt = buf; + size_t to_read = 0, rd = 0; + to_read = (br->read_pos < br->write_pos ? br->write_pos : br->capacity) - + br->read_pos; + to_read = to_read > count ? count : to_read; + + memcpy(tgt, &br->buf[br->read_pos], to_read); + tgt += to_read; + rd += to_read; + consume(br, to_read); + + // did we wrap around and have things left to read? + if (br->read_pos == 0 && !br->empty && rd < count) { + to_read = br->write_pos; + to_read = to_read > count ? count : to_read; + + memcpy(tgt, br->buf, to_read); + tgt += to_read; + rd += to_read; + consume(br, to_read); + } + + return rd; +} diff --git a/src/dged/bufread.h b/src/dged/bufread.h new file mode 100644 index 0000000..11a18ff --- /dev/null +++ b/src/dged/bufread.h @@ -0,0 +1,13 @@ +#ifndef _BUFREAD_H +#define _BUFREAD_H + +#include <stddef.h> +#include <stdint.h> +#include <sys/types.h> + +struct bufread; +struct bufread *bufread_create(int fd, size_t capacity); +void bufread_destroy(struct bufread *br); +ssize_t bufread_read(struct bufread *br, uint8_t *buf, size_t count); + +#endif diff --git a/src/dged/command.h b/src/dged/command.h index 2b0f074..ed1c2cc 100644 --- a/src/dged/command.h +++ b/src/dged/command.h @@ -82,7 +82,7 @@ struct command { #define COMMAND_FN(name_, command_name, function, userdata_) \ static struct command command_name##_command = { \ .fn = function, \ - .name = #name_, \ + .name = name_, \ .userdata = userdata_, \ }; diff --git a/src/dged/display.c b/src/dged/display.c index e992cc9..ad9dad2 100644 --- a/src/dged/display.c +++ b/src/dged/display.c @@ -359,6 +359,13 @@ void command_list_set_inverted_colors(struct command_list *list) { cmd->len = 1; } +void command_list_set_underline(struct command_list *list) { + struct push_fmt_cmd *cmd = + add_command(list, RenderCommand_PushFormat)->data.push_fmt; + cmd->fmt[0] = '4'; + cmd->len = 1; +} + void command_list_reset_color(struct command_list *list) { add_command(list, RenderCommand_ClearFormat); } diff --git a/src/dged/display.h b/src/dged/display.h index cfa2eca..6f7f12d 100644 --- a/src/dged/display.h +++ b/src/dged/display.h @@ -198,6 +198,11 @@ void command_list_set_color_fg(struct command_list *list, uint8_t red, void command_list_set_inverted_colors(struct command_list *list); /** + * Enable underline. + */ +void command_list_set_underline(struct command_list *list); + +/** * Reset the color and styling information. * * The following draw commands will have their formatting reset to the default. diff --git a/src/dged/hook.h b/src/dged/hook.h new file mode 100644 index 0000000..66e2839 --- /dev/null +++ b/src/dged/hook.h @@ -0,0 +1,84 @@ +#ifndef _HOOK_H +#define _HOOK_H + +#include <stdint.h> + +#include "vec.h" + +/** Callback when removing hooks to clean up userdata */ +typedef void (*remove_hook_cb)(void *userdata); + +#define HOOK_IMPL(name, callback_type) \ + struct name##_hook { \ + uint32_t id; \ + callback_type callback; \ + void *userdata; \ + }; \ + \ + typedef VEC(struct name##_hook) name##_hook_vec; \ + \ + static inline uint32_t insert_##name##_hook( \ + name##_hook_vec *hooks, uint32_t *id, callback_type callback, \ + void *userdata) { \ + uint32_t iid = ++(*id); \ + struct name##_hook hook = (struct name##_hook){ \ + .id = iid, \ + .callback = callback, \ + .userdata = userdata, \ + }; \ + VEC_PUSH(hooks, hook); \ + \ + return iid; \ + } \ + \ + static inline void remove_##name##_hook(name##_hook_vec *hooks, uint32_t id, \ + remove_hook_cb callback) { \ + uint64_t found_at = (uint64_t) - 1; \ + VEC_FOR_EACH_INDEXED(hooks, struct name##_hook *h, idx) { \ + if (h->id == id) { \ + if (callback != NULL) { \ + callback(h->userdata); \ + } \ + found_at = idx; \ + break; \ + } \ + } \ + if (found_at != (uint64_t) - 1) { \ + if (found_at < VEC_SIZE(hooks) - 1) { \ + VEC_SWAP(hooks, found_at, VEC_SIZE(hooks) - 1); \ + } \ + VEC_POP(hooks, struct name##_hook removed); \ + (void)removed; \ + } \ + } + +#define HOOK_IMPL_NO_REMOVE(name, callback_type) \ + struct name##_hook { \ + uint32_t id; \ + callback_type callback; \ + void *userdata; \ + }; \ + \ + typedef VEC(struct name##_hook) name##_hook_vec; \ + \ + static inline uint32_t insert_##name##_hook( \ + name##_hook_vec *hooks, uint32_t *id, callback_type callback, \ + void *userdata) { \ + uint32_t iid = ++(*id); \ + struct name##_hook hook = (struct name##_hook){ \ + .id = iid, \ + .callback = callback, \ + .userdata = userdata, \ + }; \ + VEC_PUSH(hooks, hook); \ + \ + return iid; \ + } + +#define dispatch_hook(hooks, hook_type, ...) \ + VEC_FOR_EACH(hooks, hook_type *h) { h->callback(__VA_ARGS__, h->userdata); } + +#define dispatch_hook_no_args(hooks, hook_type) \ + VEC_FOR_EACH(hooks, hook_type *h) { h->callback(h->userdata); } + +#endif diff --git a/src/dged/json.c b/src/dged/json.c index 24d5c15..a514f00 100644 --- a/src/dged/json.c +++ b/src/dged/json.c @@ -2,13 +2,24 @@ #include "hash.h" #include "hashmap.h" -#include "utf8.h" #include "vec.h" #include <stddef.h> #include <stdio.h> -HASHMAP_ENTRY_TYPE(json_object_member, struct json_value); +struct json_key_value { + struct s8 key; + struct json_value value; +}; + +HASHMAP_ENTRY_TYPE(json_object_member, struct json_key_value); + +static char errbuf[1024] = {0}; + +static const char *format_error(uint32_t line, uint32_t col, const char *msg) { + snprintf(errbuf, 1024, "(%d, %d): %s", line, col, msg); + return errbuf; +} struct json_object { HASHMAP(struct json_object_member) members; @@ -18,22 +29,176 @@ struct json_array { VEC(struct json_value) values; }; -static void setarray(struct json_value *val) { - val->type = Json_Array; - val->value.array = calloc(1, sizeof(struct json_array)); - VEC_INIT(&val->value.array->values, 10); +static struct json_value create_array(struct json_value *parent) { + struct json_value val = {0}; + val.type = Json_Array; + val.parent = parent; + val.value.array = calloc(1, sizeof(struct json_array)); + VEC_INIT(&val.value.array->values, 10); + + return val; } -static void setobject(struct json_value *val) { - val->type = Json_Object; - val->value.object = calloc(1, sizeof(struct json_object)); - HASHMAP_INIT(&val->value.object->members, 10, hash_name); +static struct json_value create_object(struct json_value *parent) { + struct json_value val = {0}; + val.type = Json_Object; + val.parent = parent; + val.value.object = calloc(1, sizeof(struct json_object)); + HASHMAP_INIT(&val.value.object->members, 10, hash_name); + + return val; } -static void setstring(struct json_value *val, uint8_t *current) { - val->type = Json_String; - val->value.string.s = current; - val->value.string.l = 0; +struct s8 unescape_json_string(struct s8 input) { + /* FIXME: this is a bit funky and does not take + unicode characters into account and probably also + misses some escape codes. */ + size_t new_size = 0; + bool escape = false; + for (size_t bi = 0; bi < input.l; ++bi) { + uint8_t b = input.s[bi]; + if (b == '\\' && !escape) { + escape = true; + continue; + } + + ++new_size; + escape = false; + } + + escape = false; + uint8_t *buf = calloc(new_size, 1); + size_t bufi = 0; + for (size_t bi = 0; bi < input.l; ++bi) { + uint8_t b = input.s[bi]; + + if (b == '\\' && !escape) { + escape = true; + continue; + } + + if (escape) { + switch (b) { + case 'b': + buf[bufi] = '\b'; + break; + case '\\': + buf[bufi] = '\\'; + break; + case 'f': + buf[bufi] = '\f'; + break; + case 'n': + buf[bufi] = '\n'; + break; + case 'r': + buf[bufi] = '\r'; + break; + case 't': + buf[bufi] = '\t'; + break; + case '"': + buf[bufi] = '"'; + break; + default: + buf[bufi] = b; + } + } else { + buf[bufi] = b; + } + + escape = false; + ++bufi; + } + + return (struct s8){ + .s = buf, + .l = new_size, + }; +} + +struct s8 escape_json_string(struct s8 input) { + size_t new_size = 0; + for (size_t bi = 0; bi < input.l; ++bi) { + uint8_t b = input.s[bi]; + switch (b) { + case '\\': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '"': + new_size += 2; + break; + default: + ++new_size; + } + } + + uint8_t *buf = calloc(new_size, 1); + size_t bufi = 0; + for (size_t bi = 0; bi < input.l; ++bi) { + uint8_t b = input.s[bi]; + switch (b) { + case '\\': + buf[bufi] = '\\'; + buf[bufi + 1] = '\\'; + bufi += 2; + break; + case '\b': + buf[bufi] = '\\'; + buf[bufi + 1] = 'b'; + bufi += 2; + break; + case '\f': + buf[bufi] = '\\'; + buf[bufi + 1] = 'f'; + bufi += 2; + break; + case '\n': + buf[bufi] = '\\'; + buf[bufi + 1] = 'n'; + bufi += 2; + break; + case '\r': + buf[bufi] = '\\'; + buf[bufi + 1] = 'r'; + bufi += 2; + break; + case '\t': + buf[bufi] = '\\'; + buf[bufi + 1] = 't'; + bufi += 2; + break; + case '"': + buf[bufi] = '\\'; + buf[bufi + 1] = '"'; + bufi += 2; + break; + default: + buf[bufi] = b; + ++bufi; + } + } + + return (struct s8){ + .s = buf, + .l = new_size, + }; +} + +static struct json_value create_string(const uint8_t *start, uint32_t len, + struct json_value *parent) { + struct json_value val = {0}; + val.type = Json_String; + val.parent = parent; + val.value.string.s = (uint8_t *)start; + val.value.string.l = len; + val.start = start; + val.end = start + len; + + return val; } static bool is_number(uint8_t byte) { return byte >= '0' && byte <= '9'; } @@ -43,149 +208,299 @@ enum object_parse_state { ObjectParseState_Value, }; -struct json_result json_parse(uint8_t *buf, uint64_t size) { - struct json_result res = { - .ok = true, - .result.document.type = Json_Null, +struct parser_state { + const uint8_t *buf; + uint64_t pos; + uint64_t len; + uint32_t line; + uint32_t col; +}; + +static struct json_result parse_string(struct parser_state *state, + struct json_value *parent) { + uint64_t start_pos = ++state->pos; /* ++ to skip start of string (") */ + bool literal = false; + while (state->pos < state->len && + (literal || state->buf[state->pos] != '"')) { + + // skip literal " escaped with \" + literal = state->buf[state->pos] == '\\'; + ++state->pos; + ++state->col; + } + + if (state->pos < state->len) { + uint64_t len = state->pos - start_pos; + + // skip over " + ++state->pos; + ++state->col; + + return (struct json_result){ + .ok = true, + .result.document = create_string(&state->buf[start_pos], len, parent), + }; + } + + return (struct json_result){ + .ok = false, + .result.error = "expected end of string, found EOF", }; +} - struct json_value *parent = NULL; - struct json_value *current = &res.result.document; - struct json_value tmp_key = {0}; - struct json_value tmp_val = {0}; - uint32_t line = 1, col = 0; - - enum object_parse_state obj_parse_state = ObjectParseState_Key; - for (uint64_t bufi = 0; bufi < size; ++bufi) { - uint8_t byte = buf[bufi]; - - // handle appends to the current scope - if (current->type == Json_Array) { - VEC_PUSH(¤t->value.array->values, tmp_val); - parent = current; - - // start looking for next value - tmp_val.type = Json_Null; - current = &tmp_val; - } else if (current->type == Json_Object && - obj_parse_state == ObjectParseState_Key) { - // key is in tmp_key, start looking for value - obj_parse_state = ObjectParseState_Value; - parent = current; - - tmp_val.type = Json_Null; - current = &tmp_val; - } else if (current->type == Json_Object && - obj_parse_state == ObjectParseState_Value) { - // value is in tmp_val - // TODO: remove this alloc, should not be needed - char *k = s8tocstr(tmp_key.value.string); - uint32_t hash = 0; - HASHMAP_INSERT(¤t->value.object->members, struct json_object_member, - k, tmp_val, hash); - (void)hash; - free(k); - - // start looking for next key - obj_parse_state = ObjectParseState_Key; - parent = current; - - tmp_key.type = Json_Null; - current = &tmp_key; +static struct json_result parse_number(struct parser_state *state, + struct json_value *parent) { + uint64_t start_pos = state->pos; + while (state->pos < state->len && + (is_number(state->buf[state->pos]) || state->buf[state->pos] == '-' || + state->buf[state->pos] == '.')) { + ++state->pos; + ++state->col; + } + + if (state->pos < state->len) { + uint64_t len = state->pos - start_pos; + char *nmbr = + s8tocstr((struct s8){.s = (uint8_t *)&state->buf[start_pos], .l = len}); + struct json_result res = { + .ok = true, + .result.document.type = Json_Number, + .result.document.value.number = atof(nmbr), + .result.document.parent = parent, + .result.document.start = &state->buf[start_pos], + .result.document.end = &state->buf[state->pos], + }; + free(nmbr); + return res; + } + + return (struct json_result){ + .ok = false, + .result.error = "expected end of number, found EOF", + }; +} + +static struct json_result parse_value(struct parser_state *state, + struct json_value *parent) { + uint8_t byte = state->buf[state->pos]; + switch (byte) { + case '"': + return parse_string(state, parent); + case 't': + state->pos += 4; + state->col += 4; + return (struct json_result){ + .ok = true, + .result.document.type = Json_Bool, + .result.document.start = &state->buf[state->pos - 4], + .result.document.end = &state->buf[state->pos], + .result.document.value.boolean = true, + .result.document.parent = parent, + }; + case 'f': + state->pos += 5; + state->col += 5; + return (struct json_result){ + .ok = true, + .result.document.type = Json_Bool, + .result.document.value.boolean = false, + .result.document.start = &state->buf[state->pos - 5], + .result.document.end = &state->buf[state->pos], + .result.document.parent = parent, + }; + case 'n': + state->pos += 4; + state->col += 4; + return (struct json_result){ + .ok = true, + .result.document.type = Json_Null, + .result.document.start = &state->buf[state->pos - 4], + .result.document.end = &state->buf[state->pos], + .result.document.parent = parent, + }; + default: + if (is_number(byte) || byte == '-' || byte == '.') { + return parse_number(state, parent); } + break; + } - switch (byte) { - case '[': - setarray(current); - parent = current; + return (struct json_result){ + .ok = false, + .result.error = format_error(state->line, state->col, "expected value"), + }; +} - tmp_val.type = Json_Null; - current = &tmp_val; - break; - case ']': - current = parent; - break; - case '{': - setobject(current); - obj_parse_state = ObjectParseState_Key; - parent = current; +struct json_value *insert(struct json_value *container, struct json_value *key_, + struct json_value *value) { + + struct json_value *inserted = NULL; + // where to put value? + if (container->type == Json_Object) { + // TODO: remove this alloc, should not be needed + char *k = s8tocstr(key_->value.string); + HASHMAP_APPEND(&container->value.object->members, struct json_object_member, + k, struct json_object_member * val); + + // TODO: duplicate key + if (val != NULL) { + inserted = &val->value.value; + val->value.value = *value; + val->value.key = s8dup(key_->value.string); + } - tmp_key.type = Json_Null; - current = &tmp_key; - break; + free(k); + } else if (container->type == Json_Array) { + VEC_APPEND(&container->value.array->values, struct json_value * val); + inserted = val; + *val = *value; + } else { // root + *container = *value; + inserted = container; + } + + return inserted; +} + +struct json_result json_parse(const uint8_t *buf, uint64_t size) { + + enum object_parse_state expected = ObjectParseState_Value; + struct parser_state state = { + .buf = buf, + .pos = 0, + .len = size, + .line = 1, + .col = 0, + }; + + struct json_value root = {0}, key = {0}, value = {0}; + struct json_value *container = &root; + + while (state.pos < state.len) { + switch (state.buf[state.pos]) { + case ',': + case ' ': + case ':': + case '\r': + case '\t': + ++state.col; + ++state.pos; + continue; + + case '\n': + ++state.line; + ++state.pos; + state.col = 0; + continue; + + case ']': case '}': - current = parent; - break; - case '"': - if (current->type == Json_String) { - // finish off the string - current->value.string.l = (buf + bufi) - current->value.string.s; - current = parent; - } else { - setstring(current, buf + bufi + 1 /* skip " */); + container->end = &state.buf[state.pos + 1]; + container = container->parent; + + if (container->type == Json_Object) { + expected = ObjectParseState_Key; } + + ++state.pos; + ++state.col; + continue; + + case '[': + value = create_array(container); + value.start = &state.buf[state.pos]; + ++state.pos; + ++state.col; break; - case '\n': - ++line; - col = 0; + case '{': + value = create_object(container); + value.start = &state.buf[state.pos]; + ++state.pos; + ++state.col; break; default: - if (current->type == Json_String) { - // append to string - } else if (current->type == Json_Number && - !(is_number(byte) || byte == '-' || byte == '.')) { - // end of number - current->value.string.l = (buf + bufi) - current->value.string.s; - char *nmbr = s8tocstr(current->value.string); - current->value.number = atof(nmbr); - free(nmbr); - - current = parent; - - } else if (current->type == Json_Null && - (is_number(byte) || byte == '-' || byte == '.')) { - // borrow string storage in the value for storing number - // as a string - setstring(current, buf + bufi); - current->type = Json_Number; - } else if (byte == 't') { - current->type = Json_Bool; - current->value.boolean = true; - - current = parent; - } else if (byte == 'f') { - current->type = Json_Bool; - current->value.boolean = false; - - current = parent; - } else if (byte == 'n') { - current->type = Json_Null; - - current = parent; + // parse out a value or a key + switch (expected) { + + case ObjectParseState_Key: { + if (container->type == Json_Object) { + struct json_result res = parse_string(&state, container); + + if (!res.ok) { + json_destroy(&root); + return res; + } + + key = res.result.document; + } + expected = ObjectParseState_Value; + // dont insert anything now, we still need a value + continue; + } + + case ObjectParseState_Value: { + struct json_result res = parse_value(&state, container); + + if (!res.ok) { + json_destroy(&root); + return res; + } + + value = res.result.document; + + if (container->type == Json_Object) { + expected = ObjectParseState_Key; + } + break; + } } break; } - // TODO: not entirely correct - ++col; + // insert the value we have created into the + // structure + struct json_value *inserted = insert(container, &key, &value); + + // did we insert a container? + // In this case, this is the current container and + // set the expectation for value or key correctly + // depending on the type + if (inserted != NULL && + (value.type == Json_Object || value.type == Json_Array)) { + container = inserted; + + if (value.type == Json_Object) { + expected = ObjectParseState_Key; + } else { + expected = ObjectParseState_Value; + } + } } - return res; + + return (struct json_result){ + .ok = true, + .result.document = root, + }; } void json_destroy(struct json_value *value) { switch (value->type) { - case Json_Array: + case Json_Array: { struct json_array *arr = value->value.array; VEC_FOR_EACH(&arr->values, struct json_value * val) { json_destroy(val); } VEC_DESTROY(&arr->values); - break; - case Json_Object: + free(arr); + } break; + case Json_Object: { struct json_object *obj = value->value.object; HASHMAP_FOR_EACH(&obj->members, struct json_object_member * memb) { - json_destroy(&memb->value); + s8delete(memb->value.key); + json_destroy(&memb->value.value); } HASHMAP_DESTROY(&obj->members); + free(obj); + } break; case Json_Null: case Json_Number: case Json_String: @@ -212,6 +527,8 @@ uint64_t json_len(struct json_object *obj) { return HASHMAP_SIZE(&obj->members); } +bool json_empty(struct json_object *obj) { return json_len(obj) == 0; } + bool json_contains(struct json_object *obj, struct s8 key) { // TODO: get rid of alloc char *k = s8tocstr(key); @@ -222,13 +539,45 @@ bool json_contains(struct json_object *obj, struct s8 key) { return res; } +void json_foreach(struct json_object *obj, + void (*cb)(struct s8, struct json_value *, void *), + void *userdata) { + HASHMAP_FOR_EACH(&obj->members, struct json_object_member * entry) { + cb(entry->value.key, &entry->value.value, userdata); + } +} + struct json_value *json_get(struct json_object *obj, struct s8 key) { // TODO: get rid of alloc char *k = s8tocstr(key); HASHMAP_GET(&obj->members, struct json_object_member, k, - struct json_value * result); + struct json_key_value * result); free(k); - return result; + return result != NULL ? &result->value : NULL; +} + +void json_set(struct json_object *obj, struct s8 key_, struct json_value val) { + // TODO: get rid of alloc + char *k = s8tocstr(key_); + uint32_t hash = 0; + + struct json_key_value v = { + .value = val, + .key = s8dup(key_), + }; + HASHMAP_INSERT(&obj->members, struct json_object_member, k, v, hash); + + (void)hash; + (void)key; + free(k); +} + +void json_array_foreach(struct json_array *arr, void *userdata, + void (*cb)(uint64_t, struct json_value *, void *)) { + + VEC_FOR_EACH_INDEXED(&arr->values, struct json_value * val, i) { + cb(i, val, userdata); + } } diff --git a/src/dged/json.h b/src/dged/json.h index c0428b9..7f64e31 100644 --- a/src/dged/json.h +++ b/src/dged/json.h @@ -7,9 +7,9 @@ #include "s8.h" enum json_type { + Json_Null = 0, Json_Array, Json_Object, - Json_Null, Json_Number, Json_String, Json_Bool, @@ -24,6 +24,10 @@ struct json_value { double number; bool boolean; } value; + struct json_value *parent; + + const uint8_t *start; + const uint8_t *end; }; struct json_result { @@ -34,21 +38,125 @@ struct json_result { } result; }; -struct json_writer; +/** + * Parse a json document from a string. + * + * @returns Structure describing the result of the parse + * operation. The member @ref ok, if true represents a + * successful parse, with the result in @ref result.document. + * If @ref ok is false, the parse operation has an error, + * and @ref result.error contains a descriptive error message. + */ +struct json_result json_parse(const uint8_t *buf, uint64_t size); -struct json_result json_parse(uint8_t *buf, uint64_t size); +/** + * Destroy a json value, returning all memory + * allocated for the structure. + * + * @param [in] value The json value to destroy. + */ void json_destroy(struct json_value *value); +/** + * Check if a JSON object is empty. + * + * @param [in] obj The JSON object to check if empty. + * + * @returns True if @ref obj is empty, false otherwise. + */ +bool json_empty(struct json_object *obj); + +/** + * Return the number of members in a JSON object. + * + * @param [in] obj The JSON object to get number of members for. + * + * @returns The number of members in @ref obj. + */ uint64_t json_len(struct json_object *obj); + +/** + * Test if the JSON object contains the specified key. + * + * @param [in] obj The JSON object to look for @ref key in. + * @param [in] key The key to search for. + * + * @returns True if @ref key exists in @ref obj, false otherwise. + */ bool json_contains(struct json_object *obj, struct s8 key); + +/** + * Iterate all key-value pairs in a JSON object. + * + * @param [in] obj The JSON object to iterate. + * @param [in] cb The callback to call for each kv-pair. + * @param [in] userdata Pointer that is sent unmodified to @ref cb. + */ +void json_foreach(struct json_object *obj, + void (*cb)(struct s8, struct json_value *, void *), + void *userdata); + +/** + * Get a value from a JSON object. + * + * @param [in] obj The JSON object to get from. + * @param [in] key The key of the value to get. + * + * @returns A pointer to the json value distinguished by @ref key, + * if it exists, NULL otherwise. + */ struct json_value *json_get(struct json_object *obj, struct s8 key); +/** + * Set a value in a JSON object. + * + * @param [in] obj The JSON object to set in. + * @param [in] key The key of the value to set. + * @param [in] value The JSON value to set. + */ +void json_set(struct json_object *obj, struct s8 key, struct json_value val); + +/** + * Get the length of a JSON array. + * + * @param [in] arr The array to get the length of + * + * @returns The length of @ref arr. + */ uint64_t json_array_len(struct json_array *arr); -void json_array_foreach(struct json_array *arr, - void (*cb)(uint64_t, struct json_value)); + +/** + * Iterate a JSON array. + * + * @param [in] arr The array to iterate. + * @param [in] userdata Pointer to user-defined data that is passed + to the callback. + * @param [in] cb The callback to invoke for each member in @ref arr. + */ +void json_array_foreach(struct json_array *arr, void *userdata, + void (*cb)(uint64_t, struct json_value *, void *)); + +/** + * Get a member from a JSON array by index. + * + * @param [in] arr The array to get from. + * @param [in] idx The index to get the value at. + * + * @returns A pointer to the value at @ref idx in @ref arr. If @ref idx + * is outside the array length, this returns NULL. + */ struct json_value *json_array_get(struct json_array *arr, uint64_t idx); -struct json_writer *json_writer_create(); -struct s8 json_writer_done(struct json_writer *writer); +/** + * Render a JSON value to a string. + * + * @param [in] val The json value to render to a string. + * + * @returns The JSON object rendered as a string. + */ +struct s8 json_value_to_string(const struct json_value *val); + +struct s8 unescape_json_string(struct s8 input); +struct s8 escape_json_string(struct s8 input); #endif diff --git a/src/dged/jsonrpc.c b/src/dged/jsonrpc.c new file mode 100644 index 0000000..215274f --- /dev/null +++ b/src/dged/jsonrpc.c @@ -0,0 +1,118 @@ +#include "jsonrpc.h" + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> + +struct jsonrpc_message jsonrpc_parse(const uint8_t *buf, uint64_t size) { + + struct json_result res = json_parse(buf, size); + if (!res.ok) { + return (struct jsonrpc_message){ + .type = Jsonrpc_Response, + .document = (struct json_value){.type = Json_Null, .parent = NULL}, + .message.response = (struct jsonrpc_response){ + .id = (struct json_value){.type = Json_Null}, + .ok = false, + .value.error = + (struct jsonrpc_error){ + .code = 0, + .message = s8(res.result.error), + }, + }}; + } + + struct json_value doc = res.result.document; + struct json_object *obj = doc.value.object; + + if (json_contains(obj, s8("error"))) { + struct json_object *err_obj = json_get(obj, s8("error"))->value.object; + return (struct jsonrpc_message){ + .type = Jsonrpc_Response, + .document = doc, + .message.response = + (struct jsonrpc_response){ + .id = *json_get(obj, s8("id")), + .ok = false, + .value.error = + (struct jsonrpc_error){ + .code = json_get(err_obj, s8("code"))->value.number, + .message = + json_get(err_obj, s8("message"))->value.string, + }, + }, + }; + } else if (!json_contains(obj, s8("id"))) { + // no id == notification + return (struct jsonrpc_message){ + .type = Jsonrpc_Notification, + .document = doc, + .message.notification = + (struct jsonrpc_notification){ + .method = json_get(obj, s8("method"))->value.string, + .params = *json_get(obj, s8("params")), + }, + }; + } else if (json_contains(obj, s8("method"))) { + // request + return (struct jsonrpc_message){ + .type = Jsonrpc_Request, + .document = doc, + .message.request = (struct jsonrpc_request){ + .id = *json_get(obj, s8("id")), + .method = json_get(obj, s8("method"))->value.string, + .params = *json_get(obj, s8("params")), + }}; + } + + // response + return (struct jsonrpc_message){ + .type = Jsonrpc_Response, + .document = doc, + .message.response = (struct jsonrpc_response){ + .id = *json_get(obj, s8("id")), + .ok = true, + .value.result = *json_get(obj, s8("result")), + }}; +} + +struct s8 jsonrpc_format_request(struct json_value id, struct s8 method, + struct s8 params) { + const char *fmt = "{ \"jsonrpc\": \"2.0\", \"id\": %d, \"method\": \"%.*s\", " + "\"params\": %.*s }"; + size_t s = snprintf(NULL, 0, fmt, (int)id.value.number, method.l, method.s, + params.l, params.s); + char *buf = calloc(s + 1, 1); + snprintf(buf, s + 1, fmt, (int)id.value.number, method.l, method.s, params.l, + params.s); + + return (struct s8){ + .s = (uint8_t *)buf, + .l = s, + }; +} + +struct s8 jsonrpc_format_response(struct json_value id, struct s8 result) { + const char *fmt = "{ \"jsonrpc\": \"2.0\", \"id\": %d, \"result\": %.*s }"; + size_t s = snprintf(NULL, 0, fmt, (int)id.value.number, result.l, result.s); + char *buf = calloc(s + 1, 1); + snprintf(buf, s + 1, fmt, (int)id.value.number, result.l, result.s); + + return (struct s8){ + .s = (uint8_t *)buf, + .l = s, + }; +} + +struct s8 jsonrpc_format_notification(struct s8 method, struct s8 params) { + const char *fmt = "{ \"jsonrpc\": \"2.0\", \"method\": \"%.*s\", " + "\"params\": %.*s }"; + size_t s = snprintf(NULL, 0, fmt, method.l, method.s, params.l, params.s); + char *buf = calloc(s + 1, 1); + snprintf(buf, s + 1, fmt, method.l, method.s, params.l, params.s); + + return (struct s8){ + .s = (uint8_t *)buf, + .l = s, + }; +} diff --git a/src/dged/jsonrpc.h b/src/dged/jsonrpc.h new file mode 100644 index 0000000..2ac2787 --- /dev/null +++ b/src/dged/jsonrpc.h @@ -0,0 +1,58 @@ +#ifndef _JSONRPC_H +#define _JSONRPC_H + +#include <stdint.h> + +#include "json.h" +#include "s8.h" + +enum jsonrpc_type { + Jsonrpc_Request, + Jsonrpc_Response, + Jsonrpc_Notification, +}; + +struct jsonrpc_request { + struct json_value id; + struct s8 method; + struct json_value params; +}; + +struct jsonrpc_error { + int code; + struct s8 message; + struct json_value data; +}; + +struct jsonrpc_notification { + struct s8 method; + struct json_value params; +}; + +struct jsonrpc_response { + struct json_value id; + bool ok; + union jsonrpc_value { + struct json_value result; + struct jsonrpc_error error; + } value; +}; + +struct jsonrpc_message { + enum jsonrpc_type type; + union jsonrpc_msg { + struct jsonrpc_request request; + struct jsonrpc_response response; + struct jsonrpc_notification notification; + } message; + + struct json_value document; +}; + +struct jsonrpc_message jsonrpc_parse(const uint8_t *buf, uint64_t size); +struct s8 jsonrpc_format_request(struct json_value id, struct s8 method, + struct s8 params); +struct s8 jsonrpc_format_response(struct json_value id, struct s8 result); +struct s8 jsonrpc_format_notification(struct s8 method, struct s8 params); + +#endif diff --git a/src/dged/keyboard.c b/src/dged/keyboard.c index 04565e0..5447947 100644 --- a/src/dged/keyboard.c +++ b/src/dged/keyboard.c @@ -124,7 +124,7 @@ struct keyboard_update keyboard_update(struct keyboard *kbd, const uint32_t bufsize = 1024; uint8_t *buf = malloc(bufsize), *writepos = buf; int nbytes = 0, nread = 0; - while ((nread = read(kbd->fd, writepos, bufsize)) == bufsize) { + while ((nread = read(kbd->fd, writepos, bufsize)) == (int)bufsize) { nbytes += bufsize; buf = realloc(buf, nbytes + bufsize); writepos = buf + nbytes; diff --git a/src/dged/lsp.c b/src/dged/lsp.c index 3c699f4..dae0603 100644 --- a/src/dged/lsp.c +++ b/src/dged/lsp.c @@ -1,29 +1,55 @@ #include "lsp.h" #include <assert.h> +#include <errno.h> +#include <fcntl.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include "buffer.h" +#include "bufread.h" +#include "jsonrpc.h" #include "process.h" #include "reactor.h" +struct pending_write { + char headers[256]; + uint64_t headers_len; + uint64_t written; + struct s8 payload; +}; + +typedef VEC(struct pending_write) write_vec; + +enum read_state { + Read_Headers, + Read_Payload, +}; + struct lsp { const char *name; char *const *command; struct process *process; struct reactor *reactor; struct buffer *stderr_buffer; - struct lsp_client client_impl; uint32_t stdin_event; uint32_t stdout_event; uint32_t stderr_event; + + write_vec writes; + + enum read_state read_state; + struct bufread *reader; + uint8_t header_buffer[4096]; + size_t header_len; + size_t content_len; + size_t curr_content_len; + uint8_t *reader_buffer; }; struct lsp *lsp_create(char *const command[], struct reactor *reactor, - struct buffer *stderr_buffer, - struct lsp_client client_impl, const char *name) { + struct buffer *stderr_buffer, const char *name) { // check length of command if (command == NULL) { return NULL; @@ -60,12 +86,15 @@ struct lsp *lsp_create(char *const command[], struct reactor *reactor, } } lsp->stderr_buffer = stderr_buffer; - lsp->client_impl = client_impl; lsp->reactor = reactor; lsp->stdin_event = -1; lsp->stdout_event = -1; lsp->stderr_event = -1; + lsp->reader = NULL; + lsp->read_state = Read_Headers; + lsp->curr_content_len = 0; + VEC_INIT(&lsp->writes, 64); return lsp; } @@ -74,28 +103,131 @@ void lsp_destroy(struct lsp *lsp) { if (lsp->process != NULL) { free(lsp->process); } - if (lsp->command != NULL) { - char *command = lsp->command[0]; - while (command != NULL) { - free(command); - ++command; - } + if (lsp->command != NULL) { free((void *)lsp->command); } + + VEC_DESTROY(&lsp->writes); + free(lsp); } -uint32_t lsp_update(struct lsp *lsp, struct lsp_response **responses, - uint32_t responses_capacity) { +static bool read_headers(struct lsp *lsp) { + bool prev_was_cr = false; + while (true) { + uint8_t b; + ssize_t res = bufread_read(lsp->reader, &b, 1); + + if (res == -1 || res == 0) { + return false; + } + + if (b == '\n' && prev_was_cr && lsp->header_len == 0) { + // end of headers + lsp->reader_buffer = calloc(lsp->content_len, 1); + lsp->curr_content_len = 0; + lsp->read_state = Read_Payload; + + return true; + } else if (b == '\n' && prev_was_cr) { + // end of individual header + lsp->header_buffer[lsp->header_len] = '\0'; + + if (lsp->header_len > 15 && + memcmp(lsp->header_buffer, "Content-Length:", 15) == 0) { + lsp->content_len = atoi((const char *)&lsp->header_buffer[16]); + } + + lsp->header_len = 0; + + continue; + } + + prev_was_cr = false; + if (b == '\r') { + prev_was_cr = true; + continue; + } + + // TODO: handle this case + if (lsp->header_len < 4096) { + lsp->header_buffer[lsp->header_len] = b; + ++lsp->header_len; + } + } +} + +static bool read_payload(struct lsp *lsp) { + ssize_t res = + bufread_read(lsp->reader, &lsp->reader_buffer[lsp->curr_content_len], + lsp->content_len - lsp->curr_content_len); + + if (res == -1) { + return false; + } else if (res == 0) { + return false; + } + + lsp->curr_content_len += res; + return lsp->curr_content_len == lsp->content_len; +} + +static void init_lsp_message(struct lsp_message *lsp_msg, uint8_t *payload, + size_t len) { + + lsp_msg->jsonrpc_msg = jsonrpc_parse(payload, len); + lsp_msg->parsed = true; // this is parsed to json + lsp_msg->payload.s = payload; + lsp_msg->payload.l = len; + + switch (lsp_msg->jsonrpc_msg.type) { + case Jsonrpc_Request: { + lsp_msg->type = Lsp_Request; + struct jsonrpc_request *jreq = &lsp_msg->jsonrpc_msg.message.request; + struct lsp_request *lreq = &lsp_msg->message.request; - (void)responses; - (void)responses_capacity; + lreq->id = (request_id)jreq->id.value.number; + lreq->method = jreq->method; + lreq->params = jreq->params; + } break; + case Jsonrpc_Response: { + lsp_msg->type = Lsp_Response; + struct jsonrpc_response *jresp = &lsp_msg->jsonrpc_msg.message.response; + struct lsp_response *lresp = &lsp_msg->message.response; + + lresp->id = (request_id)jresp->id.value.number; + lresp->ok = jresp->ok; + if (lresp->ok) { + lresp->value.result = jresp->value.result; + } else { + lresp->value.error.code = jresp->value.error.code; + lresp->value.error.message = jresp->value.error.message; + lresp->value.error.data = jresp->value.error.data; + } + } break; + + case Jsonrpc_Notification: { + lsp_msg->type = Lsp_Notification; + struct jsonrpc_notification *jnot = + &lsp_msg->jsonrpc_msg.message.notification; + struct lsp_notification *lnot = &lsp_msg->message.notification; + + lnot->method = jnot->method; + lnot->params = jnot->params; + } break; + } +} + +uint32_t lsp_update(struct lsp *lsp, struct lsp_message *msgs, + uint32_t nmax_msgs) { if (!lsp_server_running(lsp)) { return -1; } + uint32_t nmsgs = 0; + // read stderr if (lsp->stderr_event != (uint32_t)-1) { uint8_t buf[1024]; @@ -109,7 +241,105 @@ uint32_t lsp_update(struct lsp *lsp, struct lsp_response **responses, } } - return 0; + // write pending requests + if (reactor_poll_event(lsp->reactor, lsp->stdin_event)) { + VEC_FOR_EACH(&lsp->writes, struct pending_write * w) { + ssize_t written = 0; + ssize_t to_write = 0; + + // write headers first + if (w->written < w->headers_len) { + to_write = w->headers_len - w->written; + written = write(lsp->process->stdin, w->headers + w->written, to_write); + } + + // did an error occur + if (written < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + // TODO: log error somehow + } + goto cleanup_writes; + } else { + w->written += written; + } + + // write content next + if (w->written >= w->headers_len) { + to_write = w->payload.l + w->headers_len - w->written; + size_t offset = w->written - w->headers_len; + written = write(lsp->process->stdin, w->payload.s + offset, to_write); + } + + // did an error occur + if (written < 0) { + if (errno != EAGAIN && errno != EWOULDBLOCK) { + // TODO: log error somehow + } + goto cleanup_writes; + } else { + w->written += written; + } + } + } + +cleanup_writes: + /* lsp->writes = filter(&lsp->writes, x: x.written < x.payload.l + + * x.headers_len) */ + if (!VEC_EMPTY(&lsp->writes)) { + write_vec writes = lsp->writes; + VEC_INIT(&lsp->writes, VEC_CAPACITY(&writes)); + + VEC_FOR_EACH(&writes, struct pending_write * w) { + if (w->written < w->payload.l + w->headers_len) { + // copying 256 bytes, goodbye vaccuum tubes... + VEC_PUSH(&lsp->writes, *w); + } else { + s8delete(w->payload); + } + } + VEC_DESTROY(&writes); + } + + if (VEC_EMPTY(&lsp->writes)) { + reactor_unregister_interest(lsp->reactor, lsp->stdin_event); + lsp->stdin_event = (uint32_t)-1; + } + + // process incoming messages + // TODO: handle the case where we might leave data + if (reactor_poll_event(lsp->reactor, lsp->stdout_event)) { + bool has_data = true; + while (has_data) { + switch (lsp->read_state) { + case Read_Headers: + has_data = read_headers(lsp); + break; + case Read_Payload: { + bool payload_ready = read_payload(lsp); + if (payload_ready) { + if (nmsgs == nmax_msgs) { + return nmsgs; + } + + init_lsp_message(&msgs[nmsgs], lsp->reader_buffer, lsp->content_len); + ++nmsgs; + + // set up for next message + lsp->reader_buffer = NULL; + lsp->content_len = 0; + lsp->read_state = Read_Headers; + } + + // it only returns if we are out of data or if + // the payload is ready + has_data = payload_ready; + break; + } + } + } + } + + return nmsgs; } int lsp_start_server(struct lsp *lsp) { @@ -123,33 +353,61 @@ int lsp_start_server(struct lsp *lsp) { lsp->process = calloc(1, sizeof(struct process)); memcpy(lsp->process, &p, sizeof(struct process)); + + lsp->stdout_event = reactor_register_interest( + lsp->reactor, lsp->process->stdout, ReadInterest); + + if (lsp->stdout_event == (uint32_t)-1) { + return -3; + } + lsp->stderr_event = reactor_register_interest( lsp->reactor, lsp->process->stderr, ReadInterest); + lsp->reader = bufread_create(lsp->process->stdout, 8192); + return 0; } int lsp_restart_server(struct lsp *lsp) { - if (lsp_server_running(lsp)) { - lsp_stop_server(lsp); - } - + lsp_stop_server(lsp); return lsp_start_server(lsp); } void lsp_stop_server(struct lsp *lsp) { - process_kill(lsp->process); - process_destroy(lsp->process); - free(lsp->process); - lsp->process = NULL; -} + if (lsp->stderr_event != (uint32_t)-1) { + reactor_unregister_interest(lsp->reactor, lsp->stderr_event); + lsp->stderr_event = (uint32_t)-1; + } -bool lsp_server_running(const struct lsp *lsp) { - if (lsp->process == NULL) { - return false; + if (lsp->stdin_event != (uint32_t)-1) { + reactor_unregister_interest(lsp->reactor, lsp->stdin_event); + lsp->stdin_event = (uint32_t)-1; + } + + if (lsp->stdout_event != (uint32_t)-1) { + reactor_unregister_interest(lsp->reactor, lsp->stdout_event); + lsp->stdout_event = (uint32_t)-1; + } + + if (lsp_server_running(lsp)) { + process_kill(lsp->process); + } + + if (lsp->process != NULL) { + process_destroy(lsp->process); + free(lsp->process); + lsp->process = NULL; } - return process_running(lsp->process); + if (lsp->reader != NULL) { + bufread_destroy(lsp->reader); + lsp->reader = NULL; + } +} + +bool lsp_server_running(const struct lsp *lsp) { + return lsp->process != NULL ? process_running(lsp->process) : false; } uint64_t lsp_server_pid(const struct lsp *lsp) { @@ -161,3 +419,75 @@ uint64_t lsp_server_pid(const struct lsp *lsp) { } const char *lsp_server_name(const struct lsp *lsp) { return lsp->name; } + +struct lsp_message lsp_create_request(request_id id, struct s8 method, + struct s8 payload) { + struct lsp_message msg = { + .type = Lsp_Request, + .parsed = false, // payload is raw + .message.request.method = method, + .message.request.id = id, + .payload = jsonrpc_format_request( + (struct json_value){ + .type = Json_Number, + .value.number = (double)id, + .parent = NULL, + }, + method, payload.l > 0 ? payload : s8("{}")), + }; + + return msg; +} + +struct lsp_message lsp_create_response(request_id id, bool ok, + struct s8 payload) { + struct lsp_message msg = { + .type = Lsp_Response, + .parsed = false, // payload is raw + .message.response.ok = ok, + .message.response.id = id, + .payload = jsonrpc_format_response( + (struct json_value){ + .type = Json_Number, + .value.number = (double)id, + .parent = NULL, + }, + payload.l > 0 ? payload : s8("{}")), + }; + + return msg; +} + +struct lsp_message lsp_create_notification(struct s8 method, + struct s8 payload) { + struct lsp_message msg = { + .type = Lsp_Notification, + .parsed = false, // payload is raw + .message.notification.method = method, + .payload = jsonrpc_format_notification(method, payload.l > 0 ? payload + : s8("{}")), + }; + + return msg; +} + +void lsp_send(struct lsp *lsp, struct lsp_message message) { + + VEC_APPEND(&lsp->writes, struct pending_write * w); + w->headers_len = snprintf(w->headers, 256, "Content-Length: %d\r\n\r\n", + message.payload.l); + w->payload = message.payload; + w->written = 0; + + if (lsp->stdin_event == (uint32_t)-1) { + lsp->stdin_event = reactor_register_interest( + lsp->reactor, lsp->process->stdin, WriteInterest); + } +} + +void lsp_message_destroy(struct lsp_message *message) { + if (message->parsed) { + json_destroy(&message->jsonrpc_msg.document); + } + s8delete(message->payload); +} diff --git a/src/dged/lsp.h b/src/dged/lsp.h index 3fd6285..32d00bc 100644 --- a/src/dged/lsp.h +++ b/src/dged/lsp.h @@ -1,6 +1,10 @@ #ifndef _LSP_H #define _LSP_H +#include <stddef.h> + +#include "json.h" +#include "jsonrpc.h" #include "location.h" #include "s8.h" @@ -8,55 +12,59 @@ struct buffer; struct lsp; struct reactor; -typedef uint32_t request_id; - -struct lsp_response { - request_id id; - bool ok; - union payload_data { - void *result; - struct s8 error; - } payload; -}; +typedef uint64_t request_id; -struct lsp_notification { - int something; +struct lsp_response_error { + int code; + struct s8 message; + struct json_value data; }; -struct lsp_client { - void (*log_message)(int type, struct s8 msg); +enum lsp_message_type { + Lsp_Notification, + Lsp_Request, + Lsp_Response, }; -struct hover { - struct s8 contents; +struct lsp_response { + request_id id; - bool has_range; - struct region *range; + bool ok; + union data { + struct json_value result; + struct lsp_response_error error; + } value; }; -struct text_doc_item { - struct s8 uri; - struct s8 language_id; - uint32_t version; - struct s8 text; +struct lsp_request { + request_id id; + struct s8 method; + struct json_value params; }; -struct text_doc_position { - struct s8 uri; - struct location pos; +struct lsp_notification { + struct s8 method; + struct json_value params; }; -struct initialize_params { - struct s8 client_name; - struct s8 client_version; +struct lsp_message { + enum lsp_message_type type; + bool parsed; + union message_data { + struct lsp_response response; + struct lsp_request request; + struct lsp_notification notification; + } message; + + struct s8 payload; + struct jsonrpc_message jsonrpc_msg; }; // lifecycle functions struct lsp *lsp_create(char *const command[], struct reactor *reactor, - struct buffer *stderr_buffer, - struct lsp_client client_impl, const char *name); -uint32_t lsp_update(struct lsp *lsp, struct lsp_response **responses, - uint32_t responses_capacity); + struct buffer *stderr_buffer, const char *name); +uint32_t lsp_update(struct lsp *lsp, struct lsp_message *msgs, + uint32_t nmax_msgs); void lsp_destroy(struct lsp *lsp); // process control functions @@ -67,9 +75,16 @@ bool lsp_server_running(const struct lsp *lsp); uint64_t lsp_server_pid(const struct lsp *lsp); const char *lsp_server_name(const struct lsp *lsp); +// lsp message creation +struct lsp_message lsp_create_request(request_id id, struct s8 method, + struct s8 payload); +struct lsp_message lsp_create_notification(struct s8 method, struct s8 payload); +struct lsp_message lsp_create_response(request_id id, bool ok, + struct s8 payload); + +void lsp_message_destroy(struct lsp_message *message); + // protocol functions -void lsp_initialize(struct lsp *lsp, struct initialize_params); -void lsp_did_open_document(struct lsp *lsp, struct text_doc_item document); -request_id lsp_hover(struct lsp *lsp, struct text_doc_position); +void lsp_send(struct lsp *lsp, struct lsp_message message); #endif diff --git a/src/dged/minibuffer.c b/src/dged/minibuffer.c index c74a900..59417ab 100644 --- a/src/dged/minibuffer.c +++ b/src/dged/minibuffer.c @@ -11,17 +11,28 @@ #include <stdlib.h> #include <string.h> +struct prompt_key { + char key[16]; + char name[128]; +}; + static struct minibuffer { struct buffer *buffer; struct timespec expires; char prompt[128]; + struct prompt_key prompt_keys[16]; + uint32_t nprompt_keys; + struct command_ctx prompt_command_ctx; bool prompt_active; + struct window *prev_window; struct buffer *message_buffer; + struct timespec created_at; + } g_minibuffer = {0}; uint32_t minibuffer_draw_prompt(struct command_list *commands) { @@ -34,7 +45,31 @@ uint32_t minibuffer_draw_prompt(struct command_list *commands) { command_list_draw_text(commands, 0, 0, (uint8_t *)g_minibuffer.prompt, len); command_list_reset_color(commands); - return len; + uint32_t xoffset = len; + for (uint32_t i = 0; i < g_minibuffer.nprompt_keys; ++i) { + struct prompt_key *pk = &g_minibuffer.prompt_keys[i]; + + command_list_set_index_color_fg(commands, Color_Green); + size_t keylen = strlen(pk->key); + command_list_draw_text_copy(commands, xoffset, 0, (uint8_t *)pk->key, + keylen); + command_list_reset_color(commands); + + xoffset += keylen; + + command_list_draw_text(commands, xoffset, 0, (uint8_t *)" -> ", 4); + xoffset += 4; + + command_list_set_index_color_fg(commands, Color_Magenta); + size_t namelen = strlen(pk->name); + command_list_draw_text_copy(commands, xoffset, 0, (uint8_t *)pk->name, + namelen); + command_list_reset_color(commands); + + xoffset += namelen + 1; + } + + return xoffset; } static void minibuffer_abort_prompt_internal(bool clear); @@ -89,6 +124,27 @@ void update(struct buffer *buffer, void *userdata) { } } +static void print_message(const char *buff, size_t len) { + if (g_minibuffer.message_buffer == NULL) { + return; + } + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + + uint64_t elapsed = (((uint64_t)ts.tv_sec * 1e9 + (uint64_t)ts.tv_nsec) - + ((uint64_t)g_minibuffer.created_at.tv_sec * 1e9 + + (uint64_t)g_minibuffer.created_at.tv_nsec)) / + 1e6; + + struct s8 timestamp = s8from_fmt("%d: ", elapsed); + struct location at = buffer_add(g_minibuffer.message_buffer, + buffer_end(g_minibuffer.message_buffer), + timestamp.s, timestamp.l); + s8delete(timestamp); + + buffer_add(g_minibuffer.message_buffer, at, (uint8_t *)buff, len); +} + void minibuffer_init(struct buffer *buffer, struct buffers *buffers) { if (g_minibuffer.buffer != NULL) { return; @@ -98,13 +154,17 @@ void minibuffer_init(struct buffer *buffer, struct buffers *buffers) { g_minibuffer.expires.tv_sec = 0; g_minibuffer.expires.tv_nsec = 0; g_minibuffer.prompt_active = false; + g_minibuffer.nprompt_keys = 0; buffer_add_update_hook(g_minibuffer.buffer, update, &g_minibuffer); g_minibuffer.message_buffer = buffers_add(buffers, buffer_create("*messages*")); + + clock_gettime(CLOCK_MONOTONIC, &g_minibuffer.created_at); } -void echo(uint32_t timeout, const char *fmt, va_list args) { +static void echo(uint32_t timeout, const char *fmt, va_list args, + bool message) { if (g_minibuffer.prompt_active || g_minibuffer.buffer == NULL) { return; } @@ -120,11 +180,8 @@ void echo(uint32_t timeout, const char *fmt, va_list args) { buffer_set_text(g_minibuffer.buffer, (uint8_t *)buff, nbytes > 2048 ? 2048 : nbytes); - // we can get messages before this is set up - if (g_minibuffer.message_buffer != NULL) { - buffer_add(g_minibuffer.message_buffer, - buffer_end(g_minibuffer.message_buffer), (uint8_t *)buff, - nbytes > 2048 ? 2048 : nbytes); + if (message) { + print_message(buff, nbytes > 2048 ? 2048 : nbytes); } } @@ -139,10 +196,7 @@ void message(const char *fmt, ...) { static char buff[2048]; size_t nbytes = vsnprintf(buff, 2048, fmt, args); va_end(args); - - buffer_add(g_minibuffer.message_buffer, - buffer_end(g_minibuffer.message_buffer), (uint8_t *)buff, - nbytes > 2048 ? 2048 : nbytes); + print_message(buff, nbytes > 2048 ? 2048 : nbytes); } void minibuffer_destroy(void) { @@ -158,14 +212,28 @@ struct buffer *minibuffer_buffer(void) { return g_minibuffer.buffer; } void minibuffer_echo(const char *fmt, ...) { va_list args; va_start(args, fmt); - echo(1000, fmt, args); + echo(1000, fmt, args, true); va_end(args); } void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...) { va_list args; va_start(args, fmt); - echo(timeout, fmt, args); + echo(timeout, fmt, args, true); + va_end(args); +} + +void minibuffer_display(const char *fmt, ...) { + va_list args; + va_start(args, fmt); + echo(1000, fmt, args, false); + va_end(args); +} + +void minibuffer_display_timeout(uint32_t timeout, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + echo(timeout, fmt, args, false); va_end(args); } @@ -226,6 +294,50 @@ int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, return 0; } +int32_t minibuffer_keymap_prompt(struct command_ctx command_ctx, + const char *fmt, struct keymap *keys, ...) { + if (g_minibuffer.buffer == NULL) { + return 1; + } + + for (uint32_t i = 0; i < keys->nbindings; ++i) { + struct prompt_key *pk = &g_minibuffer.prompt_keys[i]; + struct binding *bind = &keys->bindings[i]; + key_name(&bind->key, pk->key, 16); + + switch (bind->type) { + case BindingType_Command: + // FIXME: this is not awesome + memcpy(pk->name, "<cmd>", 5); + pk->name[5] = '\0'; + break; + case BindingType_Keymap: + memcpy(pk->name, "<map>", 5); + pk->name[5] = '\0'; + break; + case BindingType_DirectCommand: { + const char *n = bind->data.direct_command->name; + size_t l = strlen(n); + if (l > 0) { + l = l > 127 ? 127 : l; + memcpy(pk->name, n, l); + pk->name[l] = '\0'; + } + } break; + } + } + g_minibuffer.nprompt_keys = keys->nbindings; + + minibuffer_setup(command_ctx, NULL); + + va_list args; + va_start(args, keys); + minibuffer_set_prompt_internal(fmt, args); + va_end(args); + + return 0; +} + void minibuffer_set_prompt(const char *fmt, ...) { va_list args; va_start(args, fmt); @@ -243,6 +355,7 @@ static void minibuffer_abort_prompt_internal(bool clear) { } g_minibuffer.prompt_active = false; + g_minibuffer.nprompt_keys = 0; } void minibuffer_abort_prompt(void) { minibuffer_abort_prompt_internal(true); } diff --git a/src/dged/minibuffer.h b/src/dged/minibuffer.h index 0b98904..9f16dfc 100644 --- a/src/dged/minibuffer.h +++ b/src/dged/minibuffer.h @@ -51,6 +51,25 @@ void minibuffer_echo(const char *fmt, ...); void minibuffer_echo_timeout(uint32_t timeout, const char *fmt, ...); /** + * Echo a message to the minibuffer without saving it in the message buffer. + * + * @param fmt Format string for the message. + * @param ... Format arguments. + */ +void minibuffer_display(const char *fmt, ...); + +/** + * Echo a message to the minibuffer that disappears after @p timeout + * without saving it in the message buffer. + * + * @param timeout The timeout in seconds after which the message should + * disappear. + * @param fmt Format string for the message. + * @param ... Format arguments. + */ +void minibuffer_display_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 @@ -66,6 +85,9 @@ int32_t minibuffer_prompt(struct command_ctx command_ctx, const char *fmt, ...); int32_t minibuffer_prompt_initial(struct command_ctx command_ctx, const char *initial, const char *fmt, ...); +int32_t minibuffer_keymap_prompt(struct command_ctx command_ctx, + const char *fmt, struct keymap *keys, ...); + void minibuffer_set_prompt(const char *fmt, ...); uint32_t minibuffer_draw_prompt(struct command_list *commands); diff --git a/src/dged/path.c b/src/dged/path.c index 735ef0c..d8422f0 100644 --- a/src/dged/path.c +++ b/src/dged/path.c @@ -1,4 +1,5 @@ #include "path.h" +#include "unistd.h" #include <limits.h> #include <stdint.h> @@ -32,7 +33,37 @@ char *expanduser(const char *path) { } char *to_abspath(const char *path) { + if (strlen(path) > 0 && path[0] == '/') { + return strdup(path); + } + char *exp = expanduser(path); + if (access(path, F_OK) == -1) { + // anchor to cwd + const char *cwd = getcwd(NULL, 0); + if (cwd == NULL) { + return strdup(path); + } + + size_t cwdlen = strlen(cwd); + size_t pathlen = strlen(path); + size_t len = cwdlen + pathlen + (pathlen > 0 ? 2 : 1); + char *ret = calloc(len, sizeof(char)); + memcpy(ret, cwd, cwdlen); + + if (pathlen > 0) { + ret[cwdlen] = '/'; + memcpy(ret + cwdlen + 1, path, pathlen); + } + + ret[len - 1] = '\0'; + + free((void *)cwd); + free(exp); + + return ret; + } + char *p = realpath(path, NULL); if (p != NULL) { free(exp); diff --git a/src/dged/process-posix.c b/src/dged/process-posix.c index 94ceb5f..7cfb29b 100644 --- a/src/dged/process-posix.c +++ b/src/dged/process-posix.c @@ -117,7 +117,11 @@ struct process_create_result process_create(char *const command[], }; } -void process_destroy(struct process *p) { (void)p; } +void process_destroy(struct process *p) { + close(p->stdin); + close(p->stdout); + close(p->stderr); +} bool process_running(const struct process *p) { return waitpid(p->id, NULL, WNOHANG) == 0; diff --git a/src/dged/s8.c b/src/dged/s8.c index 71b6c6d..e641b11 100644 --- a/src/dged/s8.c +++ b/src/dged/s8.c @@ -1,17 +1,66 @@ #include "s8.h" +#include <ctype.h> +#include <stdarg.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> +struct s8 s8new(const char *s, uint32_t len) { + uint8_t *mem = calloc(len, 1); + memcpy(mem, s, len); + return (struct s8){ + .s = mem, + .l = len, + }; +} + +void s8delete(struct s8 s) { + if (s.s != NULL) { + free(s.s); + } + s.l = 0; + s.s = NULL; +} + +struct s8 s8from_fmt(const char *fmt, ...) { + va_list args; + + va_start(args, fmt); + ssize_t len = vsnprintf(NULL, 0, fmt, args); + va_end(args); + + if (len == -1) { + return (struct s8){ + .s = NULL, + .l = 0, + }; + } + + char *buf = calloc(len + 1, 1); + + va_list args2; + va_start(args2, fmt); + vsnprintf(buf, len + 1, fmt, args2); + va_end(args2); + + return (struct s8){ + .s = (uint8_t *)buf, + .l = len, + }; +} + bool s8eq(struct s8 s1, struct s8 s2) { return s1.l == s2.l && memcmp(s1.s, s2.s, s1.l) == 0; } int s8cmp(struct s8 s1, struct s8 s2) { if (s1.l < s2.l) { - return memcmp(s1.s, s2.s, s1.l); + int res = memcmp(s1.s, s2.s, s1.l); + return res == 0 ? -s2.s[s1.l] : res; } else if (s2.l < s1.l) { - return memcmp(s1.s, s2.s, s2.l); + int res = memcmp(s1.s, s2.s, s2.l); + return res == 0 ? s1.s[s2.l] : res; } return memcmp(s1.s, s2.s, s1.l); @@ -25,13 +74,22 @@ char *s8tocstr(struct s8 s) { } bool s8startswith(struct s8 s, struct s8 prefix) { - if (prefix.l > s.l) { + if (prefix.l == 0 || prefix.l > s.l) { return false; } return memcmp(s.s, prefix.s, prefix.l) == 0; } +bool s8endswith(struct s8 s, struct s8 suffix) { + if (suffix.l > s.l) { + return false; + } + + size_t ldiff = s.l - suffix.l; + return memcmp(s.s + ldiff, suffix.s, suffix.l) == 0; +} + struct s8 s8dup(struct s8 s) { struct s8 new = {0}; new.l = s.l; @@ -41,3 +99,15 @@ struct s8 s8dup(struct s8 s) { return new; } + +bool s8empty(struct s8 s) { return s.s == NULL || s.l == 0; } + +bool s8onlyws(struct s8 s) { + for (size_t i = 0; i < s.l; ++i) { + if (!isspace(s.s[i])) { + return false; + } + } + + return true; +} diff --git a/src/dged/s8.h b/src/dged/s8.h index 5a2504e..fe0f5b7 100644 --- a/src/dged/s8.h +++ b/src/dged/s8.h @@ -3,18 +3,26 @@ #include <stdbool.h> #include <stdint.h> - -#define s8(s) ((struct s8){(uint8_t *)s, strlen(s)}) +#include <string.h> struct s8 { uint8_t *s; uint32_t l; }; +#define s8(s) ((struct s8){(uint8_t *)s, strlen(s)}) + +struct s8 s8new(const char *s, uint32_t len); +void s8delete(struct s8 s); +struct s8 s8from_fmt(const char *fmt, ...); +char *s8tocstr(struct s8 s); + bool s8eq(struct s8 s1, struct s8 s2); int s8cmp(struct s8 s1, struct s8 s2); -char *s8tocstr(struct s8 s); bool s8startswith(struct s8 s, struct s8 prefix); +bool s8endswith(struct s8 s, struct s8 suffix); struct s8 s8dup(struct s8 s); +bool s8empty(struct s8 s); +bool s8onlyws(struct s8 s); #endif diff --git a/src/dged/syntax.c b/src/dged/syntax.c index 5d9aeaa..0ffa8d4 100644 --- a/src/dged/syntax.c +++ b/src/dged/syntax.c @@ -2,7 +2,6 @@ #include <ctype.h> #include <dlfcn.h> -#include <errno.h> #include <fcntl.h> #include <regex.h> #include <string.h> @@ -14,7 +13,6 @@ #include "buffer.h" #include "display.h" -#include "hash.h" #include "minibuffer.h" #include "path.h" #include "s8.h" @@ -33,6 +31,7 @@ struct predicate { bool (*eval)(struct s8, uint32_t, struct s8[], struct s8, void *); uint32_t argc; + struct s8 argv[32]; void *data; @@ -332,9 +331,8 @@ static bool eval_predicates(struct highlight *h, struct text *text, #define match_cname(cname, capture) \ (s8eq(cname, s8(capture)) || s8startswith(cname, s8(capture "."))) -static void update_parser(struct buffer *buffer, void *userdata, - struct location origin, uint32_t width, - uint32_t height) { +static void update_parser(struct buffer *buffer, struct location origin, + uint32_t width, uint32_t height, void *userdata) { (void)width; @@ -406,7 +404,8 @@ static void update_parser(struct buffer *buffer, void *userdata, highlight = false; } else if (match_cname(cname, "label")) { highlight = false; - } else if (match_cname(cname, "type")) { + } else if (match_cname(cname, "type") || + match_cname(cname, "constructor")) { highlight = true; color = Color_Cyan; } else if (match_cname(cname, "variable")) { diff --git a/src/dged/text.c b/src/dged/text.c index 18ab04f..e609557 100644 --- a/src/dged/text.c +++ b/src/dged/text.c @@ -20,10 +20,13 @@ struct line { uint32_t nbytes; }; -struct text_property_entry { - struct location start; - struct location end; - struct text_property property; +typedef VEC(struct text_property) property_vec; + +#define MAX_LAYERS 16 + +struct property_layer { + layer_id id; + property_vec properties; }; struct text { @@ -31,7 +34,10 @@ struct text { struct line *lines; uint32_t nlines; uint32_t capacity; - VEC(struct text_property_entry) properties; + property_vec properties; + struct property_layer property_layers[MAX_LAYERS]; + uint32_t nproperty_layers; + layer_id current_layer_id; }; struct text *text_create(uint32_t initial_capacity) { @@ -39,6 +45,7 @@ struct text *text_create(uint32_t initial_capacity) { txt->lines = calloc(initial_capacity, sizeof(struct line)); txt->capacity = initial_capacity; txt->nlines = 0; + txt->current_layer_id = 1; VEC_INIT(&txt->properties, 32); @@ -48,6 +55,10 @@ struct text *text_create(uint32_t initial_capacity) { void text_destroy(struct text *text) { VEC_DESTROY(&text->properties); + for (size_t i = 0; i < text->nproperty_layers; ++i) { + VEC_DESTROY(&text->property_layers[i].properties); + } + for (uint32_t li = 0; li < text->nlines; ++li) { free(text->lines[li].data); text->lines[li].data = NULL; @@ -364,6 +375,15 @@ void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, } struct text_chunk text_get_line(struct text *text, uint32_t line) { + if (line >= text_num_lines(text)) { + return (struct text_chunk){ + .text = NULL, + .nbytes = 0, + .line = line, + .allocated = false, + }; + } + struct line *src_line = &text->lines[line]; return (struct text_chunk){ .text = src_line->data, @@ -453,15 +473,41 @@ struct text_chunk text_get_region(struct text *text, uint32_t start_line, }; } +static property_vec *find_property_layer(struct text *text, layer_id layer) { + if (layer == PropertyLayer_Default) { + return &text->properties; + } + + for (size_t i = 0; i < text->nproperty_layers; ++i) { + if (text->property_layers[i].id == layer) { + return &text->property_layers[i].properties; + } + } + + return NULL; +} + void text_add_property(struct text *text, uint32_t start_line, uint32_t start_offset, uint32_t end_line, uint32_t end_offset, struct text_property property) { - struct text_property_entry entry = { - .start = (struct location){.line = start_line, .col = start_offset}, - .end = (struct location){.line = end_line, .col = end_offset}, - .property = property, - }; - VEC_PUSH(&text->properties, entry); + text_add_property_to_layer(text, start_line, start_offset, end_line, + end_offset, property, PropertyLayer_Default); +} + +void text_add_property_to_layer(struct text *text, uint32_t start_line, + uint32_t start_offset, uint32_t end_line, + uint32_t end_offset, + struct text_property property, layer_id layer) { + + property_vec *target_vec = find_property_layer(text, layer); + + if (target_vec == NULL) { + return; + } + + property.start = (struct location){.line = start_line, .col = start_offset}; + property.end = (struct location){.line = end_line, .col = end_offset}; + VEC_PUSH(target_vec, property); } void text_get_properties(struct text *text, uint32_t line, uint32_t offset, @@ -469,17 +515,110 @@ void text_get_properties(struct text *text, uint32_t line, uint32_t offset, uint32_t max_nproperties, uint32_t *nproperties) { struct location location = {.line = line, .col = offset}; uint32_t nres = 0; - VEC_FOR_EACH(&text->properties, struct text_property_entry * prop) { + VEC_FOR_EACH(&text->properties, struct text_property * prop) { + if (nres == max_nproperties) { + break; + } + if (location_is_between(location, prop->start, prop->end)) { - properties[nres] = &prop->property; + properties[nres] = prop; ++nres; + } + } + for (size_t i = 0; i < text->nproperty_layers; ++i) { + property_vec *pv = &text->property_layers[i].properties; + VEC_FOR_EACH(pv, struct text_property * prop) { if (nres == max_nproperties) { break; } + + if (location_is_between(location, prop->start, prop->end)) { + properties[nres] = prop; + ++nres; + } + } + } + + *nproperties = nres; +} + +void text_get_properties_filtered(struct text *text, uint32_t line, + uint32_t offset, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties, layer_id layer) { + + struct location location = {.line = line, .col = offset}; + uint32_t nres = 0; + property_vec *pv = find_property_layer(text, layer); + + if (pv == NULL) { + return; + } + + VEC_FOR_EACH(pv, struct text_property * prop) { + if (nres == max_nproperties) { + break; + } + + if (location_is_between(location, prop->start, prop->end)) { + properties[nres] = prop; + ++nres; } } + *nproperties = nres; } void text_clear_properties(struct text *text) { VEC_CLEAR(&text->properties); } + +layer_id text_add_property_layer(struct text *text) { + if (text->nproperty_layers < MAX_LAYERS) { + + struct property_layer *layer = + &text->property_layers[text->nproperty_layers]; + layer->id = text->current_layer_id; + VEC_INIT(&layer->properties, 16); + + ++text->current_layer_id; + ++text->nproperty_layers; + + return layer->id; + } + + return (layer_id)-1; +} + +void text_remove_property_layer(struct text *text, layer_id layer) { + for (size_t i = 0; i < text->nproperty_layers; ++i) { + struct property_layer *l = &text->property_layers[i]; + if (layer == l->id) { + + // swap to last place + struct property_layer temp = + text->property_layers[text->nproperty_layers - 1]; + text->property_layers[text->nproperty_layers - 1] = + text->property_layers[i]; + text->property_layers[i] = temp; + + // drop from array + text->property_layers[text->nproperty_layers - 1].id = (layer_id)-1; + VEC_DESTROY( + &text->property_layers[text->nproperty_layers - 1].properties); + --text->nproperty_layers; + + return; + } + } +} + +void text_clear_property_layer(struct text *text, layer_id layer) { + property_vec *pv = find_property_layer(text, layer); + + if (pv == NULL) { + return; + } + + VEC_CLEAR(pv); +} diff --git a/src/dged/text.h b/src/dged/text.h index 505c86a..ec14650 100644 --- a/src/dged/text.h +++ b/src/dged/text.h @@ -62,9 +62,13 @@ struct text_property_colors { uint32_t fg; bool set_bg; uint32_t bg; + bool underline; + bool inverted; }; struct text_property { + struct location start; + struct location end; enum text_property_type type; union property_data { struct text_property_colors colors; @@ -72,14 +76,34 @@ struct text_property { } data; }; +typedef uint64_t layer_id; +enum layer_ids { + PropertyLayer_Default = 0, +}; + void text_add_property(struct text *text, uint32_t start_line, uint32_t start_offset, uint32_t end_line, uint32_t end_offset, struct text_property property); +void text_add_property_to_layer(struct text *text, uint32_t start_line, + uint32_t start_offset, uint32_t end_line, + uint32_t end_offset, + struct text_property property, layer_id layer); + +layer_id text_add_property_layer(struct text *text); +void text_remove_property_layer(struct text *text, layer_id layer); + void text_get_properties(struct text *text, uint32_t line, uint32_t offset, struct text_property **properties, uint32_t max_nproperties, uint32_t *nproperties); +void text_get_properties_filtered(struct text *text, uint32_t line, + uint32_t offset, + struct text_property **properties, + uint32_t max_nproperties, + uint32_t *nproperties, layer_id layer); + void text_clear_properties(struct text *text); +void text_clear_property_layer(struct text *text, layer_id layer); #endif diff --git a/src/dged/vec.h b/src/dged/vec.h index 1289a08..59d6bce 100644 --- a/src/dged/vec.h +++ b/src/dged/vec.h @@ -1,6 +1,7 @@ #ifndef _VEC_H #define _VEC_H +#include <stdint.h> #include <stdlib.h> #define VEC(entry) \ @@ -12,7 +13,7 @@ } #define VEC_INIT(vec, initial_capacity) \ - (vec)->entries = malloc(sizeof((vec)->entries[0]) * initial_capacity); \ + (vec)->entries = calloc(initial_capacity, sizeof((vec)->entries[0])); \ (vec)->temp = calloc(1, sizeof((vec)->entries[0])); \ (vec)->capacity = initial_capacity; \ (vec)->nentries = 0; @@ -23,6 +24,7 @@ free((vec)->temp); \ free((vec)->entries); \ (vec)->entries = NULL; \ + (vec)->temp = NULL; \ (vec)->capacity = 0; \ (vec)->nentries = 0; @@ -69,6 +71,13 @@ keep && idx != size; keep = !keep, idx++) \ for (var = (vec)->entries + idx; keep; keep = !keep) +#define VEC_FOR_EACH_REVERSE(vec, var) VEC_FOR_EACH_INDEXED_REVERSE(vec, var, i) + +#define VEC_FOR_EACH_INDEXED_REVERSE(vec, var, idx) \ + for (uint32_t keep = 1, idx = 0, size = (vec)->nentries; \ + keep && idx != size; keep = !keep, idx++) \ + for (var = &(vec)->entries[size - idx - 1]; keep; keep = !keep) + #define VEC_SIZE(vec) (vec)->nentries #define VEC_CAPACITY(vec) (vec)->capacity #define VEC_ENTRIES(vec) (vec)->entries diff --git a/src/dged/window.c b/src/dged/window.c index 7ad4794..82b90d5 100644 --- a/src/dged/window.c +++ b/src/dged/window.c @@ -55,8 +55,6 @@ static void buffer_removed(struct buffer *buffer, void *userdata) { if (window_buffer(w) == buffer) { if (window_has_prev_buffer_view(w)) { window_set_buffer(w, window_prev_buffer_view(w)->buffer); - buffer_view_destroy(&w->prev_buffer_view); - w->has_prev_buffer_view = false; } else { struct buffers *buffers = (struct buffers *)userdata; struct buffer *b = buffers_find(buffers, "*messages*"); @@ -71,9 +69,10 @@ static void buffer_removed(struct buffer *buffer, void *userdata) { window_set_buffer(w, b); } } - buffer_view_destroy(&w->prev_buffer_view); - w->has_prev_buffer_view = false; } + + buffer_view_destroy(&w->prev_buffer_view); + w->has_prev_buffer_view = false; } BINTREE_NEXT(n); @@ -198,7 +197,36 @@ void windows_resize(uint32_t height, uint32_t width) { window_tree_resize(BINTREE_ROOT(&g_windows.windows), height - 1, width); } -void windows_update(void *(*frame_alloc)(size_t), float frame_time) { +bool windows_update(void *(*frame_alloc)(size_t), float frame_time) { + bool needs_render = false; + struct window_node *n = BINTREE_ROOT(&g_windows.windows); + BINTREE_FIRST(n); + uint32_t window_id = 0; + while (n != NULL) { + struct window *w = &BINTREE_VALUE(n); + if (w->type == Window_Buffer) { + char name[16] = {0}; + snprintf(name, 15, "bufview-%s", w->buffer_view.buffer->name); + w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, + w->y, 4, name); + + struct buffer_view_update_params p = { + .commands = w->commands, + .window_id = window_id, + .frame_time = frame_time, + .width = w->width, + .height = w->height, + .window_x = w->x, + .window_y = w->y, + .frame_alloc = frame_alloc, + }; + + needs_render |= buffer_view_update(&w->buffer_view, &p); + ++window_id; + } + + BINTREE_NEXT(n); + } struct window *w = &g_minibuffer_window; w->x = 0; @@ -224,7 +252,7 @@ void windows_update(void *(*frame_alloc)(size_t), float frame_time) { .frame_alloc = frame_alloc, }; - buffer_view_update(&w->buffer_view, &p); + needs_render |= buffer_view_update(&w->buffer_view, &p); command_list_draw_command_list(w->commands, inner_commands); if (g_popup_visible) { @@ -239,9 +267,12 @@ void windows_update(void *(*frame_alloc)(size_t), float frame_time) { struct window *rw = root_window(); uint32_t w_x = w->x; uint32_t w_y = w->y; - uint32_t width = w_x + w->width > rw->width ? rw->width - w_x : w->width; - uint32_t height = - w_y + w->height > rw->height ? rw->height - w_y : w->height; + uint32_t width = w_x + w->width > rw->width + ? (rw->width >= w_x ? rw->width - w_x : 0) + : w->width; + uint32_t height = w_y + w->height > rw->height + ? (rw->height >= w_y ? rw->height - w_y : 0) + : w->height; // is there space for padding? if (w_x > 1 && w_x + width + hpadding <= rw->width) { @@ -317,38 +348,11 @@ void windows_update(void *(*frame_alloc)(size_t), float frame_time) { .frame_alloc = frame_alloc, }; - buffer_view_update(&w->buffer_view, &p); + needs_render |= buffer_view_update(&w->buffer_view, &p); command_list_draw_command_list(w->commands, inner); } - struct window_node *n = BINTREE_ROOT(&g_windows.windows); - BINTREE_FIRST(n); - uint32_t window_id = 0; - while (n != NULL) { - struct window *w = &BINTREE_VALUE(n); - if (w->type == Window_Buffer) { - char name[16] = {0}; - snprintf(name, 15, "bufview-%s", w->buffer_view.buffer->name); - w->commands = command_list_create(w->height * w->width, frame_alloc, w->x, - w->y, 4, name); - - struct buffer_view_update_params p = { - .commands = w->commands, - .window_id = window_id, - .frame_time = frame_time, - .width = w->width, - .height = w->height, - .window_x = w->x, - .window_y = w->y, - .frame_alloc = frame_alloc, - }; - - buffer_view_update(&w->buffer_view, &p); - ++window_id; - } - - BINTREE_NEXT(n); - } + return needs_render; } void windows_render(struct display *display) { diff --git a/src/dged/window.h b/src/dged/window.h index 7738e16..d646d78 100644 --- a/src/dged/window.h +++ b/src/dged/window.h @@ -27,7 +27,7 @@ void windows_init(uint32_t height, uint32_t width, void windows_destroy(void); void windows_resize(uint32_t height, uint32_t width); -void windows_update(void *(*frame_alloc)(size_t), float frame_time); +bool windows_update(void *(*frame_alloc)(size_t), float frame_time); void windows_render(struct display *display); struct window *root_window(void); diff --git a/src/main/bindings.c b/src/main/bindings.c index 889c32b..db6c924 100644 --- a/src/main/bindings.c +++ b/src/main/bindings.c @@ -8,6 +8,11 @@ static struct keymap g_global_keymap, g_ctrlx_map, g_windows_keymap, g_buffer_default_keymap; +HOOK_IMPL(buffer_keymaps, buffer_keymaps_cb); + +static buffer_keymaps_hook_vec g_buffer_keymaps_hooks; +uint32_t g_buffer_keymaps_hook_id = 0; + struct buffer_keymap { buffer_keymap_id id; struct buffer *buffer; @@ -17,6 +22,8 @@ struct buffer_keymap { static VEC(struct buffer_keymap) g_buffer_keymaps; static buffer_keymap_id g_current_keymap_id; +struct keymap *buffer_default_keymap(void) { return &g_buffer_default_keymap; } + void set_default_buffer_bindings(struct keymap *keymap) { struct binding buffer_bindings[] = { BINDING(Ctrl, 'B', "backward-char"), @@ -144,6 +151,8 @@ void init_bindings(void) { VEC_INIT(&g_buffer_keymaps, 32); g_current_keymap_id = 0; + VEC_INIT(&g_buffer_keymaps_hooks, 16); + /* Minibuffer binds. * This map is actually never removed so forget about the id. */ @@ -204,6 +213,14 @@ uint32_t buffer_keymaps(struct buffer *buffer, struct keymap *keymaps[], } } + // hooks + VEC_FOR_EACH(&g_buffer_keymaps_hooks, struct buffer_keymaps_hook * hook) { + if (nkeymaps < max_nkeymaps) { + nkeymaps += hook->callback(buffer, &keymaps[nkeymaps], + max_nkeymaps - nkeymaps, hook->userdata); + } + } + return nkeymaps; } @@ -219,3 +236,11 @@ void destroy_bindings(void) { VEC_DESTROY(&g_buffer_keymaps); } + +uint32_t buffer_add_keymaps_hook(buffer_keymaps_cb callback, void *userdata) { + return insert_buffer_keymaps_hook( + &g_buffer_keymaps_hooks, &g_buffer_keymaps_hook_id, callback, userdata); +} +void buffer_remove_keymaps_hook(uint32_t id, remove_hook_cb callback) { + remove_buffer_keymaps_hook(&g_buffer_keymaps_hooks, id, callback); +} diff --git a/src/main/bindings.h b/src/main/bindings.h index 96f20fd..74f43e3 100644 --- a/src/main/bindings.h +++ b/src/main/bindings.h @@ -1,15 +1,24 @@ #include <stdint.h> +#include "dged/hook.h" + struct keymap; struct buffer; struct binding; void init_bindings(void); +struct keymap *buffer_default_keymap(void); + typedef uint64_t buffer_keymap_id; buffer_keymap_id buffer_add_keymap(struct buffer *buffer, struct keymap keymap); void buffer_remove_keymap(buffer_keymap_id id); uint32_t buffer_keymaps(struct buffer *buffer, struct keymap *keymaps[], uint32_t max_nkeymaps); +typedef uint32_t (*buffer_keymaps_cb)(struct buffer *, struct keymap **, + uint32_t, void *); +uint32_t buffer_add_keymaps_hook(buffer_keymaps_cb callback, void *userdata); +void buffer_remove_keymaps_hook(uint32_t id, remove_hook_cb callback); + void destroy_bindings(void); diff --git a/src/main/cmds.c b/src/main/cmds.c index fdd1d87..7d63661 100644 --- a/src/main/cmds.c +++ b/src/main/cmds.c @@ -5,6 +5,7 @@ #include <sys/stat.h> #include "dged/binding.h" + #include "dged/buffer.h" #include "dged/buffer_view.h" #include "dged/buffers.h" @@ -18,6 +19,9 @@ #include "bindings.h" #include "completion.h" +#include "completion/buffer.h" +#include "completion/command.h" +#include "completion/path.h" #include "search-replace.h" static void (*g_terminate_cb)(void) = NULL; @@ -32,7 +36,7 @@ static int32_t _abort(struct command_ctx ctx, int argc, const char *argv[]) { disable_completion(minibuffer_buffer()); minibuffer_abort_prompt(); buffer_view_clear_mark(window_buffer_view(ctx.active_window)); - minibuffer_echo_timeout(4, "š£ aborted"); + minibuffer_display_timeout(4, "š£ aborted"); return 0; } @@ -63,17 +67,13 @@ static int32_t write_file(struct command_ctx ctx, int argc, const char *argv[]) { const char *pth = NULL; if (argc == 0) { - struct completion_provider providers[] = {path_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = false}}), - providers, 1, write_file_comp_inserted); + struct completion_provider providers[] = { + create_path_provider(write_file_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); return minibuffer_prompt(ctx, "write to file: "); } + disable_completion(minibuffer_buffer()); pth = argv[0]; buffer_set_filename(window_buffer(ctx.active_window), pth); buffer_to_file(window_buffer(ctx.active_window)); @@ -81,18 +81,16 @@ static int32_t write_file(struct command_ctx ctx, int argc, return 0; } -static void run_interactive_comp_inserted(void) { minibuffer_execute(); } +static void run_interactive_comp_inserted(struct command *cmd) { + (void)cmd; + minibuffer_execute(); +} int32_t run_interactive(struct command_ctx ctx, int argc, const char *argv[]) { if (argc == 0) { - struct completion_provider providers[] = {commands_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = false}}), - providers, 1, run_interactive_comp_inserted); + struct completion_provider providers[] = { + create_commands_provider(ctx.commands, run_interactive_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); return minibuffer_prompt(ctx, "execute: "); } @@ -134,19 +132,18 @@ int32_t do_switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { COMMAND_FN("do-switch-buffer", do_switch_buffer, do_switch_buffer, NULL) -static void switch_buffer_comp_inserted(void) { minibuffer_execute(); } +static void switch_buffer_comp_inserted(struct buffer *buffer) { + // TODO: do useful stuff with buffer here + (void)buffer; + minibuffer_execute(); +} int32_t switch_buffer(struct command_ctx ctx, int argc, const char *argv[]) { if (argc == 0) { minibuffer_clear(); - struct completion_provider providers[] = {buffer_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = false}}), - providers, 1, switch_buffer_comp_inserted); + struct completion_provider providers[] = { + create_buffer_provider(ctx.buffers, switch_buffer_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); ctx.self = &do_switch_buffer_command; if (window_has_prev_buffer_view(ctx.active_window)) { @@ -184,19 +181,18 @@ int32_t do_kill_buffer(struct command_ctx ctx, int argc, const char *argv[]) { COMMAND_FN("do-kill-buffer", do_kill_buffer, do_kill_buffer, NULL) -static void kill_buffer_comp_inserted(void) { minibuffer_execute(); } +static void kill_buffer_comp_inserted(struct buffer *buffer) { + // TODO: do something with buffer + (void)buffer; + minibuffer_execute(); +} int32_t kill_buffer(struct command_ctx ctx, int argc, const char *argv[]) { if (argc == 0) { minibuffer_clear(); - struct completion_provider providers[] = {buffer_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = false}}), - providers, 1, kill_buffer_comp_inserted); + struct completion_provider providers[] = { + create_buffer_provider(ctx.buffers, kill_buffer_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); ctx.self = &do_kill_buffer_command; return minibuffer_prompt(ctx, "kill buffer (default %s): ", @@ -254,8 +250,11 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) { struct buffer *listbuf = (struct buffer *)userdata; const char *path = buffer->filename != NULL ? buffer->filename : "<no-file>"; + const char *modified = + buffer->filename != NULL && buffer->modified ? "*" : ""; char buf[1024]; - size_t written = snprintf(buf, 1024, "%-24s %s", buffer->name, path); + size_t written = + snprintf(buf, 1024, "%-24s %s%s", buffer->name, path, modified); if (written > 0) { struct location begin = buffer_end(listbuf); @@ -275,9 +274,9 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) { size_t pathlen = strlen(path); uint32_t nchars_path = utf8_nchars((uint8_t *)path, pathlen); buffer_add_text_property( - listbuf, (struct location){.line = begin.line, .col = begin.col + 24}, + listbuf, (struct location){.line = begin.line, .col = begin.col + 25}, (struct location){.line = begin.line, - .col = begin.col + 24 + nchars_path}, + .col = begin.col + 25 + nchars_path}, (struct text_property){.type = TextProperty_Colors, .data.colors = (struct text_property_colors){ .set_bg = false, @@ -294,8 +293,7 @@ void buffer_to_list_line(struct buffer *buffer, void *userdata) { } } -int32_t buflist_visit_cmd(struct command_ctx ctx, int argc, - const char *argv[]) { +int32_t buflist_visit_cmd(struct command_ctx ctx, int argc, const char **argv) { (void)argc; (void)argv; @@ -321,7 +319,6 @@ int32_t buflist_close_cmd(struct command_ctx ctx, int argc, const char *argv[]) { return execute_command(&do_switch_buffer_command, ctx.commands, ctx.active_window, ctx.buffers, argc, argv); - return 0; } void buflist_refresh(struct buffer *buffer, void *userdata) { @@ -371,6 +368,35 @@ int32_t buflist_kill_cmd(struct command_ctx ctx, int argc, const char *argv[]) { return 0; } +int32_t buflist_save_cmd(struct command_ctx ctx, int argc, const char *argv[]) { + (void)argc; + (void)argv; + + struct window *w = ctx.active_window; + + struct buffer_view *bv = window_buffer_view(w); + struct text_chunk text = buffer_line(bv->buffer, bv->dot.line); + + char *end = (char *)memchr(text.text, ' ', text.nbytes); + + if (end != NULL) { + uint32_t len = end - (char *)text.text; + char *bufname = (char *)malloc(len + 1); + strncpy(bufname, (const char *)text.text, len); + bufname[len] = '\0'; + + struct buffer *buffer = buffers_find(ctx.buffers, bufname); + if (buffer != NULL) { + buffer_to_file(buffer); + } + free(bufname); + execute_command(&buflist_refresh_command, ctx.commands, ctx.active_window, + ctx.buffers, 0, NULL); + } + + return 0; +} + int32_t buffer_list(struct command_ctx ctx, int argc, const char *argv[]) { (void)argc; (void)argv; @@ -401,10 +427,16 @@ int32_t buffer_list(struct command_ctx ctx, int argc, const char *argv[]) { .fn = buflist_close_cmd, }; + static struct command buflist_save = { + .name = "buflist-save", + .fn = buflist_save_cmd, + }; + struct binding bindings[] = { ANONYMOUS_BINDING(ENTER, &buflist_visit), ANONYMOUS_BINDING(None, 'k', &buflist_kill), ANONYMOUS_BINDING(None, 'q', &buflist_close), + ANONYMOUS_BINDING(None, 's', &buflist_save), ANONYMOUS_BINDING(None, 'g', &buflist_refresh_command), }; struct keymap km = keymap_create("buflist", 8); @@ -456,15 +488,16 @@ static int32_t open_file(struct buffers *buffers, struct window *active_window, int32_t find_file(struct command_ctx ctx, int argc, const char *argv[]) { if (argc == 0) { minibuffer_clear(); - struct completion_provider providers[] = {path_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = true}}), - providers, 1, find_file_comp_inserted); - return minibuffer_prompt(ctx, "find file: "); + struct completion_provider providers[] = { + create_path_provider(find_file_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); + + int32_t r = minibuffer_prompt(ctx, "find file: "); + + // Trigger directly + complete(minibuffer_buffer(), buffer_end(minibuffer_buffer())); + + return r; } disable_completion(minibuffer_buffer()); @@ -487,14 +520,9 @@ int32_t find_file_relative(struct command_ctx ctx, int argc, size_t dirlen = strlen(dir); if (argc == 0) { minibuffer_clear(); - struct completion_provider providers[] = {path_provider()}; - enable_completion(minibuffer_buffer(), - ((struct completion_trigger){ - .kind = CompletionTrigger_Input, - .data.input = - (struct completion_trigger_input){ - .nchars = 0, .trigger_initially = true}}), - providers, 1, find_file_comp_inserted); + struct completion_provider providers[] = { + create_path_provider(find_file_comp_inserted)}; + add_completion_providers(minibuffer_buffer(), providers, 1); ctx.self = &find_file_command; @@ -505,6 +533,9 @@ int32_t find_file_relative(struct command_ctx ctx, int argc, minibuffer_prompt_initial(ctx, dir_with_slash, "find file: "); free(filename); free(dir_with_slash); + + complete(minibuffer_buffer(), buffer_end(minibuffer_buffer())); + return 0; } diff --git a/src/main/completion.c b/src/main/completion.c index 38d75ab..d777408 100644 --- a/src/main/completion.c +++ b/src/main/completion.c @@ -12,91 +12,75 @@ #include "dged/buffer.h" #include "dged/buffer_view.h" #include "dged/buffers.h" +#include "dged/display.h" #include "dged/minibuffer.h" #include "dged/path.h" #include "dged/window.h" #include "bindings.h" +#include "frame-hooks.h" -struct active_completion_ctx { - struct completion_trigger trigger; - uint32_t trigger_current_nchars; - struct completion_provider *providers; - uint32_t nproviders; - insert_cb on_completion_inserted; -}; - -struct completion_state { - struct completion completions[50]; - uint32_t ncompletions; - uint32_t current_completion; - bool active; - buffer_keymap_id keymap_id; - bool keymap_active; - struct active_completion_ctx *ctx; -} g_state = {0}; - -static struct buffer *g_target_buffer = NULL; - -static void hide_completion(void); - -static bool is_space(const struct codepoint *c) { - // TODO: utf8 whitespace and other whitespace - return c->codepoint == ' '; -} - -static uint32_t complete_path(struct completion_context ctx, void *userdata); -static struct completion_provider g_path_provider = { - .name = "path", - .complete = complete_path, - .userdata = NULL, -}; +struct buffer_completion { + struct buffer *buffer; + uint32_t insert_hook_id; + uint32_t remove_hook_id; -static uint32_t complete_buffers(struct completion_context ctx, void *userdata); -static struct completion_provider g_buffer_provider = { - .name = "buffers", - .complete = complete_buffers, - .userdata = NULL, + VEC(struct completion_provider) providers; }; -static uint32_t complete_commands(struct completion_context ctx, - void *userdata); -static struct completion_provider g_commands_provider = { - .name = "commands", - .complete = complete_commands, - .userdata = NULL, +struct completion_item { + struct region area; + struct completion completion; }; -struct completion_provider path_provider(void) { return g_path_provider; } +static struct completion_state { + VEC(struct buffer_completion) buffer_completions; + VEC(struct completion_item) completions; + uint64_t completion_index; + struct buffer *completions_buffer; + buffer_keymap_id keymap_id; + struct buffer *target; + layer_id highlight_current_layer; + bool insert_in_progress; + bool paused; +} g_state; -struct completion_provider buffer_provider(void) { return g_buffer_provider; } +static struct region active_completion_region(struct completion_state *state) { + struct region reg = + region_new((struct location){0, 0}, (struct location){0, 0}); + if (state->completion_index < VEC_SIZE(&state->completions)) { + reg = VEC_ENTRIES(&state->completions)[state->completion_index].area; + } -struct completion_provider commands_provider(void) { - return g_commands_provider; + return reg; } -struct active_completion { - struct buffer *buffer; - uint32_t insert_hook_id; - uint32_t remove_hook_id; -}; - -VEC(struct active_completion) g_active_completions; - static int32_t goto_next_completion(struct command_ctx ctx, int argc, const char *argv[]) { (void)ctx; (void)argc; (void)argv; - if (g_state.current_completion < g_state.ncompletions - 1) { - ++g_state.current_completion; + if (!completion_active()) { + return 0; + } + + if (VEC_EMPTY(&g_state.completions)) { + g_state.completion_index = 0; + return 0; + } + + size_t ncompletions = VEC_SIZE(&g_state.completions); + if (g_state.completion_index >= ncompletions - 1) { + g_state.completion_index = ncompletions - 1; + return 0; } + ++g_state.completion_index; + if (completion_active()) { - buffer_view_goto( - window_buffer_view(popup_window()), - ((struct location){.line = g_state.current_completion, .col = 0})); + buffer_view_goto(window_buffer_view(popup_window()), + active_completion_region(&g_state).begin); } return 0; @@ -108,14 +92,19 @@ static int32_t goto_prev_completion(struct command_ctx ctx, int argc, (void)argc; (void)argv; - if (g_state.current_completion > 0) { - --g_state.current_completion; + if (!completion_active()) { + return 0; } + if (g_state.completion_index == 0) { + return 0; + } + + --g_state.completion_index; + if (completion_active()) { - buffer_view_goto( - window_buffer_view(popup_window()), - ((struct location){.line = g_state.current_completion, .col = 0})); + buffer_view_goto(window_buffer_view(popup_window()), + active_completion_region(&g_state).begin); } return 0; @@ -127,524 +116,325 @@ static int32_t insert_completion(struct command_ctx ctx, int argc, (void)argc; (void)argv; - // is it in the popup? - struct completion *comp = &g_state.completions[g_state.current_completion]; - bool done = comp->complete; - const char *ins = comp->insert; - size_t inslen = strlen(ins); - buffer_view_add(window_buffer_view(windows_get_active()), (uint8_t *)ins, - inslen); + if (!completion_active()) { + return 0; + } - if (done) { - g_state.ctx->on_completion_inserted(); - abort_completion(); + struct buffer_view *bv = window_buffer_view(popup_window()); + struct window *target_window = windows_get_active(); + struct buffer_view *target = window_buffer_view(target_window); + VEC_FOR_EACH(&g_state.completions, struct completion_item * item) { + if (region_is_inside(item->area, bv->dot)) { + g_state.insert_in_progress = true; + item->completion.selected(item->completion.data, target); + g_state.insert_in_progress = false; + return 0; + } } return 0; } -static void clear_completions(void) { - for (uint32_t ci = 0; ci < g_state.ncompletions; ++ci) { - free((void *)g_state.completions[ci].display); - free((void *)g_state.completions[ci].insert); - g_state.completions[ci].display = NULL; - g_state.completions[ci].insert = NULL; - g_state.completions[ci].complete = false; - } - g_state.ncompletions = 0; -} - COMMAND_FN("next-completion", next_completion, goto_next_completion, NULL) COMMAND_FN("prev-completion", prev_completion, goto_prev_completion, NULL) COMMAND_FN("insert-completion", insert_completion, insert_completion, NULL) -static void update_completions(struct buffer *buffer, - struct active_completion_ctx *ctx, - struct location location) { - clear_completions(); - for (uint32_t pi = 0; pi < ctx->nproviders; ++pi) { - struct completion_provider *provider = &ctx->providers[pi]; - - struct completion_context comp_ctx = (struct completion_context){ - .buffer = buffer, - .location = location, - .max_ncompletions = 50 - g_state.ncompletions, - .completions = g_state.completions, - }; - - g_state.ncompletions += provider->complete(comp_ctx, provider->userdata); - } - - window_set_buffer_e(popup_window(), g_target_buffer, false, false); - struct buffer_view *v = window_buffer_view(popup_window()); - - size_t max_width = 0; - uint32_t prev_selection = g_state.current_completion; - - buffer_clear(v->buffer); - buffer_view_goto(v, (struct location){.line = 0, .col = 0}); - if (g_state.ncompletions > 0) { - for (uint32_t compi = 0; compi < g_state.ncompletions; ++compi) { - const char *disp = g_state.completions[compi].display; - size_t width = strlen(disp); - if (width > max_width) { - max_width = width; - } - buffer_view_add(v, (uint8_t *)disp, width); - buffer_view_add(v, (uint8_t *)"\n", 1); +static void clear_completions(struct completion_state *state) { + buffer_clear(state->completions_buffer); + VEC_FOR_EACH(&state->completions, struct completion_item * item) { + if (item->completion.cleanup != NULL) { + item->completion.cleanup(item->completion.data); } - - // select the closest one to previous selection - g_state.current_completion = prev_selection < g_state.ncompletions - ? prev_selection - : g_state.ncompletions - 1; - - buffer_view_goto( - v, (struct location){.line = g_state.current_completion, .col = 0}); - - struct window *target_window = window_find_by_buffer(buffer); - struct window_position winpos = window_position(target_window); - struct buffer_view *view = window_buffer_view(target_window); - uint32_t height = g_state.ncompletions > 10 ? 10 : g_state.ncompletions; - windows_show_popup(winpos.y + location.line - height - 1, - winpos.x + view->fringe_width + location.col + 1, - max_width + 2, height); - - if (!g_state.keymap_active) { - struct keymap km = keymap_create("completion", 8); - struct binding comp_bindings[] = { - ANONYMOUS_BINDING(Ctrl, 'N', &next_completion_command), - ANONYMOUS_BINDING(Ctrl, 'P', &prev_completion_command), - ANONYMOUS_BINDING(ENTER, &insert_completion_command), - }; - keymap_bind_keys(&km, comp_bindings, - sizeof(comp_bindings) / sizeof(comp_bindings[0])); - g_state.keymap_id = buffer_add_keymap(buffer, km); - g_state.keymap_active = true; - } - } else { - hide_completion(); - } -} - -static void on_buffer_delete(struct buffer *buffer, - struct edit_location deleted, void *userdata) { - struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata; - - if (g_state.active) { - update_completions(buffer, ctx, deleted.coordinates.begin); } -} -static void on_buffer_insert(struct buffer *buffer, - struct edit_location inserted, void *userdata) { - struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata; - - if (!g_state.active) { - uint32_t nchars = 0; - switch (ctx->trigger.kind) { - case CompletionTrigger_Input: - for (uint32_t line = inserted.coordinates.begin.line; - line <= inserted.coordinates.end.line; ++line) { - nchars += buffer_line_length(buffer, line); - } - nchars -= inserted.coordinates.begin.col + - (buffer_line_length(buffer, inserted.coordinates.end.line) - - inserted.coordinates.end.col); - - ctx->trigger_current_nchars += nchars; - - if (ctx->trigger_current_nchars < ctx->trigger.data.input.nchars) { - return; - } - - ctx->trigger_current_nchars = 0; - break; + VEC_CLEAR(&state->completions); + state->completion_index = 0; - case CompletionTrigger_Char: - // TODO - break; - } - - // activate completion - g_state.active = true; - g_state.ctx = ctx; + if (completion_active()) { + buffer_view_goto(window_buffer_view(popup_window()), + (struct location){0, 0}); } - - update_completions(buffer, ctx, inserted.coordinates.end); -} - -static void update_completion_buffer(struct buffer *buffer, void *userdata) { - (void)buffer; - (void)userdata; - - buffer_add_text_property( - g_target_buffer, - (struct location){.line = g_state.current_completion, .col = 0}, - (struct location){.line = g_state.current_completion, - .col = buffer_line_length(g_target_buffer, - g_state.current_completion)}, - (struct text_property){.type = TextProperty_Colors, - .data.colors = (struct text_property_colors){ - .set_bg = false, - .bg = 0, - .set_fg = true, - .fg = 4, - }}); } -void init_completion(struct buffers *buffers, struct commands *commands) { - if (g_target_buffer == NULL) { - g_target_buffer = buffers_add(buffers, buffer_create("*completions*")); - buffer_add_update_hook(g_target_buffer, update_completion_buffer, NULL); - } +static void update_window_position(struct completion_state *state) { - g_buffer_provider.userdata = buffers; - g_commands_provider.userdata = commands; - VEC_INIT(&g_active_completions, 32); -} + size_t ncompletions = VEC_SIZE(&state->completions); -struct oneshot_completion { - uint32_t hook_id; - struct active_completion_ctx *ctx; -}; + struct window *target_window = windows_get_active(); + struct window *root_wind = root_window(); -static void cleanup_oneshot(void *userdata) { free(userdata); } + size_t nlines = buffer_num_lines(state->completions_buffer); + size_t max_width = 10; -static void oneshot_completion_hook(struct buffer *buffer, void *userdata) { - struct oneshot_completion *comp = (struct oneshot_completion *)userdata; + window_set_buffer_e(popup_window(), state->completions_buffer, false, false); + struct window_position winpos = window_position(target_window); + struct buffer_view *view = window_buffer_view(target_window); + uint32_t height = ncompletions > 10 ? 10 : ncompletions; - // activate completion - g_state.active = true; - g_state.ctx = comp->ctx; + size_t xpos = + winpos.x + view->fringe_width + (view->dot.col - view->scroll.col) + 1; - struct window *w = window_find_by_buffer(buffer); - if (w != NULL) { - struct buffer_view *v = window_buffer_view(w); - update_completions(buffer, comp->ctx, v->dot); + // should it be over or under? + size_t relative_line = (view->dot.line - view->scroll.line); + size_t ypos = winpos.y + relative_line; + if (ypos > 10) { + ypos -= height + 1; } else { - update_completions(buffer, comp->ctx, - (struct location){.line = 0, .col = 0}); + ypos += 3; } - // this is a oneshot after all - buffer_remove_update_hook(buffer, comp->hook_id, cleanup_oneshot); -} - -void enable_completion(struct buffer *source, struct completion_trigger trigger, - struct completion_provider *providers, - uint32_t nproviders, insert_cb on_completion_inserted) { - // check if we are already active - VEC_FOR_EACH(&g_active_completions, struct active_completion * c) { - if (c->buffer == source) { - disable_completion(source); + for (uint64_t i = 0; i < nlines; ++i) { + size_t linelen = buffer_line_length(state->completions_buffer, i); + if (linelen > max_width) { + max_width = linelen; } } - struct active_completion_ctx *ctx = - calloc(1, sizeof(struct active_completion_ctx)); - ctx->trigger = trigger; - ctx->on_completion_inserted = on_completion_inserted; - ctx->nproviders = nproviders; - ctx->providers = calloc(nproviders, sizeof(struct completion_provider)); - memcpy(ctx->providers, providers, - sizeof(struct completion_provider) * nproviders); - - uint32_t insert_hook_id = - buffer_add_insert_hook(source, on_buffer_insert, ctx); - uint32_t remove_hook_id = - buffer_add_delete_hook(source, on_buffer_delete, ctx); - - VEC_PUSH(&g_active_completions, ((struct active_completion){ - .buffer = source, - .insert_hook_id = insert_hook_id, - .remove_hook_id = remove_hook_id, - })); - - // do we want to trigger initially? - if (ctx->trigger.kind == CompletionTrigger_Input && - ctx->trigger.data.input.trigger_initially) { - struct oneshot_completion *comp = - calloc(1, sizeof(struct oneshot_completion)); - comp->ctx = ctx; - comp->hook_id = - buffer_add_update_hook(source, oneshot_completion_hook, comp); - } -} + size_t available = window_width(root_wind) - xpos - 5; + max_width = max_width >= available ? available : max_width; -static void hide_completion(void) { - windows_close_popup(); - if (g_state.active) { - buffer_remove_keymap(g_state.keymap_id); - g_state.keymap_active = false; - } + windows_show_popup(ypos, xpos, max_width, height); } -void abort_completion(void) { - hide_completion(); - g_state.active = false; - clear_completions(); +static void update_window_pos_frame_hook(void *data) { + struct completion_state *state = (struct completion_state *)data; + update_window_position(state); } -bool completion_active(void) { - return popup_window_visible() && - window_buffer(popup_window()) == g_target_buffer && g_state.active; -} +static void open_completion(struct completion_state *state) { -static void cleanup_active_comp_ctx(void *userdata) { - struct active_completion_ctx *ctx = (struct active_completion_ctx *)userdata; + size_t ncompletions = VEC_SIZE(&state->completions); - if (g_state.ctx == ctx && g_state.active) { + if (ncompletions == 0) { abort_completion(); + return; } - free(ctx->providers); - free(ctx); -} + struct window *target_window = windows_get_active(); + struct buffer *buffer = window_buffer(target_window); + if (!completion_active() || state->target != buffer) { -static void do_nothing(void *userdata) { (void)userdata; } + // clear any previous keymaps + abort_completion(); -static void cleanup_active_completion(struct active_completion *comp) { - buffer_remove_delete_hook(comp->buffer, comp->remove_hook_id, do_nothing); - buffer_remove_insert_hook(comp->buffer, comp->insert_hook_id, - cleanup_active_comp_ctx); -} + struct keymap km = keymap_create("completion", 8); + struct binding comp_bindings[] = { + ANONYMOUS_BINDING(Ctrl, 'N', &next_completion_command), + ANONYMOUS_BINDING(Ctrl, 'P', &prev_completion_command), + ANONYMOUS_BINDING(ENTER, &insert_completion_command), + }; + keymap_bind_keys(&km, comp_bindings, + sizeof(comp_bindings) / sizeof(comp_bindings[0])); -void disable_completion(struct buffer *buffer) { - VEC_FOR_EACH_INDEXED(&g_active_completions, struct active_completion * comp, - i) { - if (buffer == comp->buffer) { - VEC_SWAP(&g_active_completions, i, VEC_SIZE(&g_active_completions) - 1); - VEC_POP(&g_active_completions, struct active_completion removed); - cleanup_active_completion(&removed); - } + state->keymap_id = buffer_add_keymap(buffer, km); } -} -void destroy_completion(void) { - // clean up any active completions we might have - VEC_FOR_EACH(&g_active_completions, struct active_completion * comp) { - cleanup_active_completion(comp); - } - VEC_DESTROY(&g_active_completions); + // need to run next frame to have the correct position + run_next_frame(update_window_pos_frame_hook, state); } -static bool is_hidden(const char *filename) { - return filename[0] == '.' && filename[1] != '\0' && filename[1] != '.'; -} +static void add_completions_impl(struct completion *completions, + size_t ncompletions) { + for (uint32_t i = 0; i < ncompletions; ++i) { + struct completion *c = &completions[i]; + struct region area = c->render(c->data, g_state.completions_buffer); + VEC_APPEND(&g_state.completions, struct completion_item * new); + new->area = area; + new->completion = *c; + } -static int cmp_completions(const void *comp_a, const void *comp_b) { - struct completion *a = (struct completion *)comp_a; - struct completion *b = (struct completion *)comp_b; - return strcmp(a->display, b->display); + open_completion(&g_state); } -static uint32_t complete_path(struct completion_context ctx, void *userdata) { - (void)userdata; - - // obtain path from the buffer - struct text_chunk txt = {0}; - if (ctx.buffer == minibuffer_buffer()) { - txt = minibuffer_content(); - } else { - struct match_result start = - buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); - if (!start.found) { - start.at = (struct location){.line = ctx.location.line, .col = 0}; - return 0; +static void update_completions(struct completion_state *state, + struct buffer *buffer, struct location location, + bool deletion) { + clear_completions(state); + struct buffer_completion *buffer_config = NULL; + VEC_FOR_EACH(&state->buffer_completions, struct buffer_completion * bc) { + if (buffer == bc->buffer) { + buffer_config = bc; + break; } - txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); } - char *path = calloc(txt.nbytes + 1, sizeof(char)); - memcpy(path, txt.text, txt.nbytes); - path[txt.nbytes] = '\0'; - - if (txt.allocated) { - free(txt.text); + if (buffer_config == NULL) { + return; } - uint32_t n = 0; - char *p1 = to_abspath(path); - char *p2 = strdup(p1); - - size_t inlen = strlen(path); + VEC_FOR_EACH(&buffer_config->providers, + struct completion_provider * provider) { + struct completion_context comp_ctx = (struct completion_context){ + .buffer = buffer, + .location = location, + .add_completions = add_completions_impl, + }; - if (ctx.max_ncompletions == 0) { - goto done; + provider->complete(comp_ctx, deletion, provider->userdata); } +} - const char *dir = p1; - const char *file = ""; +static void update_comp_buffer(struct buffer *buffer, void *userdata) { + struct completion_state *state = (struct completion_state *)userdata; - // check the input path here since - // to_abspath removes trailing slashes - if (inlen == 0 || path[inlen - 1] != '/') { - dir = dirname(p1); - file = basename(p2); + buffer_clear_text_property_layer(buffer, state->highlight_current_layer); + + if (buffer_is_empty(buffer)) { + abort_completion(); } - DIR *d = opendir(dir); - if (d == NULL) { - goto done; + struct region reg = active_completion_region(state); + if (region_has_size(reg)) { + buffer_add_text_property_to_layer(buffer, reg.begin, reg.end, + (struct text_property){ + .type = TextProperty_Colors, + .data.colors = + (struct text_property_colors){ + .inverted = true, + .set_fg = false, + .set_bg = false, + .underline = false, + }, + }, + state->highlight_current_layer); } +} - errno = 0; - size_t filelen = strlen(file); - bool file_is_curdir = (filelen == 1 && memcmp(file, ".", 1) == 0); - while (n < ctx.max_ncompletions) { - struct dirent *de = readdir(d); - if (de == NULL && errno != 0) { - // skip the erroring entry - errno = 0; - continue; - } else if (de == NULL && errno == 0) { - break; - } +static void on_buffer_changed(struct buffer *buffer, struct edit_location edit, + bool deletion, void *userdata) { + struct completion_state *state = (struct completion_state *)userdata; - switch (de->d_type) { - case DT_DIR: - case DT_REG: - case DT_LNK: - if (!is_hidden(de->d_name) && - (filelen == 0 || file_is_curdir || - (filelen <= strlen(de->d_name) && - memcmp(file, de->d_name, filelen) == 0))) { - - const char *disp = strdup(de->d_name); - ctx.completions[n] = (struct completion){ - .display = disp, - .insert = strdup(disp + (file_is_curdir ? 0 : filelen)), - .complete = de->d_type == DT_REG, - }; - ++n; - } - break; - } + if (state->insert_in_progress || state->paused) { + return; } - closedir(d); + update_completions(state, buffer, edit.coordinates.end, deletion); +} -done: - free(path); - free(p1); - free(p2); +static void on_buffer_insert(struct buffer *buffer, struct edit_location edit, + void *userdata) { + on_buffer_changed(buffer, edit, false, userdata); +} - qsort(ctx.completions, n, sizeof(struct completion), cmp_completions); - return n; +static void on_buffer_delete(struct buffer *buffer, struct edit_location edit, + void *userdata) { + on_buffer_changed(buffer, edit, true, userdata); } -struct needle_match_ctx { - const char *needle; - struct completion *completions; - uint32_t max_ncompletions; - uint32_t ncompletions; -}; +void init_completion(struct buffers *buffers) { + if (g_state.completions_buffer == NULL) { + struct buffer b = buffer_create("*completions*"); + b.lazy_row_add = false; + b.force_show_ws_off = true; + b.retain_properties = true; + g_state.completions_buffer = buffers_add(buffers, b); + } -static void buffer_matches(struct buffer *buffer, void *userdata) { - struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata; + g_state.highlight_current_layer = + buffer_add_text_property_layer(g_state.completions_buffer); + buffer_add_update_hook(g_state.completions_buffer, update_comp_buffer, + &g_state); - if (strncmp(ctx->needle, buffer->name, strlen(ctx->needle)) == 0 && - ctx->ncompletions < ctx->max_ncompletions) { - ctx->completions[ctx->ncompletions] = (struct completion){ - .display = strdup(buffer->name), - .insert = strdup(buffer->name + strlen(ctx->needle)), - .complete = true, - }; - ++ctx->ncompletions; - } + g_state.keymap_id = (uint64_t)-1; + g_state.target = NULL; + + VEC_INIT(&g_state.buffer_completions, 50); + VEC_INIT(&g_state.completions, 50); + g_state.completion_index = 0; + g_state.insert_in_progress = false; + g_state.paused = false; } -static uint32_t complete_buffers(struct completion_context ctx, - void *userdata) { - struct buffers *buffers = (struct buffers *)userdata; - if (buffers == NULL) { - return 0; - } +void add_completion_providers(struct buffer *source, + struct completion_provider *providers, + uint32_t nproviders) { - struct text_chunk txt = {0}; - if (ctx.buffer == minibuffer_buffer()) { - txt = minibuffer_content(); - } else { - struct match_result start = - buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); - if (!start.found) { - start.at = (struct location){.line = ctx.location.line, .col = 0}; - return 0; + struct buffer_completion *comp = NULL; + VEC_FOR_EACH(&g_state.buffer_completions, struct buffer_completion * c) { + if (c->buffer == source) { + comp = c; + break; } - txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); } - char *needle = calloc(txt.nbytes + 1, sizeof(char)); - memcpy(needle, txt.text, txt.nbytes); - needle[txt.nbytes] = '\0'; + if (comp == NULL) { + VEC_APPEND(&g_state.buffer_completions, + struct buffer_completion * new_comp); + + uint32_t insert_hook_id = + buffer_add_insert_hook(source, on_buffer_insert, &g_state); + uint32_t remove_hook_id = + buffer_add_delete_hook(source, on_buffer_delete, &g_state); - if (txt.allocated) { - free(txt.text); + new_comp->buffer = source; + new_comp->insert_hook_id = insert_hook_id; + new_comp->remove_hook_id = remove_hook_id; + VEC_INIT(&new_comp->providers, nproviders); + comp = new_comp; } - struct needle_match_ctx match_ctx = (struct needle_match_ctx){ - .needle = needle, - .max_ncompletions = ctx.max_ncompletions, - .completions = ctx.completions, - .ncompletions = 0, - }; - buffers_for_each(buffers, buffer_matches, &match_ctx); + for (uint32_t i = 0; i < nproviders; ++i) { + VEC_PUSH(&comp->providers, providers[i]); + } +} - free(needle); - return match_ctx.ncompletions; +void complete(struct buffer *buffer, struct location at) { + update_completions(&g_state, buffer, at, false); } -static void command_matches(struct command *command, void *userdata) { - struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata; +void abort_completion(void) { + windows_close_popup(); - if (strncmp(ctx->needle, command->name, strlen(ctx->needle)) == 0 && - ctx->ncompletions < ctx->max_ncompletions) { - ctx->completions[ctx->ncompletions] = (struct completion){ - .display = strdup(command->name), - .insert = strdup(command->name + strlen(ctx->needle)), - .complete = true, - }; - ++ctx->ncompletions; + if (g_state.keymap_id != (uint64_t)-1) { + buffer_remove_keymap(g_state.keymap_id); } + + g_state.keymap_id = (uint64_t)-1; + g_state.target = NULL; } -static uint32_t complete_commands(struct completion_context ctx, - void *userdata) { +bool completion_active(void) { + return popup_window_visible() && + window_buffer(popup_window()) == g_state.completions_buffer; +} - struct commands *commands = (struct commands *)userdata; - if (commands == NULL) { - return 0; - } - struct text_chunk txt = {0}; - if (ctx.buffer == minibuffer_buffer()) { - txt = minibuffer_content(); - } else { - struct match_result start = - buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); - if (!start.found) { - start.at = (struct location){.line = ctx.location.line, .col = 0}; - return 0; +static void do_nothing(void *userdata) { (void)userdata; } + +static void cleanup_buffer_completion(struct buffer_completion *comp) { + buffer_remove_delete_hook(comp->buffer, comp->remove_hook_id, do_nothing); + buffer_remove_insert_hook(comp->buffer, comp->insert_hook_id, do_nothing); + + VEC_FOR_EACH(&comp->providers, struct completion_provider * provider) { + if (provider->cleanup != NULL) { + provider->cleanup(provider->userdata); } - txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); } - char *needle = calloc(txt.nbytes + 1, sizeof(char)); - memcpy(needle, txt.text, txt.nbytes); - needle[txt.nbytes] = '\0'; + VEC_DESTROY(&comp->providers); +} - if (txt.allocated) { - free(txt.text); +void disable_completion(struct buffer *buffer) { + VEC_FOR_EACH_INDEXED(&g_state.buffer_completions, + struct buffer_completion * comp, i) { + if (buffer == comp->buffer) { + VEC_SWAP(&g_state.buffer_completions, i, + VEC_SIZE(&g_state.buffer_completions) - 1); + VEC_POP(&g_state.buffer_completions, struct buffer_completion removed); + cleanup_buffer_completion(&removed); + } } +} - struct needle_match_ctx match_ctx = (struct needle_match_ctx){ - .needle = needle, - .max_ncompletions = ctx.max_ncompletions, - .completions = ctx.completions, - .ncompletions = 0, - }; - commands_for_each(commands, command_matches, &match_ctx); - - free(needle); - return match_ctx.ncompletions; +void destroy_completion(void) { + clear_completions(&g_state); + // clean up any active completions we might have + VEC_FOR_EACH(&g_state.buffer_completions, struct buffer_completion * comp) { + cleanup_buffer_completion(comp); + } + VEC_DESTROY(&g_state.buffer_completions); + VEC_DESTROY(&g_state.completions); } + +void pause_completion() { g_state.paused = true; } + +void resume_completion() { g_state.paused = false; } diff --git a/src/main/completion.h b/src/main/completion.h index f2ce186..25f1ea2 100644 --- a/src/main/completion.h +++ b/src/main/completion.h @@ -1,6 +1,8 @@ #ifndef _COMPLETION_H #define _COMPLETION_H +#include <stddef.h> + #include "dged/location.h" /** @file completion.h @@ -9,29 +11,22 @@ struct buffer; struct buffers; +struct buffer_view; struct commands; -/** - * A single completion. - */ +typedef struct region (*completion_render_fn)(void *, struct buffer *); +typedef void (*completion_selected_fn)(void *, struct buffer_view *); +typedef void (*completion_cleanup_fn)(void *); + struct completion { - /** The display text for the completion. */ - const char *display; - - /** The text to insert for this completion. */ - const char *insert; - - /** - * True if this completion item represent a fully expanded value. - * - * One example might be when the file completion represents a - * file (and not a directory) which means that there is not - * going to be more to complete after picking this completion - * item. - */ - bool complete; + void *data; + completion_render_fn render; + completion_selected_fn selected; + completion_cleanup_fn cleanup; }; +typedef void (*add_completions)(struct completion *, size_t); + /** * Context for calculating completions. */ @@ -40,20 +35,19 @@ struct completion_context { struct buffer *buffer; /** The current location in the buffer. */ - const struct location location; + struct location location; - /** The capacity of @ref completion_context.completions. */ - const uint32_t max_ncompletions; - - /** The resulting completions */ - struct completion *completions; + /** Callback for adding items to the completion list */ + add_completions add_completions; }; /** * A function that provides completions. */ -typedef uint32_t (*completion_fn)(struct completion_context ctx, - void *userdata); +typedef void (*completion_fn)(struct completion_context ctx, bool deletion, + void *userdata); + +typedef void (*provider_cleanup_fn)(void *); /** * A completion provider. @@ -62,54 +56,21 @@ struct completion_provider { /** Name of the completion provider */ char name[16]; - /** Completion function. Called to get new completions. */ + /** Completion function. Called to trigger retreival of new completions. */ completion_fn complete; + /** Cleanup function called when provider is destroyed. */ + provider_cleanup_fn cleanup; + /** Userdata sent to @ref completion_provider.complete */ void *userdata; }; /** - * Type of event that triggers a completion. - */ -enum completion_trigger_kind { - /** Completion is triggered on any input. */ - CompletionTrigger_Input = 0, - - /** Completion is triggered on a specific char. */ - CompletionTrigger_Char = 1, -}; - -/** - * Description for @c CompletionTrigger_Input. - */ -struct completion_trigger_input { - /** Trigger completion after this many chars */ - uint32_t nchars; - - /** Trigger an initial complete? */ - bool trigger_initially; -}; - -/** - * Completion trigger descriptor. - */ -struct completion_trigger { - /** Type of trigger. */ - enum completion_trigger_kind kind; - union completion_trigger_data { - uint32_t c; - struct completion_trigger_input input; - } data; -}; - -/** * Initialize the completion system. * - * @param buffers The buffer list to complete from. - * @param commands The command list to complete from. */ -void init_completion(struct buffers *buffers, struct commands *commands); +void init_completion(struct buffers *buffers); /** * Tear down the completion system. @@ -117,48 +78,23 @@ void init_completion(struct buffers *buffers, struct commands *commands); void destroy_completion(void); /** - * Callback for completion inserted. - */ -typedef void (*insert_cb)(void); - -/** * Enable completions in the buffer @p source. * * @param source [in] The buffer to provide completions for. - * @param trigger [in] The completion trigger to use for this completion. * @param providers [in] The completion providers to use. * @param nproviders [in] The number of providers in @p providers. - * @param on_completion_inserted [in] Callback to be called when a completion - * has been inserted. - */ -void enable_completion(struct buffer *source, struct completion_trigger trigger, - struct completion_provider *providers, - uint32_t nproviders, insert_cb on_completion_inserted); - -/** - * Create a new path completion provider. - * - * This provider completes filesystem paths. - * @returns A filesystem path @ref completion_provider. */ -struct completion_provider path_provider(void); +void add_completion_providers(struct buffer *source, + struct completion_provider *providers, + uint32_t nproviders); /** - * Create a new buffer completion provider. + * Trigger a completion at @ref at in @ref buffer. * - * This provider completes buffer names from the - * buffer list. - * @returns A buffer name @ref completion_provider. + * @param buffer [in] Buffer to complete in. + * @param at [in] The location in @ref buffer to provide completions at. */ -struct completion_provider buffer_provider(void); - -/** - * Create a new command completion provider. - * - * This provider completes registered command names. - * @returns A command name @ref completion_provider. - */ -struct completion_provider commands_provider(void); +void complete(struct buffer *buffer, struct location at); /** * Abort any active completion. @@ -173,10 +109,20 @@ void abort_completion(void); bool completion_active(void); /** - * Disable completion for @ref buffer. + * Get a pointer to the buffer used to hold completion items. + * + * @returns A pointer to the buffer holding completions. + */ +struct buffer *completion_buffer(void); + +/** + * Disable completion for @ref buffer, removing all providers. * * @param buffer [in] Buffer to disable completions for. */ void disable_completion(struct buffer *buffer); +void pause_completion(); +void resume_completion(); + #endif diff --git a/src/main/completion/buffer.c b/src/main/completion/buffer.c new file mode 100644 index 0000000..8074414 --- /dev/null +++ b/src/main/completion/buffer.c @@ -0,0 +1,148 @@ +#include "buffer.h" + +#include <string.h> + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/minibuffer.h" + +#include "main/completion.h" + +static bool is_space(const struct codepoint *c) { + // TODO: utf8 whitespace and other whitespace + return c->codepoint == ' '; +} + +typedef void (*on_buffer_selected_cb)(struct buffer *); + +struct buffer_completion { + struct buffer *buffer; + on_buffer_selected_cb on_buffer_selected; +}; + +struct buffer_provider_data { + struct buffers *buffers; + on_buffer_selected_cb on_buffer_selected; +}; + +static void buffer_comp_selected(void *data, struct buffer_view *target) { + struct buffer_completion *bc = (struct buffer_completion *)data; + buffer_set_text(target->buffer, (uint8_t *)bc->buffer->name, + strlen(bc->buffer->name)); + + abort_completion(); + bc->on_buffer_selected(bc->buffer); +} + +static struct region buffer_comp_render(void *data, + struct buffer *comp_buffer) { + struct buffer *buffer = ((struct buffer_completion *)data)->buffer; + struct location begin = buffer_end(comp_buffer); + buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)buffer->name, + strlen(buffer->name)); + + struct location end = buffer_end(comp_buffer); + buffer_newline(comp_buffer, buffer_end(comp_buffer)); + return region_new(begin, end); +} + +static void buffer_comp_cleanup(void *data) { + struct buffer_completion *bc = (struct buffer_completion *)data; + free(bc); +} + +struct needle_match_ctx { + const char *needle; + struct completion *completions; + uint32_t max_ncompletions; + uint32_t ncompletions; + on_buffer_selected_cb on_buffer_selected; +}; + +static void buffer_matches(struct buffer *buffer, void *userdata) { + struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata; + + if (strncmp(ctx->needle, buffer->name, strlen(ctx->needle)) == 0 && + ctx->ncompletions < ctx->max_ncompletions) { + + struct buffer_completion *comp_data = + calloc(1, sizeof(struct buffer_completion)); + comp_data->buffer = buffer; + comp_data->on_buffer_selected = ctx->on_buffer_selected; + ctx->completions[ctx->ncompletions] = (struct completion){ + .render = buffer_comp_render, + .selected = buffer_comp_selected, + .cleanup = buffer_comp_cleanup, + .data = comp_data, + }; + ++ctx->ncompletions; + } +} + +static void buffer_complete(struct completion_context ctx, bool deletion, + void *userdata) { + (void)deletion; + struct buffer_provider_data *pd = (struct buffer_provider_data *)userdata; + struct buffers *buffers = pd->buffers; + if (buffers == NULL) { + return; + } + + struct text_chunk txt = {0}; + if (ctx.buffer == minibuffer_buffer()) { + txt = minibuffer_content(); + } else { + struct match_result start = + buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); + if (!start.found) { + start.at = (struct location){.line = ctx.location.line, .col = 0}; + return; + } + txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); + } + + char *needle = calloc(txt.nbytes + 1, sizeof(char)); + memcpy(needle, txt.text, txt.nbytes); + needle[txt.nbytes] = '\0'; + + if (txt.allocated) { + free(txt.text); + } + + struct completion *completions = calloc(50, sizeof(struct completion)); + + struct needle_match_ctx match_ctx = (struct needle_match_ctx){ + .needle = needle, + .max_ncompletions = 50, + .completions = completions, + .ncompletions = 0, + .on_buffer_selected = pd->on_buffer_selected, + }; + + buffers_for_each(buffers, buffer_matches, &match_ctx); + ctx.add_completions(match_ctx.completions, match_ctx.ncompletions); + free(completions); + free(needle); +} + +static void cleanup_provider(void *data) { + struct buffer_provider_data *bpd = (struct buffer_provider_data *)data; + free(bpd); +} + +struct completion_provider +create_buffer_provider(struct buffers *buffers, + on_buffer_selected_cb on_buffer_selected) { + struct buffer_provider_data *data = + calloc(1, sizeof(struct buffer_provider_data)); + data->buffers = buffers; + data->on_buffer_selected = on_buffer_selected; + + return (struct completion_provider){ + .name = "buffers", + .complete = buffer_complete, + .userdata = data, + .cleanup = cleanup_provider, + }; +} diff --git a/src/main/completion/buffer.h b/src/main/completion/buffer.h new file mode 100644 index 0000000..c2b6d42 --- /dev/null +++ b/src/main/completion/buffer.h @@ -0,0 +1,18 @@ +#ifndef _MAIN_COMPLETION_BUFFER_H +#define _MAIN_COMPLETION_BUFFER_H + +struct buffer; +struct buffers; + +/** + * Create a new buffer completion provider. + * + * This provider completes buffer names from the + * buffer list. + * @returns A buffer name @ref completion_provider. + */ +struct completion_provider +create_buffer_provider(struct buffers *buffers, + void (*on_buffer_selected)(struct buffer *)); + +#endif diff --git a/src/main/completion/command.c b/src/main/completion/command.c new file mode 100644 index 0000000..e4900ed --- /dev/null +++ b/src/main/completion/command.c @@ -0,0 +1,151 @@ +#include "command.h" + +#include <stdbool.h> +#include <string.h> + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/command.h" +#include "dged/minibuffer.h" +#include "dged/utf8.h" + +#include "main/completion.h" + +static bool is_space(const struct codepoint *c) { + // TODO: utf8 whitespace and other whitespace + return c->codepoint == ' '; +} + +typedef void (*on_command_selected_cb)(struct command *); + +struct command_completion { + struct command *command; + on_command_selected_cb on_command_selected; +}; + +struct command_provider_data { + struct commands *commands; + on_command_selected_cb on_command_selected; +}; + +static void command_comp_selected(void *data, struct buffer_view *target) { + struct command_completion *cc = (struct command_completion *)data; + buffer_set_text(target->buffer, (uint8_t *)cc->command->name, + strlen(cc->command->name)); + + abort_completion(); + cc->on_command_selected(cc->command); +} + +static struct region command_comp_render(void *data, + struct buffer *comp_buffer) { + struct command *command = ((struct command_completion *)data)->command; + struct location begin = buffer_end(comp_buffer); + buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)command->name, + strlen(command->name)); + + struct location end = buffer_end(comp_buffer); + buffer_newline(comp_buffer, buffer_end(comp_buffer)); + + return region_new(begin, end); +} + +static void command_comp_cleanup(void *data) { + struct command_completion *cc = (struct command_completion *)data; + free(cc); +} + +struct needle_match_ctx { + const char *needle; + struct completion *completions; + uint32_t max_ncompletions; + uint32_t ncompletions; + on_command_selected_cb on_command_selected; +}; + +static void command_matches(struct command *command, void *userdata) { + struct needle_match_ctx *ctx = (struct needle_match_ctx *)userdata; + + if (strncmp(ctx->needle, command->name, strlen(ctx->needle)) == 0 && + ctx->ncompletions < ctx->max_ncompletions) { + + struct command_completion *comp_data = + calloc(1, sizeof(struct command_completion)); + comp_data->command = command; + comp_data->on_command_selected = ctx->on_command_selected; + ctx->completions[ctx->ncompletions] = (struct completion){ + .render = command_comp_render, + .selected = command_comp_selected, + .cleanup = command_comp_cleanup, + .data = comp_data, + }; + ++ctx->ncompletions; + } +} + +static void command_complete(struct completion_context ctx, bool deletion, + void *userdata) { + (void)deletion; + struct command_provider_data *pd = (struct command_provider_data *)userdata; + struct commands *commands = pd->commands; + if (commands == NULL) { + return; + } + + struct text_chunk txt = {0}; + if (ctx.buffer == minibuffer_buffer()) { + txt = minibuffer_content(); + } else { + struct match_result start = + buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); + if (!start.found) { + start.at = (struct location){.line = ctx.location.line, .col = 0}; + return; + } + txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); + } + + char *needle = calloc(txt.nbytes + 1, sizeof(char)); + memcpy(needle, txt.text, txt.nbytes); + needle[txt.nbytes] = '\0'; + + if (txt.allocated) { + free(txt.text); + } + + struct completion *completions = calloc(50, sizeof(struct completion)); + + struct needle_match_ctx match_ctx = (struct needle_match_ctx){ + .needle = needle, + .max_ncompletions = 50, + .completions = completions, + .ncompletions = 0, + .on_command_selected = pd->on_command_selected, + }; + + commands_for_each(commands, command_matches, &match_ctx); + ctx.add_completions(match_ctx.completions, match_ctx.ncompletions); + free(completions); + free(needle); +} + +static void cleanup_provider(void *data) { + struct command_provider_data *cpd = (struct command_provider_data *)data; + free(cpd); +} + +struct completion_provider +create_commands_provider(struct commands *commands, + on_command_selected_cb on_command_selected) { + struct command_provider_data *data = + calloc(1, sizeof(struct command_provider_data)); + data->commands = commands; + data->on_command_selected = on_command_selected; + + return (struct completion_provider){ + .name = "commands", + .complete = command_complete, + .userdata = data, + .cleanup = cleanup_provider, + }; +} diff --git a/src/main/completion/command.h b/src/main/completion/command.h new file mode 100644 index 0000000..c25df57 --- /dev/null +++ b/src/main/completion/command.h @@ -0,0 +1,17 @@ +#ifndef _MAIN_COMPLETION_COMMAND_H +#define _MAIN_COMPLETION_COMMAND_H + +struct command; +struct commands; + +/** + * Create a new command completion provider. + * + * This provider completes registered command names. + * @returns A command name @ref completion_provider. + */ +struct completion_provider +create_commands_provider(struct commands *, + void (*on_command_selected)(struct command *)); + +#endif diff --git a/src/main/completion/path.c b/src/main/completion/path.c new file mode 100644 index 0000000..708da3d --- /dev/null +++ b/src/main/completion/path.c @@ -0,0 +1,268 @@ +#define _DEFAULT_SOURCE +#include "path.h" + +#include <dirent.h> +#include <errno.h> +#include <libgen.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/display.h" +#include "dged/minibuffer.h" +#include "dged/path.h" +#include "dged/s8.h" +#include "dged/utf8.h" + +static bool is_space(const struct codepoint *c) { + // TODO: utf8 whitespace and other whitespace + return c->codepoint == ' '; +} + +typedef void (*on_complete_path_cb)(void); + +struct path_completion { + struct s8 name; + struct region replace; + unsigned char type; + on_complete_path_cb on_complete_path; +}; + +static void path_selected(void *data, struct buffer_view *target) { + struct path_completion *comp_path = (struct path_completion *)data; + struct location loc = buffer_delete(target->buffer, comp_path->replace); + loc = buffer_add(target->buffer, loc, (uint8_t *)comp_path->name.s, + comp_path->name.l); + buffer_view_goto(target, loc); + switch (comp_path->type) { + case DT_DIR: + if (s8eq(comp_path->name, s8("."))) { + // trigger "dired" in this case + abort_completion(); + comp_path->on_complete_path(); + return; + } + + buffer_view_add(target, (uint8_t *)"/", 1); + break; + default: + break; + } + + // if the user selected a "normal" file, + // the completion is finished + if (comp_path->type == DT_REG) { + abort_completion(); + comp_path->on_complete_path(); + } else { + complete(target->buffer, target->dot); + } +} + +static struct region path_render(void *data, struct buffer *comp_buffer) { + struct path_completion *comp_path = (struct path_completion *)data; + + struct location start = buffer_end(comp_buffer); + buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)comp_path->name.s, + comp_path->name.l); + switch (comp_path->type) { + case DT_DIR: + if (!(s8eq(comp_path->name, s8(".")) || s8eq(comp_path->name, s8("..")))) { + buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)"/", 1); + struct location end = buffer_end(comp_buffer); + buffer_add_text_property(comp_buffer, start, end, + (struct text_property){ + .start = start, + .end = end, + .type = TextProperty_Colors, + .data.colors = + (struct text_property_colors){ + .set_fg = true, + .fg = Color_Magenta, + }, + }); + } + break; + case DT_LNK: { + struct location end = buffer_end(comp_buffer); + buffer_add_text_property(comp_buffer, start, end, + (struct text_property){ + .start = start, + .end = end, + .type = TextProperty_Colors, + .data.colors = + (struct text_property_colors){ + .set_fg = true, + .fg = Color_Green, + }, + }); + } break; + default: + break; + } + + struct location end = buffer_end(comp_buffer); + buffer_add(comp_buffer, buffer_end(comp_buffer), (uint8_t *)"\n", 1); + + return region_new(start, end); +} + +static void path_cleanup(void *data) { + struct path_completion *comp_path = (struct path_completion *)data; + s8delete(comp_path->name); + free(comp_path); +} + +static int cmp_path_completions(const void *comp_a, const void *comp_b) { + struct completion *ca = (struct completion *)comp_a; + struct completion *cb = (struct completion *)comp_b; + struct path_completion *a = (struct path_completion *)ca->data; + struct path_completion *b = (struct path_completion *)cb->data; + return s8cmp(a->name, b->name); +} + +static bool is_hidden(const char *filename) { + return filename[0] == '.' && filename[1] != '\0' && filename[1] != '.'; +} + +static bool fuzzy_match_filename(const char *haystack, const char *needle) { + for (; *haystack; ++haystack) { + const char *h = haystack; + const char *n = needle; + + while (*h && *n && *h == *n) { + ++h; + ++n; + } + + // if we reached the end of needle, we found a match + if (!*n) { + return true; + } + } + + return false; +} + +static void path_complete(struct completion_context ctx, bool deletion, + void *on_complete_path) { + (void)deletion; + + // obtain path from the buffer + struct text_chunk txt = {0}; + struct location needle_end = ctx.location; + if (ctx.buffer == minibuffer_buffer()) { + txt = minibuffer_content(); + needle_end = buffer_end(minibuffer_buffer()); + } else { + struct match_result start = + buffer_find_prev_in_line(ctx.buffer, ctx.location, is_space); + if (!start.found) { + start.at = (struct location){.line = ctx.location.line, .col = 0}; + return; + } + txt = buffer_region(ctx.buffer, region_new(start.at, ctx.location)); + } + + char *path = calloc(txt.nbytes + 1, sizeof(char)); + memcpy(path, txt.text, txt.nbytes); + path[txt.nbytes] = '\0'; + + if (txt.allocated) { + free(txt.text); + } + + uint32_t n = 0; + char *p1 = to_abspath(path); + char *p2 = strdup(p1); + + size_t inlen = strlen(path); + + const char *dir = p1; + const char *file = ""; + + // check the input path here since + // to_abspath removes trailing slashes + if (inlen > 0 && path[inlen - 1] != '/') { + dir = dirname(p1); + file = basename(p2); + } + + struct completion *completions = calloc(50, sizeof(struct completion)); + + DIR *d = opendir(dir); + if (d == NULL) { + goto done; + } + + errno = 0; + size_t filelen = strlen(file); + size_t file_nchars = utf8_nchars((uint8_t *)file, filelen); + struct location needle_start = (struct location){ + .line = needle_end.line, + .col = needle_end.col - file_nchars, + }; + + bool file_is_curdir = filelen == 1 && file[0] == '.'; + while (n < 50) { + struct dirent *de = readdir(d); + if (de == NULL && errno != 0) { + // skip the erroring entry + errno = 0; + continue; + } else if (de == NULL && errno == 0) { + break; + } + + switch (de->d_type) { + case DT_DIR: + case DT_REG: + case DT_LNK: + if (!is_hidden(de->d_name) && (filelen == 0 || file_is_curdir || + fuzzy_match_filename(de->d_name, file))) { + + struct path_completion *comp_data = + calloc(1, sizeof(struct path_completion)); + comp_data->name = s8new(de->d_name, strlen(de->d_name)); + comp_data->replace = region_new(needle_start, needle_end); + comp_data->type = de->d_type; + comp_data->on_complete_path = on_complete_path; + + completions[n] = (struct completion){ + .data = comp_data, + .render = path_render, + .selected = path_selected, + .cleanup = path_cleanup, + }; + + ++n; + } + break; + } + } + + closedir(d); + +done: + free(path); + free(p1); + free(p2); + + qsort(completions, n, sizeof(struct completion), cmp_path_completions); + ctx.add_completions(completions, n); + + free(completions); +} + +struct completion_provider +create_path_provider(void (*on_complete_path)(void)) { + return (struct completion_provider){ + .name = "path", + .complete = path_complete, + .userdata = on_complete_path, + }; +} diff --git a/src/main/completion/path.h b/src/main/completion/path.h new file mode 100644 index 0000000..407cae7 --- /dev/null +++ b/src/main/completion/path.h @@ -0,0 +1,14 @@ +#ifndef _MAIN_COMPLETION_PATH_H +#define _MAIN_COMPLETION_PATH_H + +#include "main/completion.h" + +/** + * Create a new path completion provider. + * + * This provider completes filesystem paths. + * @returns A filesystem path @ref completion_provider. + */ +struct completion_provider create_path_provider(void (*on_complete_path)(void)); + +#endif diff --git a/src/main/frame-hooks.c b/src/main/frame-hooks.c new file mode 100644 index 0000000..ae7bc1e --- /dev/null +++ b/src/main/frame-hooks.c @@ -0,0 +1,27 @@ +#include "frame-hooks.h" + +#include "dged/hook.h" + +HOOK_IMPL_NO_REMOVE(next_frame, next_frame_cb); + +static next_frame_hook_vec g_next_frame_hooks; +static uint32_t g_next_frame_hook_id; + +void init_frame_hooks(void) { VEC_INIT(&g_next_frame_hooks, 16); } + +void teardown_frame_hooks(void) { VEC_DESTROY(&g_next_frame_hooks); } + +void run_next_frame(next_frame_cb callback, void *userdata) { + insert_next_frame_hook(&g_next_frame_hooks, &g_next_frame_hook_id, callback, + userdata); +} + +size_t dispatch_next_frame_hooks() { + size_t nhooks = VEC_SIZE(&g_next_frame_hooks); + if (nhooks > 0) { + dispatch_hook_no_args(&g_next_frame_hooks, struct next_frame_hook); + VEC_CLEAR(&g_next_frame_hooks); + } + + return nhooks; +} diff --git a/src/main/frame-hooks.h b/src/main/frame-hooks.h new file mode 100644 index 0000000..fc382fc --- /dev/null +++ b/src/main/frame-hooks.h @@ -0,0 +1,13 @@ +#ifndef _FRAME_HOOKS_H +#define _FRAME_HOOKS_H + +#include <stddef.h> + +typedef void (*next_frame_cb)(void *); + +void init_frame_hooks(void); +void teardown_frame_hooks(void); +void run_next_frame(next_frame_cb callback, void *userdata); +size_t dispatch_next_frame_hooks(void); + +#endif diff --git a/src/main/lsp.c b/src/main/lsp.c index d56ca07..5886ea7 100644 --- a/src/main/lsp.c +++ b/src/main/lsp.c @@ -1,38 +1,556 @@ #include "lsp.h" +#include "dged/binding.h" #include "dged/buffer.h" +#include "dged/buffer_view.h" #include "dged/buffers.h" +#include "dged/command.h" +#include "dged/display.h" #include "dged/hash.h" #include "dged/hashmap.h" -#include "dged/lsp.h" +#include "dged/lang.h" #include "dged/minibuffer.h" #include "dged/reactor.h" #include "dged/settings.h" +#include "dged/window.h" -HASHMAP_ENTRY_TYPE(lsp_entry, struct lsp *); +#include "lsp/references.h" +#include "main/bindings.h" +#include "main/completion.h" -HASHMAP(struct lsp_entry) g_lsp_clients; +#include "lsp/actions.h" +#include "lsp/choice-buffer.h" +#include "lsp/completion.h" +#include "lsp/diagnostics.h" +#include "lsp/format.h" +#include "lsp/goto.h" +#include "lsp/help.h" +#include "lsp/rename.h" + +struct lsp_pending_request { + uint64_t request_id; + response_handler handler; + void *userdata; +}; + +struct lsp_server { + struct lsp *lsp; + uint32_t restarts; + struct s8 lang_id; + struct lsp_pending_request pending_requests[16]; + + bool initialized; + + enum text_document_sync_kind sync_kind; + bool send_open_close; + bool send_save; + + enum position_encoding_kind position_encoding; + + struct lsp_diagnostics *diagnostics; + struct completion_ctx *completion_ctx; +}; + +HASHMAP_ENTRY_TYPE(lsp_entry, struct lsp_server); + +static struct lsp_data { + HASHMAP(struct lsp_entry) clients; + struct keymap keymap; + struct keymap all_keymap; -static struct create_data { struct reactor *reactor; struct buffers *buffers; -} g_create_data; -static void log_message(int type, struct s8 msg) { - (void)type; - message("%s", msg); + struct buffer *current_diagnostic_buffer; + uint64_t current_request_id; +} g_lsp_data; + +struct lsp *lsp_backend(struct lsp_server *server) { return server->lsp; } + +struct lsp_server *lsp_server_for_lang_id(const char *id) { + HASHMAP_GET(&g_lsp_data.clients, struct lsp_entry, id, + struct lsp_server * server); + return server; } -static void create_lsp_client(struct buffer *buffer, void *userdata) { +static uint32_t bytepos_to_column(struct text_chunk *text, uint32_t bytecol) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(text->text, text->nbytes, 0); + uint32_t ncols = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < bytecol) { + col += codepoint->nbytes; + ncols += unicode_visual_char_width(codepoint); + } + + return ncols; +} + +static uint32_t codepoint_pos_to_column(struct text_chunk *text, + uint32_t codepoint_col) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(text->text, text->nbytes, 0); + uint32_t ncols = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && + col < codepoint_col) { + ++col; + ncols += unicode_visual_char_width(codepoint); + } + + return ncols; +} + +static uint32_t codeunit_pos_to_column(struct text_chunk *text, + uint32_t codeunit_col) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(text->text, text->nbytes, 0); + uint32_t ncols = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && + col < codeunit_col) { + col += codepoint->codepoint >= 0x010000 ? 2 : 1; + ncols += unicode_visual_char_width(codepoint); + } + + return ncols; +} + +struct region lsp_range_to_coordinates(struct lsp_server *server, + struct buffer *buffer, + struct region range) { + + uint32_t (*col_converter)(struct text_chunk *, uint32_t) = + codeunit_pos_to_column; + + switch (server->position_encoding) { + case PositionEncoding_Utf8: + col_converter = bytepos_to_column; + break; + + case PositionEncoding_Utf16: + col_converter = codeunit_pos_to_column; + break; + + case PositionEncoding_Utf32: + col_converter = codepoint_pos_to_column; + break; + } + + struct region reg = range; + struct text_chunk beg_line = buffer_line(buffer, range.begin.line); + reg.begin.col = col_converter(&beg_line, range.begin.col); + + struct text_chunk end_line = beg_line; + if (range.begin.line != range.end.line) { + end_line = buffer_line(buffer, range.end.line); + } + reg.end.col = col_converter(&end_line, range.end.col); + + if (beg_line.allocated) { + free(beg_line.text); + } + + if (range.begin.line != range.end.line && end_line.allocated) { + free(end_line.text); + } + + return reg; +} + +uint64_t new_pending_request(struct lsp_server *server, + response_handler handler, void *userdata) { + for (int i = 0; i < 16; ++i) { + if (server->pending_requests[i].request_id == (uint64_t)-1) { + ++g_lsp_data.current_request_id; + server->pending_requests[i].request_id = g_lsp_data.current_request_id; + server->pending_requests[i].handler = handler; + server->pending_requests[i].userdata = userdata; + return g_lsp_data.current_request_id; + } + } + + return -1; +} + +static bool +request_response_received(struct lsp_server *server, uint64_t id, + struct lsp_pending_request **pending_request) { + for (int i = 0; i < 16; ++i) { + if (server->pending_requests[i].request_id == id) { + server->pending_requests[i].request_id = (uint64_t)-1; + *pending_request = &server->pending_requests[i]; + return true; + } + } + + return false; +} + +static void buffer_updated(struct buffer *buffer, void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + + diagnostic_vec *diagnostics = + diagnostics_for_buffer(server->diagnostics, buffer); + if (diagnostics == NULL) { + return; + } + + VEC_FOR_EACH(diagnostics, struct diagnostic * diag) { + struct text_property prop; + prop.type = TextProperty_Colors; + uint32_t color = diag_severity_color(diag->severity); + prop.data.colors = (struct text_property_colors){ + .set_bg = true, + .set_fg = true, + .fg = Color_White, + .bg = color, + .underline = true, + }; + + struct region reg = region_new( + diag->region.begin, buffer_previous_char(buffer, diag->region.end)); + + buffer_add_text_property(buffer, reg.begin, reg.end, prop); + + if (window_buffer(windows_get_active()) == buffer) { + struct buffer_view *bv = window_buffer_view(windows_get_active()); + + if (region_is_inside(diag->region, bv->dot)) { + size_t len = 0; + for (size_t i = 0; i < diag->message.l && diag->message.s[i] != '\n'; + ++i) { + ++len; + } + minibuffer_display_timeout(1, "%s: %.*s", + diag_severity_to_str(diag->severity), len, + diag->message.s); + } + } + } +} + +static void buffer_pre_save(struct buffer *buffer, void *userdata) { + (void)buffer; (void)userdata; +} + +static void format_on_save(struct buffer *buffer, struct lsp_server *server) { + struct setting *glob_fmt_on_save = settings_get("editor.format-on-save"); + struct setting *fmt_on_save = + lang_setting(&buffer->lang, "language-server.format-on-save"); + + if ((glob_fmt_on_save != NULL && glob_fmt_on_save->value.data.bool_value) || + (glob_fmt_on_save == NULL && fmt_on_save != NULL && + fmt_on_save->value.data.bool_value)) { + format_document_save(server, buffer); + } +} + +static void buffer_post_save(struct buffer *buffer, void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + if (server->send_save) { + struct versioned_text_document_identifier text_document = + versioned_identifier_from_buffer(buffer); - struct create_data *data = &g_create_data; + struct did_save_text_document_params params = { + .text_document = + (struct text_document_identifier){ + .uri = text_document.uri, + }, + }; + + struct s8 json_payload = did_save_text_document_params_to_json(¶ms); + + lsp_send(server->lsp, + lsp_create_notification(s8("textDocument/didSave"), json_payload)); + + versioned_text_document_identifier_free(&text_document); + s8delete(json_payload); + } + + format_on_save(buffer, server); +} + +static uint32_t count_codepoints(struct text_chunk *chunk, + uint32_t target_col) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0); + uint32_t ncodepoints = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) { + col += unicode_visual_char_width(codepoint); + ++ncodepoints; + } + + return ncodepoints; +} + +static uint32_t count_codeunits(struct text_chunk *chunk, uint32_t target_col) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0); + uint32_t ncodeunits = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) { + col += unicode_visual_char_width(codepoint); + ncodeunits += codepoint->codepoint >= 0x010000 ? 2 : 1; + } + + return ncodeunits; +} + +static uint32_t count_bytes(struct text_chunk *chunk, uint32_t target_col) { + struct utf8_codepoint_iterator iter = + create_utf8_codepoint_iterator(chunk->text, chunk->nbytes, 0); + uint32_t nbytes = 0, col = 0; + struct codepoint *codepoint = NULL; + while ((codepoint = utf8_next_codepoint(&iter)) != NULL && col < target_col) { + col += unicode_visual_char_width(codepoint); + nbytes += codepoint->nbytes; + } + + return nbytes; +} + +static struct region edit_location_to_lsp(struct buffer *buffer, + struct edit_location edit, + struct lsp_server *server) { + + struct region res = edit.coordinates; + if (server->position_encoding == PositionEncoding_Utf8) { + /* In this case, the buffer hook has already + * done the job for us. */ + res.begin.col = edit.bytes.begin.col; + res.end.col = edit.bytes.end.col; + return res; + } + + return region_to_lsp(buffer, res, server); +} + +struct region region_to_lsp(struct buffer *buffer, struct region region, + struct lsp_server *server) { + struct region res = region; + + uint32_t (*col_counter)(struct text_chunk *, uint32_t) = count_codeunits; + + switch (server->position_encoding) { + case PositionEncoding_Utf8: + col_counter = count_bytes; + return res; + + case PositionEncoding_Utf16: + col_counter = count_codeunits; + break; + + case PositionEncoding_Utf32: + col_counter = count_codepoints; + break; + } + + struct text_chunk beg_line = buffer_line(buffer, region.begin.line); + res.begin.col = col_counter(&beg_line, region.begin.col); + + struct text_chunk end_line = beg_line; + if (region.begin.line != region.end.line) { + end_line = buffer_line(buffer, region.end.line); + } + + res.end.col = col_counter(&end_line, region.end.col); + + if (beg_line.allocated) { + free(beg_line.text); + } + + if (end_line.allocated && region.begin.line != region.end.line) { + free(end_line.text); + } + + return res; +} + +static void buffer_text_changed(struct buffer *buffer, + struct edit_location range, bool delete, + void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + + struct text_chunk new_text = {0}; + switch (server->sync_kind) { + case TextDocumentSync_None: + return; + + case TextDocumentSync_Full: + new_text = + buffer_region(buffer, region_new((struct location){.line = 0, .col = 0}, + buffer_end(buffer))); + break; + + case TextDocumentSync_Incremental: + if (!delete) { + new_text = buffer_region(buffer, range.coordinates); + } + break; + } + + struct region reg = edit_location_to_lsp(buffer, range, server); + reg = delete ? reg : region_new(reg.begin, reg.begin); + struct text_document_content_change_event evt = { + .full_document = server->sync_kind == TextDocumentSync_Full, + .text = s8new((const char *)new_text.text, new_text.nbytes), + .range = reg, + }; + + struct versioned_text_document_identifier text_document = + versioned_identifier_from_buffer(buffer); + struct did_change_text_document_params params = { + .text_document = text_document, + .content_changes = &evt, + .ncontent_changes = 1, + }; + + struct s8 json_payload = did_change_text_document_params_to_json(¶ms); + + lsp_send(server->lsp, + lsp_create_notification(s8("textDocument/didChange"), json_payload)); + + versioned_text_document_identifier_free(&text_document); + s8delete(json_payload); + s8delete(evt.text); + + if (new_text.allocated) { + free(new_text.text); + } +} + +static void buffer_text_inserted(struct buffer *buffer, + struct edit_location inserted, + void *userdata) { + buffer_text_changed(buffer, inserted, false, userdata); +} + +static void buffer_text_deleted(struct buffer *buffer, + struct edit_location deleted, void *userdata) { + buffer_text_changed(buffer, deleted, true, userdata); +} + +static void send_did_open(struct lsp_server *server, struct buffer *buffer) { + if (!server->send_open_close) { + return; + } + + struct text_document_item doc = text_document_item_from_buffer(buffer); + struct did_open_text_document_params params = { + .text_document = doc, + }; + + struct s8 json_payload = did_open_text_document_params_to_json(¶ms); + lsp_send(server->lsp, + lsp_create_notification(s8("textDocument/didOpen"), json_payload)); + + text_document_item_free(&doc); + s8delete(json_payload); +} + +static void setup_completion(struct lsp_server *server, struct buffer *buffer) { + if (server->completion_ctx != NULL) { + enable_completion_for_buffer(server->completion_ctx, buffer); + } +} + +static void lsp_buffer_initialized(struct lsp_server *server, + struct buffer *buffer) { + if (s8eq(server->lang_id, s8(buffer->lang.id))) { + /* Needs to be a pre-delete hook since we need + * access to the deleted content to derive the + * correct UTF-8/16/32 position. + */ + buffer_add_pre_delete_hook(buffer, buffer_text_deleted, server); + buffer_add_insert_hook(buffer, buffer_text_inserted, server); + buffer_add_update_hook(buffer, buffer_updated, server); + buffer_add_pre_save_hook(buffer, buffer_pre_save, server); + buffer_add_post_save_hook(buffer, buffer_post_save, server); + + send_did_open(server, buffer); + setup_completion(server, buffer); + } +} + +static void apply_initialized(struct buffer *buffer, void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + lsp_buffer_initialized(server, buffer); +} + +static void handle_initialize(struct lsp_server *server, + struct lsp_response *response, void *userdata) { + (void)userdata; + + struct initialize_result res = + initialize_result_from_json(&response->value.result); + message("lsp server initialized: %.*s (%.*s)", res.server_info.name.l, + res.server_info.name.s, res.server_info.version.l, + res.server_info.version.s); + + lsp_send(server->lsp, lsp_create_notification(s8("initialized"), s8(""))); + + struct text_document_sync *tsync = &res.capabilities.text_document_sync; + server->sync_kind = tsync->kind; + server->send_open_close = tsync->open_close; + server->send_save = tsync->save; + server->position_encoding = res.capabilities.position_encoding; + + if (res.capabilities.supports_completion) { + struct completion_options *comp_opts = &res.capabilities.completion_options; + server->completion_ctx = create_completion_ctx( + server, (triggerchar_vec *)&comp_opts->trigger_characters); + } + + initialize_result_free(&res); + buffers_for_each(g_lsp_data.buffers, apply_initialized, server); + + server->initialized = true; +} + +static void init_lsp_client(struct lsp_server *server) { + if (lsp_restart_server(server->lsp) < 0) { + minibuffer_echo("failed to start language server %s process.", + lsp_server_name(server->lsp)); + return; + } + + // send some init info + struct initialize_params params = { + .process_id = lsp_server_pid(server->lsp), + .client_info = + { + .name = s8("dged"), + .version = s8("dev"), + }, + .client_capabilities = {}, + .workspace_folders = NULL, + .nworkspace_folders = 0, + }; + + uint64_t id = new_pending_request(server, handle_initialize, NULL); + struct s8 json_payload = initialize_params_to_json(¶ms); + lsp_send(server->lsp, lsp_create_request(id, s8("initialize"), json_payload)); + + s8delete(json_payload); +} + +static void create_lsp_client(struct buffer *buffer, void *userdata) { + (void)userdata; const char *id = buffer->lang.id; - HASHMAP_GET(&g_lsp_clients, struct lsp_entry, id, struct lsp * *lsp); - if (lsp == NULL) { + HASHMAP_GET(&g_lsp_data.clients, struct lsp_entry, id, + struct lsp_server * server); + if (server == NULL) { // we need to start a new server - struct setting *s = lang_setting(&buffer->lang, "language-server"); - if (!s) { // no language server set + struct setting *s = lang_setting(&buffer->lang, "language-server.command"); + if (!s) { + if (!lang_is_fundamental(&buffer->lang)) { + message("No language server set for %s. Set with " + "`languages.%s.language-server`.", + buffer->lang.id, buffer->lang.id); + } return; } @@ -40,69 +558,388 @@ static void create_lsp_client(struct buffer *buffer, void *userdata) { char bufname[1024] = {0}; snprintf(bufname, 1024, "*%s-lsp-stderr*", command[0]); - struct buffer *stderr_buf = buffers_find(data->buffers, bufname); + struct buffer *stderr_buf = buffers_find(g_lsp_data.buffers, bufname); if (stderr_buf == NULL) { struct buffer buf = buffer_create(bufname); buf.lazy_row_add = false; - stderr_buf = buffers_add(data->buffers, buf); + stderr_buf = buffers_add(g_lsp_data.buffers, buf); buffer_set_readonly(stderr_buf, true); } - struct lsp_client client_impl = { - .log_message = log_message, - }; struct lsp *new_lsp = - lsp_create(command, data->reactor, stderr_buf, client_impl, NULL); + lsp_create(command, g_lsp_data.reactor, stderr_buf, command[0]); if (new_lsp == NULL) { minibuffer_echo("failed to create language server %s", command[0]); - buffers_remove(data->buffers, bufname); + buffers_remove(g_lsp_data.buffers, bufname); return; } - HASHMAP_APPEND(&g_lsp_clients, struct lsp_entry, id, + HASHMAP_APPEND(&g_lsp_data.clients, struct lsp_entry, id, struct lsp_entry * new); - new->value = new_lsp; - - if (lsp_start_server(new_lsp) < 0) { - minibuffer_echo("failed to start language server %s process.", - lsp_server_name(new_lsp)); - return; + new->value = (struct lsp_server){ + .lsp = new_lsp, + .lang_id = s8new(id, strlen(id)), + .restarts = 0, + }; + for (int i = 0; i < 16; ++i) { + new->value.pending_requests[i].request_id = (uint64_t)-1; } + + new->value.diagnostics = diagnostics_create(); + + // support for this is determined later + new->value.completion_ctx = NULL; + + init_lsp_client(&new->value); + server = &new->value; + } + + /* An lsp for the language for this buffer is already started. + * if server is not initialized, it will get picked + * up anyway when handling the initialize response. */ + if (server->initialized) { + lsp_buffer_initialized(server, buffer); } } -static void set_default_lsp(const char *lang_id, const char *server) { +static void set_default_lsp(const char *lang_id, const char *command) { struct language l = lang_from_id(lang_id); if (!lang_is_fundamental(&l)) { lang_setting_set_default( - &l, "language-server", + &l, "language-server.command", (struct setting_value){.type = Setting_String, - .data.string_value = (char *)server}); + .data.string_value = (char *)command}); + lang_setting_set_default( + &l, "language-server.format-on-save", + (struct setting_value){.type = Setting_Bool, .data.bool_value = false}); + lang_destroy(&l); } } -void lang_servers_init(struct reactor *reactor, struct buffers *buffers) { - HASHMAP_INIT(&g_lsp_clients, 32, hash_name); +static struct s8 lsp_modeline(struct buffer_view *view, void *userdata) { + (void)userdata; + struct lsp_server *server = lsp_server_for_lang_id(view->buffer->lang.id); + if (server == NULL) { + return s8(""); + } + + return s8from_fmt( + "lsp: %s:%d", lsp_server_name(server->lsp), + lsp_server_running(server->lsp) ? lsp_server_pid(server->lsp) : 0); +} + +static uint32_t lsp_keymap_hook(struct buffer *buffer, struct keymap *keymaps[], + uint32_t max_nkeymaps, void *userdata) { + (void)userdata; + + if (max_nkeymaps < 2) { + return 0; + } + + uint32_t nadded = 1; + keymaps[0] = &g_lsp_data.all_keymap; + + if (lsp_server_for_lang_id(buffer->lang.id) != NULL) { + keymaps[1] = &g_lsp_data.keymap; + nadded = 2; + } + + return nadded; +} + +static int32_t lsp_restart_cmd(struct command_ctx ctx, int argc, + const char **argv) { + (void)ctx; + (void)argc; + (void)argv; + struct buffer *b = window_buffer(windows_get_active()); + + struct lsp_server *server = lsp_server_for_buffer(b); + if (server == NULL) { + return 0; + } + + lsp_restart_server(server->lsp); + return 0; +} + +void lang_servers_init(struct reactor *reactor, struct buffers *buffers, + struct commands *commands) { + HASHMAP_INIT(&g_lsp_data.clients, 32, hash_name); set_default_lsp("c", "clangd"); + set_default_lsp("cxx", "clangd"); set_default_lsp("rs", "rust-analyzer"); set_default_lsp("python", "pylsp"); - g_create_data.reactor = reactor; - g_create_data.buffers = buffers; - buffer_add_create_hook(create_lsp_client, NULL); + g_lsp_data.current_request_id = 0; + g_lsp_data.reactor = reactor; + g_lsp_data.buffers = buffers; + buffers_add_add_hook(buffers, create_lsp_client, NULL); + + struct command lsp_commands[] = { + {.name = "lsp-goto-definition", .fn = lsp_goto_def_cmd}, + {.name = "lsp-goto-declaration", .fn = lsp_goto_decl_cmd}, + {.name = "lsp-goto-implementation", .fn = lsp_goto_impl_cmd}, + {.name = "lsp-goto", .fn = lsp_goto_cmd}, + {.name = "lsp-goto-previous", .fn = lsp_goto_previous_cmd}, + {.name = "lsp-references", .fn = lsp_references_cmd}, + {.name = "lsp-restart", .fn = lsp_restart_cmd}, + {.name = "lsp-diagnostics", .fn = diagnostics_cmd}, + {.name = "lsp-next-diagnostic", .fn = next_diagnostic_cmd}, + {.name = "lsp-prev-diagnostic", .fn = prev_diagnostic_cmd}, + {.name = "lsp-code-actions", .fn = code_actions_cmd}, + {.name = "lsp-format", .fn = format_cmd}, + {.name = "lsp-rename", .fn = lsp_rename_cmd}, + {.name = "lsp-help", .fn = lsp_help_cmd}, + }; + + register_commands(commands, lsp_commands, + sizeof(lsp_commands) / sizeof(lsp_commands[0])); + + struct binding lsp_binds[] = { + BINDING(Meta, '.', "lsp-goto-definition"), + BINDING(Meta, '/', "lsp-goto"), + BINDING(Meta, '[', "lsp-prev-diagnostic"), + BINDING(Meta, ']', "lsp-next-diagnostic"), + BINDING(Meta, 'a', "lsp-code-actions"), + BINDING(Meta, '=', "lsp-format"), + BINDING(Meta, 'r', "lsp-rename"), + BINDING(Meta, 'h', "lsp-help"), + }; + + struct binding global_binds[] = { + BINDING(Meta, ',', "lsp-goto-previous"), + }; + + g_lsp_data.keymap = keymap_create("lsp", 32); + keymap_bind_keys(&g_lsp_data.keymap, lsp_binds, + sizeof(lsp_binds) / sizeof(lsp_binds[0])); + g_lsp_data.all_keymap = keymap_create("lsp-global", 32); + keymap_bind_keys(&g_lsp_data.all_keymap, global_binds, + sizeof(global_binds) / sizeof(global_binds[0])); + buffer_add_keymaps_hook(lsp_keymap_hook, NULL); + + buffer_view_add_modeline_hook(lsp_modeline, NULL); + + init_goto(32, buffers); +} + +void apply_edits_buffer(struct lsp_server *server, struct buffer *buffer, + text_edit_vec edits, struct location *point) { + VEC_FOR_EACH_REVERSE(&edits, struct text_edit * edit) { + struct region reg = lsp_range_to_coordinates(server, buffer, edit->range); + struct location at = reg.end; + if (region_has_size(reg)) { + if (point != NULL) { + + if (reg.end.line == point->line) { + point->col -= reg.end.col > point->col ? 0 : point->col - reg.end.col; + } + + uint64_t lines_deleted = reg.end.line - reg.begin.line; + if (lines_deleted > 0 && reg.end.line <= point->line) { + point->line -= lines_deleted; + } + } + at = buffer_delete(buffer, reg); + } + + struct location after = + buffer_add(buffer, at, edit->new_text.s, edit->new_text.l); + if (point != NULL) { + if (after.line == point->line) { + point->col += after.col; + } + + uint64_t lines_added = after.line - at.line; + if (lines_added > 0 && after.line <= point->line) { + point->line += lines_added; + } + } + } +} + +bool apply_edits(struct lsp_server *server, + const struct workspace_edit *ws_edit) { + pause_completion(); + + VEC_FOR_EACH(&ws_edit->changes, struct text_edit_pair * pair) { + if (VEC_EMPTY(&pair->edits)) { + continue; + } + + const char *p = s8tocstr(pair->uri); + struct buffer *b = buffers_find_by_filename(g_lsp_data.buffers, &p[7]); + + if (b == NULL) { + struct buffer new_buf = buffer_from_file(&p[7]); + b = buffers_add(g_lsp_data.buffers, new_buf); + } + + free((void *)p); + buffer_push_undo_boundary(b); + apply_edits_buffer(server, b, pair->edits, NULL); + buffer_push_undo_boundary(b); + } + + resume_completion(); + return true; +} + +static void handle_request(struct lsp_server *server, + struct lsp_request request) { + + struct s8 method = unescape_json_string(request.method); + if (s8eq(method, s8("workspace/applyEdit"))) { + struct workspace_edit ws_edit = workspace_edit_from_json(&request.params); + apply_edits(server, &ws_edit); + workspace_edit_free(&ws_edit); + } else { + message("unhandled lsp request (%s): id %d: %.*s", + lsp_server_name(server->lsp), request.id, request.method.l, + request.method.s); + } + + s8delete(method); +} + +static void handle_response(struct lsp_server *server, + struct lsp_response response) { + if (response.ok) { + struct lsp_pending_request *pending = NULL; + if (!request_response_received(server, response.id, &pending)) { + message("received response for id %d, server %s, which has no handler " + "registered", + response.id, lsp_server_name(server->lsp)); + } + + if (pending->handler != NULL) { + pending->handler(server, &response, pending->userdata); + } + } else { + struct s8 errmsg = response.value.error.message; + minibuffer_echo("lsp error (%s), id %d: %.*s", lsp_server_name(server->lsp), + response.id, errmsg.l, errmsg.s); + } +} + +static void handle_notification(struct lsp_server *server, + struct lsp_notification notification) { + struct s8 method = unescape_json_string(notification.method); + if (s8eq(method, s8("textDocument/publishDiagnostics"))) { + handle_publish_diagnostics(server, g_lsp_data.buffers, ¬ification); + } + + s8delete(method); +} + +#define MAX_RESTARTS 10 + +static void restart_if_needed(struct lsp_server *server) { + // if we successfully initialized the server, we can be sure + // it is up and running + if (lsp_server_running(server->lsp) && server->initialized) { + server->restarts = 0; + return; + } + + if (!lsp_server_running(server->lsp)) { + if (server->restarts < MAX_RESTARTS) { + message("restarting \"%s\" (%d/%d)...", lsp_server_name(server->lsp), + server->restarts + 1, MAX_RESTARTS); + init_lsp_client(server); + ++server->restarts; + + if (server->restarts == MAX_RESTARTS) { + minibuffer_echo("lsp \"%s\" has crashed %d times, giving up...", + lsp_server_name(server->lsp), MAX_RESTARTS); + } + } else { + // server is crashed and can only be restarted manually now + lsp_stop_server(server->lsp); + } + } } void lang_servers_update(void) { - HASHMAP_FOR_EACH(&g_lsp_clients, struct lsp_entry * e) { - lsp_update(e->value, NULL, 0); + + HASHMAP_FOR_EACH(&g_lsp_data.clients, struct lsp_entry * e) { + restart_if_needed(&e->value); + + struct lsp_message msgs[128]; + uint32_t msgs_received = lsp_update(e->value.lsp, msgs, 128); + + if (msgs_received == 0 || msgs_received == (uint32_t)-1) { + continue; + } + + char bufname[1024] = {0}; + snprintf(bufname, 1024, "*%s-lsp-messages*", lsp_server_name(e->value.lsp)); + struct buffer *output_buf = buffers_find(g_lsp_data.buffers, bufname); + if (output_buf == NULL) { + struct buffer buf = buffer_create(bufname); + buf.lazy_row_add = false; + output_buf = buffers_add(g_lsp_data.buffers, buf); + } + + buffer_set_readonly(output_buf, false); + for (uint32_t mi = 0; mi < msgs_received; ++mi) { + struct lsp_message *msg = &msgs[mi]; + buffer_add(output_buf, buffer_end(output_buf), msg->payload.s, + msg->payload.l); + buffer_add(output_buf, buffer_end(output_buf), (uint8_t *)"\n", 1); + + switch (msg->type) { + case Lsp_Response: + handle_response(&e->value, msg->message.response); + break; + + case Lsp_Request: + handle_request(&e->value, msg->message.request); + break; + + case Lsp_Notification: + handle_notification(&e->value, msg->message.notification); + break; + } + + lsp_message_destroy(msg); + } + + buffer_set_readonly(output_buf, true); } } +static void lang_server_teardown(struct lsp_server *server) { + destroy_goto(); + lsp_stop_server(server->lsp); + lsp_destroy(server->lsp); + s8delete(server->lang_id); +} + void lang_servers_teardown(void) { - HASHMAP_FOR_EACH(&g_lsp_clients, struct lsp_entry * e) { - lsp_stop_server(e->value); + HASHMAP_FOR_EACH(&g_lsp_data.clients, struct lsp_entry * e) { + diagnostics_destroy(e->value.diagnostics); + + if (e->value.completion_ctx != NULL) { + destroy_completion_ctx(e->value.completion_ctx); + } + + lang_server_teardown(&e->value); } + + keymap_destroy(&g_lsp_data.keymap); + keymap_destroy(&g_lsp_data.all_keymap); + HASHMAP_DESTROY(&g_lsp_data.clients); +} + +struct lsp_server *lsp_server_for_buffer(struct buffer *buffer) { + return lsp_server_for_lang_id(buffer->lang.id); +} + +struct lsp_diagnostics *lsp_server_diagnostics(struct lsp_server *server) { + return server->diagnostics; } diff --git a/src/main/lsp.h b/src/main/lsp.h index 736282d..27d8c93 100644 --- a/src/main/lsp.h +++ b/src/main/lsp.h @@ -1,11 +1,53 @@ #ifndef _MAIN_LSP_H #define _MAIN_LSP_H +#include <stddef.h> + +#include "dged/location.h" +#include "dged/lsp.h" +#include "dged/s8.h" +#include "dged/vec.h" + +#include "lsp/types.h" + struct reactor; struct buffers; +struct commands; -void lang_servers_init(struct reactor *reactor, struct buffers *buffers); +void lang_servers_init(struct reactor *reactor, struct buffers *buffers, + struct commands *commands); void lang_servers_update(void); void lang_servers_teardown(void); +struct lsp_server; +struct buffer; +struct workspace_edit; + +struct lsp_server *lsp_server_for_lang_id(const char *id); +struct lsp_server *lsp_server_for_buffer(struct buffer *buffer); + +void lsp_server_reload(struct lsp_server *server); +void lsp_server_shutdown(struct lsp_server *server); +struct lsp *lsp_backend(struct lsp_server *server); + +bool apply_edits(struct lsp_server *server, + const struct workspace_edit *ws_edit); + +void apply_edits_buffer(struct lsp_server *, struct buffer *, text_edit_vec, + struct location *); + +typedef void (*response_handler)(struct lsp_server *, struct lsp_response *, + void *); +uint64_t new_pending_request(struct lsp_server *server, + response_handler handler, void *userdata); + +struct region lsp_range_to_coordinates(struct lsp_server *server, + struct buffer *buffer, + struct region range); + +struct region region_to_lsp(struct buffer *buffer, struct region region, + struct lsp_server *server); + +struct lsp_diagnostics *lsp_server_diagnostics(struct lsp_server *server); + #endif diff --git a/src/main/lsp/actions.c b/src/main/lsp/actions.c new file mode 100644 index 0000000..ea792a1 --- /dev/null +++ b/src/main/lsp/actions.c @@ -0,0 +1,129 @@ +#include "actions.h" + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/lsp.h" +#include "dged/minibuffer.h" +#include "dged/window.h" +#include "main/lsp.h" +#include "main/lsp/diagnostics.h" + +#include "choice-buffer.h" +#include "types.h" + +static struct code_actions g_code_actions_result = {}; + +static void code_action_command_selected(void *selected, void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + struct lsp_command *command = (struct lsp_command *)selected; + struct s8 json_payload = lsp_command_to_json(command); + + uint64_t id = new_pending_request(server, NULL, NULL); + lsp_send( + lsp_backend(server), + lsp_create_request(id, s8("workspace/executeCommand"), json_payload)); + + s8delete(json_payload); +} + +static void code_action_selected(void *selected, void *userdata) { + struct lsp_server *server = (struct lsp_server *)userdata; + struct code_action *action = (struct code_action *)selected; + + if (action->has_edit) { + apply_edits(server, &action->edit); + } + + if (action->has_command) { + struct s8 json_payload = lsp_command_to_json(&action->command); + + uint64_t id = new_pending_request(server, NULL, NULL); + lsp_send( + lsp_backend(server), + lsp_create_request(id, s8("workspace/executeCommand"), json_payload)); + s8delete(json_payload); + } +} + +static void code_action_closed(void *userdata) { + (void)userdata; + lsp_code_actions_free(&g_code_actions_result); +} + +static void handle_code_actions_response(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + struct code_actions actions = + lsp_code_actions_from_json(&response->value.result); + + struct buffers *buffers = (struct buffers *)userdata; + + if (VEC_SIZE(&actions.commands) == 0 && + VEC_SIZE(&actions.code_actions) == 0) { + minibuffer_echo_timeout(4, "no code actions available"); + lsp_code_actions_free(&actions); + } else { + g_code_actions_result = actions; + struct choice_buffer *buf = + choice_buffer_create(s8("Code Actions"), buffers, code_action_selected, + code_action_closed, NULL, server); + + VEC_FOR_EACH(&actions.code_actions, struct code_action * action) { + struct s8 line = + s8from_fmt("%.*s, (%.*s)", action->title.l, action->title.s, + action->kind.l, action->kind.s); + choice_buffer_add_choice_with_callback(buf, line, action, + code_action_selected); + s8delete(line); + } + + VEC_FOR_EACH(&actions.commands, struct lsp_command * command) { + struct s8 line = s8from_fmt("%.*s", command->title.l, command->title.s); + choice_buffer_add_choice_with_callback(buf, line, command, + code_action_command_selected); + s8delete(line); + } + } +} + +int32_t code_actions_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(windows_get_active()); + + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + return 0; + } + + uint64_t id = + new_pending_request(server, handle_code_actions_response, ctx.buffers); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(bv->buffer); + struct code_action_params params = { + .text_document.uri = doc.uri, + .range = region_new(bv->dot, bv->dot), + }; + + VEC_INIT(¶ms.context.diagnostics, 8); + + diagnostic_vec *d = + diagnostics_for_buffer(lsp_server_diagnostics(server), bv->buffer); + if (d != NULL) { + VEC_FOR_EACH(d, struct diagnostic * diag) { + if (location_is_between(bv->dot, diag->region.begin, diag->region.end)) { + VEC_PUSH(¶ms.context.diagnostics, *diag); + } + } + } + + struct s8 json_payload = code_action_params_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/codeAction"), json_payload)); + + VEC_DESTROY(¶ms.context.diagnostics); + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); + return 0; +} diff --git a/src/main/lsp/actions.h b/src/main/lsp/actions.h new file mode 100644 index 0000000..59b4d36 --- /dev/null +++ b/src/main/lsp/actions.h @@ -0,0 +1,10 @@ +#ifndef _ACTIONS_H +#define _ACTIONS_H + +#include <stdint.h> + +#include "dged/command.h" + +int32_t code_actions_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/choice-buffer.c b/src/main/lsp/choice-buffer.c new file mode 100644 index 0000000..44186bd --- /dev/null +++ b/src/main/lsp/choice-buffer.c @@ -0,0 +1,201 @@ +#include "choice-buffer.h" + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/command.h" +#include "dged/display.h" +#include "dged/location.h" + +#include "main/bindings.h" + +struct choice { + struct region region; + void *data; + select_callback callback; +}; + +struct choice_buffer { + struct buffers *buffers; + struct buffer *buffer; + VEC(struct choice) choices; + + abort_callback abort_cb; + select_callback select_cb; + update_callback update_cb; + void *userdata; + + uint32_t buffer_removed_hook; + + struct command enter_pressed; + struct command q_pressed; +}; + +static void delete_choice_buffer(struct choice_buffer *buffer, + bool delete_underlying); + +static void underlying_buffer_destroyed(struct buffer *buffer, + void *choice_buffer) { + (void)buffer; + struct choice_buffer *cb = (struct choice_buffer *)choice_buffer; + + // run this with false since the underlying buffer is already + // being deleted + delete_choice_buffer(cb, false); +} + +static int32_t enter_pressed_fn(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + struct choice_buffer *cb = (struct choice_buffer *)ctx.userdata; + struct window *w = window_find_by_buffer(cb->buffer); + if (w == NULL) { + return 0; + } + + struct buffer_view *bv = window_buffer_view(w); + + VEC_FOR_EACH(&cb->choices, struct choice * choice) { + if (location_is_between(bv->dot, choice->region.begin, + choice->region.end)) { + if (choice->callback != NULL) { + choice->callback(choice->data, cb->userdata); + } else { + cb->select_cb(choice->data, cb->userdata); + } + + delete_choice_buffer(cb, true); + return 0; + } + } + + return 0; +} + +static int32_t choice_buffer_close_fn(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + struct choice_buffer *cb = (struct choice_buffer *)ctx.userdata; + delete_choice_buffer(cb, true); + return 0; +} + +struct choice_buffer * +choice_buffer_create(struct s8 title, struct buffers *buffers, + select_callback selected, abort_callback aborted, + update_callback update, void *userdata) { + + struct choice_buffer *b = calloc(1, sizeof(struct choice_buffer)); + VEC_INIT(&b->choices, 16); + b->select_cb = selected; + b->abort_cb = aborted; + b->update_cb = update; + b->userdata = userdata; + b->buffers = buffers; + + // set up + struct buffer buf = buffer_create("*something-choices*"); + buf.lazy_row_add = false; + buf.retain_properties = true; + b->buffer = buffers_add(b->buffers, buf); + // TODO: error? + b->buffer_removed_hook = + buffer_add_destroy_hook(b->buffer, underlying_buffer_destroyed, b); + + b->enter_pressed = (struct command){ + .name = "choice-buffer-enter", + .fn = enter_pressed_fn, + .userdata = b, + }; + + b->q_pressed = (struct command){ + .name = "choice-buffer-close", + .fn = choice_buffer_close_fn, + .userdata = b, + }; + + struct binding bindings[] = { + ANONYMOUS_BINDING(ENTER, &b->enter_pressed), + ANONYMOUS_BINDING(None, 'q', &b->q_pressed), + }; + + struct keymap km = keymap_create("choice_buffer", 8); + keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0])); + buffer_add_keymap(b->buffer, km); + + struct location begin = buffer_end(b->buffer); + buffer_add(b->buffer, buffer_end(b->buffer), title.s, title.l); + buffer_newline(b->buffer, buffer_end(b->buffer)); + buffer_add(b->buffer, buffer_end(b->buffer), (uint8_t *)"----------------", + 16); + struct location end = buffer_end(b->buffer); + buffer_add_text_property(b->buffer, begin, end, + (struct text_property){ + .type = TextProperty_Colors, + .data.colors = + (struct text_property_colors){ + .set_fg = true, + .fg = Color_Cyan, + }, + }); + buffer_newline(b->buffer, buffer_end(b->buffer)); + buffer_newline(b->buffer, buffer_end(b->buffer)); + + struct window *w = windows_get_active(); + + window_set_buffer(w, b->buffer); + struct buffer_view *bv = window_buffer_view(w); + bv->dot = buffer_end(b->buffer); + + buffer_set_readonly(b->buffer, true); + + return b; +} + +void choice_buffer_add_choice(struct choice_buffer *buffer, struct s8 text, + void *data) { + buffer_set_readonly(buffer->buffer, false); + VEC_APPEND(&buffer->choices, struct choice * new_choice); + + new_choice->data = data; + new_choice->callback = NULL; + new_choice->region.begin = buffer_end(buffer->buffer); + buffer_add(buffer->buffer, buffer_end(buffer->buffer), (uint8_t *)"- ", 2); + buffer_add(buffer->buffer, buffer_end(buffer->buffer), text.s, text.l); + new_choice->region.end = buffer_end(buffer->buffer); + buffer_newline(buffer->buffer, buffer_end(buffer->buffer)); + buffer_set_readonly(buffer->buffer, false); +} + +void choice_buffer_add_choice_with_callback(struct choice_buffer *buffer, + struct s8 text, void *data, + select_callback callback) { + buffer_set_readonly(buffer->buffer, false); + VEC_APPEND(&buffer->choices, struct choice * new_choice); + + new_choice->data = data; + new_choice->callback = callback; + new_choice->region.begin = buffer_end(buffer->buffer); + buffer_add(buffer->buffer, buffer_end(buffer->buffer), (uint8_t *)"- ", 2); + buffer_add(buffer->buffer, buffer_end(buffer->buffer), text.s, text.l); + new_choice->region.end = buffer_end(buffer->buffer); + buffer_newline(buffer->buffer, buffer_end(buffer->buffer)); + buffer_set_readonly(buffer->buffer, false); +} + +static void delete_choice_buffer(struct choice_buffer *buffer, + bool delete_underlying) { + buffer->abort_cb(buffer->userdata); + VEC_DESTROY(&buffer->choices); + if (delete_underlying) { + buffer_remove_destroy_hook(buffer->buffer, buffer->buffer_removed_hook, + NULL); + buffers_remove(buffer->buffers, buffer->buffer->name); + } + + free(buffer); +} diff --git a/src/main/lsp/choice-buffer.h b/src/main/lsp/choice-buffer.h new file mode 100644 index 0000000..c2a7c33 --- /dev/null +++ b/src/main/lsp/choice-buffer.h @@ -0,0 +1,23 @@ +#ifndef _CHOICE_BUFFER_H +#define _CHOICE_BUFFER_H + +#include "dged/s8.h" + +typedef void (*abort_callback)(void *); +typedef void (*select_callback)(void *, void *); +typedef void (*update_callback)(void *); + +struct choice_buffer; +struct buffers; + +struct choice_buffer * +choice_buffer_create(struct s8 title, struct buffers *buffers, + select_callback selected, abort_callback aborted, + update_callback update, void *userdata); +void choice_buffer_add_choice(struct choice_buffer *buffer, struct s8 text, + void *data); +void choice_buffer_add_choice_with_callback(struct choice_buffer *buffer, + struct s8 text, void *data, + select_callback callback); + +#endif diff --git a/src/main/lsp/completion.c b/src/main/lsp/completion.c new file mode 100644 index 0000000..df89255 --- /dev/null +++ b/src/main/lsp/completion.c @@ -0,0 +1,405 @@ +#include "completion.h" + +#include <stddef.h> + +#include "dged/s8.h" +#include "dged/vec.h" +#include "types.h" + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/minibuffer.h" +#include "main/completion.h" +#include "main/lsp.h" + +struct completion_ctx { + struct lsp_server *server; + struct completion_context comp_ctx; + struct completion_list completions; + struct s8 cached_with; + struct completion *completion_data; + uint64_t last_request; + + triggerchar_vec trigger_chars; +}; + +struct symbol { + struct s8 symbol; + struct region region; +}; + +static struct symbol current_symbol(struct buffer *buffer, struct location at) { + struct region word = buffer_word_at(buffer, at); + if (!region_has_size(word)) { + return (struct symbol){ + .symbol = + (struct s8){ + .s = NULL, + .l = 0, + }, + .region = word, + }; + }; + + struct text_chunk line = buffer_region(buffer, region_new(word.begin, at)); + struct s8 symbol = s8new((const char *)line.text, line.nbytes); + + if (line.allocated) { + free(line.text); + } + + return (struct symbol){.symbol = symbol, .region = word}; +} + +struct completion_ctx *create_completion_ctx(struct lsp_server *server, + triggerchar_vec *trigger_chars) { + struct completion_ctx *ctx = + (struct completion_ctx *)calloc(1, sizeof(struct completion_ctx)); + + ctx->server = server; + ctx->completion_data = NULL; + ctx->completions.incomplete = false; + ctx->cached_with.s = NULL; + ctx->cached_with.l = 0; + VEC_INIT(&ctx->completions.items, 0); + + VEC_INIT(&ctx->trigger_chars, VEC_SIZE(trigger_chars)); + VEC_FOR_EACH(trigger_chars, struct s8 * s) { + VEC_PUSH(&ctx->trigger_chars, s8dup(*s)); + } + + return ctx; +} + +void destroy_completion_ctx(struct completion_ctx *ctx) { + completion_list_free(&ctx->completions); + + if (ctx->completion_data != NULL) { + free(ctx->completion_data); + } + + s8delete(ctx->cached_with); + + VEC_FOR_EACH(&ctx->trigger_chars, struct s8 * s) { s8delete(*s); } + VEC_DESTROY(&ctx->trigger_chars); + + free(ctx); +} + +static char *item_kind_to_str(enum completion_item_kind kind) { + switch (kind) { + case CompletionItem_Text: + return "tx"; + + case CompletionItem_Method: + return "mth"; + + case CompletionItem_Function: + return "fn"; + + case CompletionItem_Constructor: + return "cons"; + + case CompletionItem_Field: + return "field"; + + case CompletionItem_Variable: + return "var"; + + case CompletionItem_Class: + return "cls"; + + case CompletionItem_Interface: + return "iface"; + + case CompletionItem_Module: + return "mod"; + + case CompletionItem_Property: + return "prop"; + + case CompletionItem_Unit: + return "unit"; + + case CompletionItem_Value: + return "val"; + + case CompletionItem_Enum: + return "enum"; + + case CompletionItem_Keyword: + return "kw"; + + case CompletionItem_Snippet: + return "snp"; + + case CompletionItem_Color: + return "col"; + + case CompletionItem_File: + return "file"; + + case CompletionItem_Reference: + return "ref"; + + case CompletionItem_Folder: + return "fld"; + + case CompletionItem_EnumMember: + return "em"; + + case CompletionItem_Constant: + return "const"; + + case CompletionItem_Struct: + return "struct"; + + case CompletionItem_Event: + return "ev"; + + case CompletionItem_Operator: + return "op"; + + case CompletionItem_TypeParameter: + return "tp"; + + default: + return ""; + } +} + +static struct region lsp_item_render(void *data, struct buffer *buffer) { + struct lsp_completion_item *item = (struct lsp_completion_item *)data; + struct location begin = buffer_end(buffer); + struct s8 kind_str = s8from_fmt("(%s)", item_kind_to_str(item->kind)); + struct s8 txt = s8from_fmt("%-8.*s%.*s", kind_str.l, kind_str.s, + item->label.l, item->label.s); + struct location end = buffer_add(buffer, begin, txt.s, txt.l); + s8delete(txt); + s8delete(kind_str); + buffer_newline(buffer, buffer_end(buffer)); + + return region_new(begin, end); +} + +static void lsp_item_selected(void *data, struct buffer_view *view) { + struct lsp_completion_item *item = (struct lsp_completion_item *)data; + struct buffer *buffer = view->buffer; + struct lsp_server *lsp_server = lsp_server_for_buffer(buffer); + + abort_completion(); + + if (lsp_server == NULL) { + return; + } + + switch (item->edit_type) { + case TextEdit_None: { + struct symbol symbol = current_symbol(buffer, view->dot); + struct s8 insert = item->insert_text; + + // FIXME: why does this happen? + if (symbol.symbol.l >= insert.l) { + s8delete(symbol.symbol); + return; + } + + if (symbol.symbol.l > 0) { + insert.s += symbol.symbol.l; + insert.l -= symbol.symbol.l; + } + + s8delete(symbol.symbol); + + struct location at = buffer_add(buffer, view->dot, insert.s, insert.l); + buffer_view_goto(view, at); + + } break; + case TextEdit_TextEdit: { + struct text_edit *ed = &item->edit.text_edit; + struct region reg = lsp_range_to_coordinates(lsp_server, buffer, ed->range); + struct location at = reg.begin; + if (!region_is_inside(reg, view->dot)) { + reg.end = view->dot; + } + + if (region_has_size(reg)) { + at = buffer_delete(buffer, reg); + } + + at = buffer_add(buffer, at, ed->new_text.s, ed->new_text.l); + buffer_view_goto(view, at); + } break; + + case TextEdit_InsertReplaceEdit: { + struct insert_replace_edit *ed = &item->edit.insert_replace_edit; + struct region reg = + lsp_range_to_coordinates(lsp_server, buffer, ed->replace); + + if (!region_is_inside(reg, view->dot)) { + reg.end = view->dot; + } + + if (region_has_size(reg)) { + buffer_delete(buffer, reg); + } + + struct location at = + buffer_add(buffer, ed->insert.begin, ed->new_text.s, ed->new_text.l); + buffer_view_goto(view, at); + } break; + } + + if (!VEC_EMPTY(&item->additional_text_edits)) { + apply_edits_buffer(lsp_server, view->buffer, item->additional_text_edits, + &view->dot); + } +} + +static void lsp_item_cleanup(void *data) { (void)data; } + +static struct s8 get_filter_text(struct lsp_completion_item *item) { + return item->filter_text.l > 0 ? item->filter_text : item->label; +} + +static void fill_completions(struct completion_ctx *lsp_ctx, struct s8 needle) { + if (lsp_ctx->completion_data != NULL) { + free(lsp_ctx->completion_data); + lsp_ctx->completion_data = NULL; + } + + size_t ncomps = VEC_SIZE(&lsp_ctx->completions.items); + + // if there is more than a single item or the user has not typed that + // single item exactly, then add to the list of completions. + lsp_ctx->completion_data = calloc(ncomps, sizeof(struct completion)); + + ncomps = 0; + VEC_FOR_EACH(&lsp_ctx->completions.items, + struct lsp_completion_item * lsp_item) { + struct s8 filter_text = get_filter_text(lsp_item); + if (needle.l == 0 || s8startswith(filter_text, needle)) { + struct completion *c = &lsp_ctx->completion_data[ncomps]; + + c->data = lsp_item; + c->render = lsp_item_render; + c->selected = lsp_item_selected; + c->cleanup = lsp_item_cleanup; + ++ncomps; + } + } + + // if there is only a single item that matches the needle exactly, + // don't add it to the list since the user has already won + if (ncomps == 1 && needle.l > 0 && + s8eq(get_filter_text(lsp_ctx->completion_data[0].data), needle)) { + return; + } + + if (ncomps > 0) { + lsp_ctx->comp_ctx.add_completions(lsp_ctx->completion_data, ncomps); + } +} + +static void handle_completion_response(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + (void)server; + struct completion_ctx *lsp_ctx = (struct completion_ctx *)userdata; + + if (response->id != lsp_ctx->last_request) { + // discard any old requests + return; + } + + completion_list_free(&lsp_ctx->completions); + lsp_ctx->completions = completion_list_from_json(&response->value.result); + + fill_completions(lsp_ctx, lsp_ctx->cached_with); +} + +static void complete_with_lsp(struct completion_context ctx, bool deletion, + void *userdata) { + (void)deletion; + struct completion_ctx *lsp_ctx = (struct completion_ctx *)userdata; + lsp_ctx->comp_ctx = ctx; + + struct symbol sym = current_symbol(ctx.buffer, ctx.location); + struct s8 symbol = sym.symbol; + + // check if the symbol is too short for triggering completion + bool should_activate = + (symbol.l >= 3 || completion_active()) && !s8onlyws(symbol); + + // use trigger chars as an alternative activation condition + if (!should_activate) { + struct location begin = buffer_previous_char(ctx.buffer, ctx.location); + struct location end = begin; + end.col += 4; + struct text_chunk txt = buffer_region(ctx.buffer, region_new(begin, end)); + struct s8 t = { + .s = txt.text, + .l = txt.nbytes, + }; + + VEC_FOR_EACH(&lsp_ctx->trigger_chars, struct s8 * tc) { + if (s8startswith(t, *tc)) { + should_activate = true; + goto done; + } + } + done: + if (txt.allocated) { + free(txt.text); + } + } + + // if we still should not activate, we give up + if (!should_activate) { + s8delete(symbol); + return; + } + + bool has_completions = !VEC_EMPTY(&lsp_ctx->completions.items); + if (completion_active() && has_completions && + !lsp_ctx->completions.incomplete && !s8empty(lsp_ctx->cached_with) && + s8startswith(symbol, lsp_ctx->cached_with)) { + fill_completions(lsp_ctx, symbol); + } else { + uint64_t id = new_pending_request(lsp_ctx->server, + handle_completion_response, lsp_ctx); + lsp_ctx->last_request = id; + + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(ctx.buffer); + struct text_document_position params = { + .uri = doc.uri, + .position = ctx.location, + }; + + s8delete(lsp_ctx->cached_with); + lsp_ctx->cached_with = s8dup(symbol); + + struct s8 json_payload = document_position_to_json(¶ms); + lsp_send( + lsp_backend(lsp_ctx->server), + lsp_create_request(id, s8("textDocument/completion"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); + } + + s8delete(symbol); +} + +void enable_completion_for_buffer(struct completion_ctx *ctx, + struct buffer *buffer) { + struct completion_provider prov = { + .name = "lsp", + .complete = complete_with_lsp, + .userdata = ctx, + }; + struct completion_provider providers[] = {prov}; + + add_completion_providers(buffer, providers, 1); +} diff --git a/src/main/lsp/completion.h b/src/main/lsp/completion.h new file mode 100644 index 0000000..f3c51c0 --- /dev/null +++ b/src/main/lsp/completion.h @@ -0,0 +1,18 @@ +#ifndef _LSP_COMPLETION_H +#define _LSP_COMPLETION_H + +#include "dged/vec.h" + +struct completion_ctx; +struct buffer; +struct lsp_server; + +typedef VEC(struct s8) triggerchar_vec; + +struct completion_ctx *create_completion_ctx(struct lsp_server *server, + triggerchar_vec *trigger_chars); +void destroy_completion_ctx(struct completion_ctx *); + +void enable_completion_for_buffer(struct completion_ctx *, struct buffer *); + +#endif diff --git a/src/main/lsp/diagnostics.c b/src/main/lsp/diagnostics.c new file mode 100644 index 0000000..fbab4c0 --- /dev/null +++ b/src/main/lsp/diagnostics.c @@ -0,0 +1,386 @@ +#include "diagnostics.h" + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/location.h" +#include "dged/lsp.h" +#include "dged/minibuffer.h" +#include "dged/vec.h" +#include "main/bindings.h" +#include "main/lsp.h" + +struct lsp_buffer_diagnostics { + struct buffer *buffer; + diagnostic_vec diagnostics; +}; + +#define DIAGNOSTIC_BUFNAME "*lsp-diagnostics*" + +typedef VEC(struct lsp_buffer_diagnostics) buffer_diagnostics_vec; + +struct lsp_diagnostics { + buffer_diagnostics_vec buffer_diagnostics; +}; + +struct diagnostic_region { + struct diagnostic *diagnostic; + struct region region; +}; + +struct active_diagnostics { + struct buffer *buffer; + VEC(struct diagnostic_region) diag_regions; +}; + +static struct active_diagnostics g_active_diagnostic; + +static struct s8 diagnostics_modeline(struct buffer_view *view, + void *userdata) { + struct lsp_diagnostics *diag = (struct lsp_diagnostics *)userdata; + + diagnostic_vec *diags = diagnostics_for_buffer(diag, view->buffer); + + size_t nerrs = 0, nwarn = 0; + if (diags != NULL) { + VEC_FOR_EACH(diags, struct diagnostic * d) { + if (d->severity == LspDiagnostic_Error) { + ++nerrs; + } else if (d->severity == LspDiagnostic_Warning) { + ++nwarn; + } + } + + return s8from_fmt("E: %d, W: %d", nerrs, nwarn); + } + + return s8(""); +} + +struct lsp_diagnostics *diagnostics_create(void) { + struct lsp_diagnostics *d = calloc(1, sizeof(struct lsp_diagnostics)); + + VEC_INIT(&d->buffer_diagnostics, 16); + buffer_view_add_modeline_hook(diagnostics_modeline, d); + VEC_INIT(&g_active_diagnostic.diag_regions, 0); + g_active_diagnostic.buffer = NULL; + return d; +} + +void diagnostics_destroy(struct lsp_diagnostics *d) { + VEC_FOR_EACH(&d->buffer_diagnostics, struct lsp_buffer_diagnostics * diag) { + VEC_FOR_EACH(&diag->diagnostics, struct diagnostic * d) { + diagnostic_free(d); + } + VEC_DESTROY(&diag->diagnostics); + } + + VEC_DESTROY(&d->buffer_diagnostics); + VEC_DESTROY(&g_active_diagnostic.diag_regions); + free(d); +} + +diagnostic_vec *diagnostics_for_buffer(struct lsp_diagnostics *d, + struct buffer *buffer) { + VEC_FOR_EACH(&d->buffer_diagnostics, struct lsp_buffer_diagnostics * diag) { + if (diag->buffer == buffer) { + return &diag->diagnostics; + } + } + + return NULL; +} + +static int32_t diagnostics_goto_fn(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + struct buffer *db = buffers_find(ctx.buffers, DIAGNOSTIC_BUFNAME); + if (db == NULL) { + return 0; + } + + struct window *w = window_find_by_buffer(db); + if (w == NULL) { + return 0; + } + + if (g_active_diagnostic.buffer == NULL) { + return 0; + } + + struct buffer_view *bv = window_buffer_view(w); + + VEC_FOR_EACH(&g_active_diagnostic.diag_regions, + struct diagnostic_region * reg) { + if (region_is_inside(reg->region, bv->dot)) { + struct window *target_win = + window_find_by_buffer(g_active_diagnostic.buffer); + if (target_win == NULL) { + // if the buffer is not open, reuse the diagnostic buffer + target_win = w; + window_set_buffer(target_win, g_active_diagnostic.buffer); + } + + buffer_view_goto(window_buffer_view(target_win), + reg->diagnostic->region.begin); + windows_set_active(target_win); + return 0; + } + } + + return 0; +} + +static int32_t diagnostics_close_fn(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + if (window_has_prev_buffer_view(ctx.active_window)) { + window_set_buffer(ctx.active_window, + window_prev_buffer_view(ctx.active_window)->buffer); + } + + return 0; +} + +static struct buffer *update_diagnostics_buffer(struct lsp_server *server, + struct buffers *buffers, + diagnostic_vec diagnostics, + struct buffer *buffer) { + char buf[2048]; + struct buffer *db = buffers_find(buffers, DIAGNOSTIC_BUFNAME); + if (db == NULL) { + struct buffer buf = buffer_create(DIAGNOSTIC_BUFNAME); + buf.lazy_row_add = false; + buf.retain_properties = true; + db = buffers_add(buffers, buf); + + static struct command diagnostics_goto = { + .name = "diagnostics-goto", + .fn = diagnostics_goto_fn, + }; + + static struct command diagnostics_close = { + .name = "diagnostics-close", + .fn = diagnostics_close_fn, + }; + + struct binding bindings[] = { + ANONYMOUS_BINDING(ENTER, &diagnostics_goto), + ANONYMOUS_BINDING(None, 'q', &diagnostics_close), + }; + struct keymap km = keymap_create("diagnostics", 8); + keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0])); + buffer_add_keymap(db, km); + } + buffer_set_readonly(db, false); + buffer_clear(db); + buffer_clear_text_properties(db); + + g_active_diagnostic.buffer = buffer; + ssize_t len = snprintf(buf, 2048, "Diagnostics for %s:\n\n", buffer->name); + if (len != -1) { + buffer_add(db, buffer_end(db), (uint8_t *)buf, len); + buffer_add_text_property( + db, (struct location){.line = 0, .col = 0}, + (struct location){.line = 1, .col = 0}, + (struct text_property){.type = TextProperty_Colors, + .data.colors.underline = true}); + } + + VEC_DESTROY(&g_active_diagnostic.diag_regions); + VEC_INIT(&g_active_diagnostic.diag_regions, VEC_SIZE(&diagnostics)); + VEC_FOR_EACH(&diagnostics, struct diagnostic * diag) { + struct location start = buffer_end(db); + char src[128]; + size_t srclen = snprintf(src, 128, "%.*s%s", diag->source.l, diag->source.s, + diag->source.l > 0 ? ": " : ""); + const char *severity_str = diag_severity_to_str(diag->severity); + size_t severity_str_len = strlen(severity_str); + struct region reg = lsp_range_to_coordinates(server, buffer, diag->region); + len = snprintf(buf, 2048, + "%s%s [%d, %d]: %.*s\n-------------------------------", src, + severity_str, reg.begin.line + 1, reg.begin.col, + diag->message.l, diag->message.s); + + if (len != -1) { + buffer_add(db, buffer_end(db), (uint8_t *)buf, len); + + struct location srcend = start; + srcend.col += srclen - 3; + buffer_add_text_property( + db, start, srcend, + (struct text_property){.type = TextProperty_Colors, + .data.colors.underline = true}); + + uint32_t color = diag_severity_color(diag->severity); + struct location sevstart = start; + sevstart.col += srclen; + struct location sevend = sevstart; + sevend.col += severity_str_len; + buffer_add_text_property( + db, sevstart, sevend, + (struct text_property){.type = TextProperty_Colors, + .data.colors.set_fg = true, + .data.colors.fg = color}); + + VEC_PUSH(&g_active_diagnostic.diag_regions, + ((struct diagnostic_region){ + .diagnostic = diag, + .region = region_new(start, buffer_end(db)), + })); + + buffer_newline(db, buffer_end(db)); + } + } + + buffer_set_readonly(db, true); + return db; +} + +void handle_publish_diagnostics(struct lsp_server *server, + struct buffers *buffers, + struct lsp_notification *notification) { + struct publish_diagnostics_params params = + diagnostics_from_json(¬ification->params); + if (s8startswith(params.uri, s8("file://"))) { + const char *p = s8tocstr(params.uri); + struct buffer *b = buffers_find_by_filename(buffers, &p[7]); + free((void *)p); + + if (b != NULL) { + struct lsp_diagnostics *ld = lsp_server_diagnostics(server); + diagnostic_vec *diagnostics = diagnostics_for_buffer(ld, b); + if (diagnostics == NULL) { + VEC_APPEND(&ld->buffer_diagnostics, + struct lsp_buffer_diagnostics * new_diag); + new_diag->buffer = b; + new_diag->diagnostics.nentries = 0; + new_diag->diagnostics.capacity = 0; + new_diag->diagnostics.temp = NULL; + new_diag->diagnostics.entries = NULL; + + diagnostics = &new_diag->diagnostics; + } + + VEC_FOR_EACH(diagnostics, struct diagnostic * diag) { + diagnostic_free(diag); + } + VEC_DESTROY(diagnostics); + + *diagnostics = params.diagnostics; + update_diagnostics_buffer(server, buffers, *diagnostics, b); + } else { + VEC_FOR_EACH(¶ms.diagnostics, struct diagnostic * diag) { + diagnostic_free(diag); + } + VEC_DESTROY(¶ms.diagnostics); + message("failed to find buffer with URI: %.*s", params.uri.l, + params.uri.s); + } + } else { + message("warning: unsupported LSP URI: %.*s", params.uri.l, params.uri.s); + } + + s8delete(params.uri); +} + +static struct lsp_diagnostics * +lsp_diagnostics_from_server(struct lsp_server *server) { + return lsp_server_diagnostics(server); +} + +int32_t next_diagnostic_cmd(struct command_ctx ctx, int argc, + const char **argv) { + (void)ctx; + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(windows_get_active()); + + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + return 0; + } + + diagnostic_vec *diagnostics = + diagnostics_for_buffer(lsp_diagnostics_from_server(server), bv->buffer); + if (diagnostics == NULL) { + return 0; + } + + if (VEC_EMPTY(diagnostics)) { + minibuffer_echo_timeout(4, "no more diagnostics"); + return 0; + } + + VEC_FOR_EACH(diagnostics, struct diagnostic * diag) { + if (location_compare(bv->dot, diag->region.begin) < 0) { + buffer_view_goto(bv, diag->region.begin); + return 0; + } + } + + buffer_view_goto(bv, VEC_FRONT(diagnostics)->region.begin); + return 0; +} + +int32_t prev_diagnostic_cmd(struct command_ctx ctx, int argc, + const char **argv) { + (void)ctx; + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(windows_get_active()); + + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + return 0; + } + + diagnostic_vec *diagnostics = + diagnostics_for_buffer(lsp_diagnostics_from_server(server), bv->buffer); + + if (diagnostics == NULL) { + return 0; + } + + if (VEC_EMPTY(diagnostics)) { + minibuffer_echo_timeout(4, "no more diagnostics"); + return 0; + } + + VEC_FOR_EACH(diagnostics, struct diagnostic * diag) { + if (location_compare(bv->dot, diag->region.begin) > 0) { + buffer_view_goto(bv, diag->region.begin); + return 0; + } + } + + buffer_view_goto(bv, VEC_BACK(diagnostics)->region.begin); + return 0; +} + +int32_t diagnostics_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer *b = window_buffer(ctx.active_window); + struct lsp_server *server = lsp_server_for_buffer(b); + + if (server == NULL) { + minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name); + return 0; + } + + diagnostic_vec *d = + diagnostics_for_buffer(lsp_diagnostics_from_server(server), b); + struct buffer *db = update_diagnostics_buffer(server, ctx.buffers, *d, b); + window_set_buffer(ctx.active_window, db); + + return 0; +} diff --git a/src/main/lsp/diagnostics.h b/src/main/lsp/diagnostics.h new file mode 100644 index 0000000..4357b8e --- /dev/null +++ b/src/main/lsp/diagnostics.h @@ -0,0 +1,26 @@ +#ifndef _DIAGNOSTICS_H +#define _DIAGNOSTICS_H + +#include "dged/command.h" +#include "main/lsp/types.h" + +struct lsp_server; +struct buffers; +struct lsp_notification; + +struct lsp_diagnostics; + +struct lsp_diagnostics *diagnostics_create(void); +void diagnostics_destroy(struct lsp_diagnostics *); + +diagnostic_vec *diagnostics_for_buffer(struct lsp_diagnostics *, + struct buffer *); +void handle_publish_diagnostics(struct lsp_server *, struct buffers *, + struct lsp_notification *); + +/* COMMANDS */ +int32_t diagnostics_cmd(struct command_ctx, int, const char **); +int32_t next_diagnostic_cmd(struct command_ctx, int, const char **); +int32_t prev_diagnostic_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/format.c b/src/main/lsp/format.c new file mode 100644 index 0000000..2019a90 --- /dev/null +++ b/src/main/lsp/format.c @@ -0,0 +1,149 @@ +#include "format.h" + +#include "completion.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/minibuffer.h" +#include "dged/settings.h" +#include "dged/window.h" +#include "main/completion.h" +#include "main/lsp.h" + +struct formatted_buffer { + struct buffer *buffer; + bool save; +}; + +static uint32_t get_tab_width(struct buffer *buffer) { + struct setting *tw = lang_setting(&buffer->lang, "tab-width"); + if (tw == NULL) { + tw = settings_get("editor.tab-width"); + } + + uint32_t tab_width = 4; + if (tw != NULL && tw->value.type == Setting_Number) { + tab_width = tw->value.data.number_value; + } + return tab_width; +} + +static bool use_tabs(struct buffer *buffer) { + struct setting *ut = lang_setting(&buffer->lang, "use-tabs"); + if (ut == NULL) { + ut = settings_get("editor.use-tabs"); + } + + bool use_tabs = false; + if (ut != NULL && ut->value.type == Setting_Bool) { + use_tabs = ut->value.data.bool_value; + } + + return use_tabs; +} + +static struct formatting_options options_from_lang(struct buffer *buffer) { + return (struct formatting_options){ + .tab_size = get_tab_width(buffer), + .use_spaces = !use_tabs(buffer), + }; +} + +void handle_format_response(struct lsp_server *server, + struct lsp_response *response, void *userdata) { + + text_edit_vec edits = text_edits_from_json(&response->value.result); + struct formatted_buffer *buffer = (struct formatted_buffer *)userdata; + + pause_completion(); + if (!VEC_EMPTY(&edits)) { + apply_edits_buffer(server, buffer->buffer, edits, NULL); + + if (buffer->save) { + buffer_to_file(buffer->buffer); + } + } + resume_completion(); + + text_edits_free(edits); + free(buffer); +} + +static void format_buffer(struct lsp_server *server, struct buffer *buffer, + bool save) { + struct formatted_buffer *b = + (struct formatted_buffer *)calloc(1, sizeof(struct formatted_buffer)); + b->buffer = buffer; + b->save = save; + + uint64_t id = new_pending_request(server, handle_format_response, b); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(buffer); + + struct document_formatting_params params = { + .text_document.uri = doc.uri, + .options = options_from_lang(buffer), + }; + + struct s8 json_payload = document_formatting_params_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/formatting"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); +} + +void format_document(struct lsp_server *server, struct buffer *buffer) { + format_buffer(server, buffer, false); +} + +void format_document_save(struct lsp_server *server, struct buffer *buffer) { + format_buffer(server, buffer, true); +} + +void format_region(struct lsp_server *server, struct buffer *buffer, + struct region region) { + struct formatted_buffer *b = + (struct formatted_buffer *)calloc(1, sizeof(struct formatted_buffer)); + b->buffer = buffer; + b->save = false; + + uint64_t id = new_pending_request(server, handle_format_response, b); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(buffer); + + struct document_range_formatting_params params = { + .text_document.uri = doc.uri, + .range = region_to_lsp(buffer, region, server), + .options = options_from_lang(buffer), + }; + + struct s8 json_payload = document_range_formatting_params_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/formatting"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); +} + +int32_t format_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)ctx; + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(windows_get_active()); + + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + return 0; + } + + struct region reg = region_new(bv->dot, bv->mark); + if (bv->mark_set && region_has_size(reg)) { + buffer_view_clear_mark(bv); + format_region(server, bv->buffer, reg); + } else { + format_document(server, bv->buffer); + } + + return 0; +} diff --git a/src/main/lsp/format.h b/src/main/lsp/format.h new file mode 100644 index 0000000..8e90ab3 --- /dev/null +++ b/src/main/lsp/format.h @@ -0,0 +1,18 @@ +#ifndef _FORMAT_H +#define _FORMAT_H + +#include "dged/command.h" +#include "dged/location.h" + +struct buffer; +struct lsp_server; +struct lsp_response; + +void format_document(struct lsp_server *, struct buffer *); +void format_document_save(struct lsp_server *, struct buffer *); +void format_region(struct lsp_server *, struct buffer *, struct region); + +/* COMMANDS */ +int32_t format_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/goto.c b/src/main/lsp/goto.c new file mode 100644 index 0000000..7d2d228 --- /dev/null +++ b/src/main/lsp/goto.c @@ -0,0 +1,297 @@ +#include "goto.h" + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/location.h" +#include "dged/minibuffer.h" +#include "dged/window.h" +#include "main/bindings.h" +#include "main/lsp.h" + +#include "choice-buffer.h" + +static struct jump_stack { + buffer_keymap_id goto_keymap_id; + struct buffer_location *stack; + uint32_t top; + uint32_t size; + struct buffers *buffers; +} g_jump_stack; + +static struct location_result g_location_result = {}; + +struct buffer_location { + struct buffer *buffer; + struct location location; +}; + +void init_goto(size_t jump_stack_depth, struct buffers *buffers) { + g_jump_stack.size = jump_stack_depth; + g_jump_stack.top = 0; + g_jump_stack.goto_keymap_id = (buffer_keymap_id)-1; + g_jump_stack.stack = calloc(g_jump_stack.size, sizeof(struct jump_stack)); + g_jump_stack.buffers = buffers; +} + +void destroy_goto(void) { + free(g_jump_stack.stack); + g_jump_stack.stack = NULL; + g_jump_stack.top = 0; + g_jump_stack.size = 0; +} + +void lsp_jump_to(struct text_document_location loc) { + if (s8startswith(loc.uri, s8("file://"))) { + const char *p = s8tocstr(loc.uri); + struct buffer *b = buffers_find_by_filename(g_jump_stack.buffers, &p[7]); + + if (b == NULL) { + struct buffer new_buf = buffer_from_file(&p[7]); + b = buffers_add(g_jump_stack.buffers, new_buf); + } + + free((void *)p); + + struct window *w = windows_get_active(); + + struct buffer_view *old_bv = window_buffer_view(w); + g_jump_stack.stack[g_jump_stack.top] = (struct buffer_location){ + .buffer = old_bv->buffer, + .location = old_bv->dot, + }; + g_jump_stack.top = (g_jump_stack.top + 1) % g_jump_stack.size; + + if (old_bv->buffer != b) { + struct window *tw = window_find_by_buffer(b); + if (tw == NULL) { + window_set_buffer(w, b); + } else { + w = tw; + windows_set_active(w); + } + } + + struct buffer_view *bv = window_buffer_view(w); + buffer_view_goto(bv, loc.range.begin); + } else { + message("warning: unsupported LSP URI: %.*s", loc.uri.l, loc.uri.s); + } +} + +static void location_selected(void *location, void *userdata) { + (void)userdata; + struct text_document_location *loc = + (struct text_document_location *)location; + lsp_jump_to(*loc); +} + +static void location_buffer_close(void *userdata) { + (void)userdata; + location_result_free(&g_location_result); +} + +static void handle_location_result(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + struct s8 title = s8((const char *)userdata); + struct location_result res = + location_result_from_json(&response->value.result); + + if (res.type == Location_Null || + (res.type == Location_Array && VEC_EMPTY(&res.location.array))) { + minibuffer_echo_timeout(2, "nothing found"); + location_result_free(&res); + return; + } + + if (res.type == Location_Single) { + lsp_jump_to(res.location.single); + location_result_free(&res); + } else if (res.type == Location_Array && VEC_SIZE(&res.location.array) == 1) { + lsp_jump_to(*VEC_FRONT(&res.location.array)); + location_result_free(&res); + } else if (res.type == Location_Array) { + + g_location_result = res; + struct choice_buffer *buf = + choice_buffer_create(title, g_jump_stack.buffers, location_selected, + location_buffer_close, NULL, server); + + VEC_FOR_EACH(&res.location.array, struct text_document_location * loc) { + choice_buffer_add_choice(buf, + s8from_fmt("%.*s: %d, %d", loc->uri.l, + loc->uri.s, loc->range.begin.line, + loc->range.begin.col), + loc); + } + } +} + +int32_t lsp_goto_def_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct buffer *b = bv->buffer; + struct lsp_server *server = lsp_server_for_buffer(b); + + if (server == NULL) { + minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name); + return 0; + } + + uint64_t id = new_pending_request(server, handle_location_result, + (void *)"Definitions"); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(b); + struct text_document_position params = { + .uri = doc.uri, + .position = bv->dot, + }; + + struct s8 json_payload = document_position_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/definition"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); + + return 0; +} + +int32_t lsp_goto_decl_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct buffer *b = bv->buffer; + struct lsp_server *server = lsp_server_for_buffer(b); + + if (server == NULL) { + minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name); + return 0; + } + + uint64_t id = new_pending_request(server, handle_location_result, + (void *)"Declarations"); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(b); + struct text_document_position params = { + .uri = doc.uri, + .position = bv->dot, + }; + + struct s8 json_payload = document_position_to_json(¶ms); + lsp_send( + lsp_backend(server), + lsp_create_request(id, s8("textDocument/declaration"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); + + return 0; +} + +int32_t lsp_goto_impl_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct buffer *b = bv->buffer; + struct lsp_server *server = lsp_server_for_buffer(b); + + if (server == NULL) { + minibuffer_echo_timeout(2, "buffer %s does not have lsp enabled", b->name); + return 0; + } + + uint64_t id = new_pending_request(server, handle_location_result, + (void *)"Implementations"); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(b); + struct text_document_position params = { + .uri = doc.uri, + .position = bv->dot, + }; + + struct s8 json_payload = document_position_to_json(¶ms); + lsp_send( + lsp_backend(server), + lsp_create_request(id, s8("textDocument/implementation"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); + + return 0; +} + +static int32_t handle_lsp_goto_key(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + buffer_remove_keymap(g_jump_stack.goto_keymap_id); + minibuffer_abort_prompt(); + + struct command *cmd = lookup_command(ctx.commands, (char *)ctx.userdata); + if (cmd == NULL) { + return 0; + } + + return execute_command(cmd, ctx.commands, windows_get_active(), ctx.buffers, + 0, NULL); +} + +COMMAND_FN("lsp-goto-definition", goto_d_pressed, handle_lsp_goto_key, + "lsp-goto-definition"); +COMMAND_FN("lsp-goto-declaration", goto_f_pressed, handle_lsp_goto_key, + "lsp-goto-declaration"); + +int32_t lsp_goto_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct binding bindings[] = { + ANONYMOUS_BINDING(None, 'd', &goto_d_pressed_command), + ANONYMOUS_BINDING(None, 'f', &goto_f_pressed_command), + }; + struct keymap m = keymap_create("lsp-goto", 8); + keymap_bind_keys(&m, bindings, sizeof(bindings) / sizeof(bindings[0])); + g_jump_stack.goto_keymap_id = buffer_add_keymap(minibuffer_buffer(), m); + return minibuffer_keymap_prompt(ctx, "lsp-goto: ", &m); +} + +int32_t lsp_goto_previous_cmd(struct command_ctx ctx, int argc, + const char **argv) { + (void)ctx; + (void)argc; + (void)argv; + + uint32_t index = + g_jump_stack.top == 0 ? g_jump_stack.size - 1 : g_jump_stack.top - 1; + + struct buffer_location *loc = &g_jump_stack.stack[index]; + if (loc->buffer == NULL) { + return 0; + } + + struct window *w = windows_get_active(); + if (window_buffer(w) != loc->buffer) { + struct window *tw = window_find_by_buffer(loc->buffer); + if (tw == NULL) { + window_set_buffer(w, loc->buffer); + } else { + w = tw; + windows_set_active(w); + } + } + + buffer_view_goto(window_buffer_view(w), loc->location); + + loc->buffer = NULL; + g_jump_stack.top = index; + + return 0; +} diff --git a/src/main/lsp/goto.h b/src/main/lsp/goto.h new file mode 100644 index 0000000..524772d --- /dev/null +++ b/src/main/lsp/goto.h @@ -0,0 +1,23 @@ +#ifndef _GOTO_H +#define _GOTO_H + +#include "dged/command.h" + +#include "types.h" + +struct lsp_server; +struct buffers; + +void init_goto(size_t jump_stack_depth, struct buffers *); +void destroy_goto(void); + +void lsp_jump_to(struct text_document_location loc); + +/* COMMANDS */ +int32_t lsp_goto_def_cmd(struct command_ctx, int, const char **); +int32_t lsp_goto_decl_cmd(struct command_ctx, int, const char **); +int32_t lsp_goto_impl_cmd(struct command_ctx, int, const char **); +int32_t lsp_goto_cmd(struct command_ctx, int, const char **); +int32_t lsp_goto_previous_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/help.c b/src/main/lsp/help.c new file mode 100644 index 0000000..e5bcc28 --- /dev/null +++ b/src/main/lsp/help.c @@ -0,0 +1,101 @@ +#include "help.h" + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/minibuffer.h" +#include "dged/s8.h" +#include "dged/window.h" + +#include "bindings.h" +#include "lsp.h" + +static int32_t close_help(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + if (window_has_prev_buffer_view(ctx.active_window)) { + window_set_buffer(ctx.active_window, + window_prev_buffer_view(ctx.active_window)->buffer); + } else { + minibuffer_echo_timeout(4, "no previous buffer to go to"); + } + + return 0; +} + +static void handle_help_response(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + (void)server; + + struct buffers *buffers = (struct buffers *)userdata; + if (response->value.result.type == Json_Null) { + minibuffer_echo_timeout(4, "help: no help found"); + return; + } + + struct buffer *b = buffers_find(buffers, "*lsp-help*"); + if (b == NULL) { + b = buffers_add(buffers, buffer_create("*lsp-help*")); + static struct command help_close = { + .name = "help_close", + .fn = close_help, + }; + + struct binding bindings[] = { + ANONYMOUS_BINDING(None, 'q', &help_close), + }; + struct keymap km = keymap_create("help", 2); + keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0])); + buffer_add_keymap(b, km); + } + + struct hover help = hover_from_json(&response->value.result); + + buffer_set_readonly(b, false); + buffer_clear(b); + buffer_add(b, buffer_end(b), help.contents.s, help.contents.l); + buffer_set_readonly(b, true); + + if (window_find_by_buffer(b) == NULL) { + window_set_buffer(windows_get_active(), b); + } + hover_free(&help); +} + +void lsp_help(struct lsp_server *server, struct buffer *buffer, + struct location at, struct buffers *buffers) { + uint64_t id = new_pending_request(server, handle_help_response, buffers); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(buffer); + + struct text_document_position pos = { + .uri = doc.uri, + .position = at, + }; + + struct s8 json_payload = document_position_to_json(&pos); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/hover"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); +} + +int32_t lsp_help_cmd(struct command_ctx ctx, int argc, const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + minibuffer_echo_timeout(4, "no lsp server associated with %s", + bv->buffer->name); + return 0; + } + + lsp_help(server, bv->buffer, bv->dot, ctx.buffers); + return 0; +} diff --git a/src/main/lsp/help.h b/src/main/lsp/help.h new file mode 100644 index 0000000..98a4478 --- /dev/null +++ b/src/main/lsp/help.h @@ -0,0 +1,16 @@ +#ifndef _LSP_HELP_H +#define _LSP_HELP_H + +#include "dged/command.h" +#include "dged/location.h" + +struct buffer; +struct buffers; +struct lsp_server; + +void lsp_help(struct lsp_server *, struct buffer *, struct location, + struct buffers *); + +int32_t lsp_help_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/references.c b/src/main/lsp/references.c new file mode 100644 index 0000000..c2438fa --- /dev/null +++ b/src/main/lsp/references.c @@ -0,0 +1,248 @@ +#include "references.h" + +#include "dged/binding.h" +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/buffers.h" +#include "dged/command.h" +#include "dged/display.h" +#include "dged/location.h" +#include "dged/minibuffer.h" +#include "dged/s8.h" +#include "dged/text.h" +#include "dged/vec.h" +#include "dged/window.h" + +#include "bindings.h" +#include "lsp.h" +#include "lsp/goto.h" +#include "lsp/types.h" +#include "unistd.h" + +struct link { + struct s8 uri; + struct region target_region; + struct region region; +}; + +typedef VEC(struct link) link_vec; + +static link_vec g_links; +static struct buffer *g_prev_buffer = NULL; +static struct location g_prev_location; + +static int32_t references_close(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + if (g_prev_buffer != NULL) { + // validate that it is still a valid buffer + struct buffer *b = + buffers_find_by_filename(ctx.buffers, g_prev_buffer->filename); + window_set_buffer(ctx.active_window, b); + buffer_view_goto(window_buffer_view(ctx.active_window), g_prev_location); + } else if (window_has_prev_buffer_view(ctx.active_window)) { + window_set_buffer(ctx.active_window, + window_prev_buffer_view(ctx.active_window)->buffer); + } else { + minibuffer_echo_timeout(4, "no previous buffer to go to"); + } + + return 0; +} + +static int32_t references_visit(struct command_ctx ctx, int argc, + const char **argv) { + + (void)argc; + (void)argv; + + struct buffer_view *view = window_buffer_view(ctx.active_window); + + VEC_FOR_EACH(&g_links, struct link * link) { + if (region_is_inside(link->region, view->dot)) { + lsp_jump_to((struct text_document_location){ + .range = link->target_region, + .uri = link->uri, + }); + + return 0; + } + } + + return 0; +} + +static void reference_buffer_closed(struct buffer *buffer, void *userdata) { + (void)buffer; + link_vec *vec = (link_vec *)userdata; + VEC_FOR_EACH(vec, struct link * link) { s8delete(link->uri); } + VEC_DESTROY(vec); +} + +static void handle_references_response(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + (void)server; + + struct buffers *buffers = (struct buffers *)userdata; + if (response->value.result.type == Json_Null) { + minibuffer_echo_timeout(4, "references: no references found"); + return; + } + + struct location_result locations = + location_result_from_json(&response->value.result); + + if (locations.type != Location_Array) { + minibuffer_echo_timeout(4, "references: expected location array"); + return; + } + + struct buffer *b = buffers_find(buffers, "*lsp-references*"); + if (b == NULL) { + b = buffers_add(buffers, buffer_create("*lsp-references*")); + b->lazy_row_add = false; + b->retain_properties = true; + static struct command ref_close = { + .name = "ref_close", + .fn = references_close, + }; + + static struct command ref_visit_cmd = { + .name = "ref_visit", + .fn = references_visit, + }; + + struct binding bindings[] = { + ANONYMOUS_BINDING(None, 'q', &ref_close), + ANONYMOUS_BINDING(ENTER, &ref_visit_cmd), + }; + struct keymap km = keymap_create("references", 2); + keymap_bind_keys(&km, bindings, sizeof(bindings) / sizeof(bindings[0])); + buffer_add_keymap(b, km); + buffer_add_destroy_hook(b, reference_buffer_closed, &g_links); + VEC_INIT(&g_links, 16); + } + + buffer_set_readonly(b, false); + buffer_clear(b); + + VEC_FOR_EACH(&g_links, struct link * link) { s8delete(link->uri); } + VEC_CLEAR(&g_links); + + buffer_clear_text_properties(b); + VEC_FOR_EACH(&locations.location.array, struct text_document_location * loc) { + uint32_t found = 0, found_at = (uint32_t)-1; + for (uint32_t i = 0; i < loc->uri.l; ++i) { + uint8_t b = loc->uri.s[i]; + if (b == ':') { + ++found; + } else if (b == '/' && found == 1) { + ++found; + } else if (b == '/' && found == 2) { + found_at = i; + } else { + found = 0; + } + } + + struct s8 path = loc->uri; + if (found_at != (uint32_t)-1) { + path.s += found_at; + path.l -= found_at; + } + + struct s8 relpath = path; + char *cwd = getcwd(NULL, 0); + if (s8startswith(relpath, s8(cwd))) { + size_t l = strlen(cwd); + // cwd does not end in / + relpath.s += l + 1; + relpath.l -= l + 1; + } + free(cwd); + + struct location start = buffer_end(b); + struct location fileend = buffer_add(b, start, relpath.s, relpath.l); + buffer_add_text_property(b, start, + (struct location){ + .line = fileend.line, + .col = fileend.col > 0 ? fileend.col - 1 : 0, + }, + (struct text_property){ + .type = TextProperty_Colors, + .data.colors = + (struct text_property_colors){ + .set_bg = false, + .set_fg = true, + .fg = Color_Magenta, + .underline = true, + }, + }); + + struct s8 line = s8from_fmt(":%d", loc->range.begin.line); + struct location end = buffer_add(b, buffer_end(b), line.s, line.l); + s8delete(line); + + VEC_PUSH(&g_links, ((struct link){ + .target_region = loc->range, + .region = region_new(start, end), + .uri = s8dup(loc->uri), + })); + + buffer_newline(b, end); + } + + buffer_set_readonly(b, true); + + if (window_find_by_buffer(b) == NULL) { + struct window *w = windows_get_active(); + g_prev_buffer = window_buffer(w); + g_prev_location = window_buffer_view(w)->dot; + window_set_buffer(w, b); + } + location_result_free(&locations); +} + +void lsp_references(struct lsp_server *server, struct buffer *buffer, + struct location at, struct buffers *buffers) { + uint64_t id = + new_pending_request(server, handle_references_response, buffers); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(buffer); + + struct reference_params params = { + .position = + { + .uri = doc.uri, + .position = at, + }, + .include_declaration = true, + }; + + struct s8 json_payload = reference_params_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/references"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); +} + +int32_t lsp_references_cmd(struct command_ctx ctx, int argc, + const char **argv) { + (void)argc; + (void)argv; + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + minibuffer_echo_timeout(4, "no lsp server associated with %s", + bv->buffer->name); + return 0; + } + + lsp_references(server, bv->buffer, bv->dot, ctx.buffers); + return 0; +} diff --git a/src/main/lsp/references.h b/src/main/lsp/references.h new file mode 100644 index 0000000..ea51987 --- /dev/null +++ b/src/main/lsp/references.h @@ -0,0 +1,19 @@ +#ifndef _LSP_REFERENCES_H +#define _LSP_REFERENCES_H + +#include <stdint.h> + +#include "dged/command.h" +#include "dged/location.h" + +struct lsp_server; +struct buffer; +struct buffers; + +void lsp_references(struct lsp_server *server, struct buffer *buffer, + struct location at, struct buffers *buffers); + +int32_t lsp_references_cmd(struct command_ctx ctx, int argc, + const char *argv[]); + +#endif diff --git a/src/main/lsp/rename.c b/src/main/lsp/rename.c new file mode 100644 index 0000000..6adc9a1 --- /dev/null +++ b/src/main/lsp/rename.c @@ -0,0 +1,61 @@ +#include "rename.h" + +#include "dged/buffer.h" +#include "dged/buffer_view.h" +#include "dged/minibuffer.h" +#include "dged/window.h" + +#include "lsp.h" + +static void handle_rename_response(struct lsp_server *server, + struct lsp_response *response, + void *userdata) { + (void)userdata; + if (response->value.result.type == Json_Null) { + minibuffer_echo_timeout(4, "rename: no edits"); + return; + } + + struct workspace_edit edit = + workspace_edit_from_json(&response->value.result); + apply_edits(server, &edit); + workspace_edit_free(&edit); +} + +void lsp_rename(struct lsp_server *server, struct buffer *buffer, + struct location location, struct s8 new_name) { + uint64_t id = new_pending_request(server, handle_rename_response, NULL); + struct versioned_text_document_identifier doc = + versioned_identifier_from_buffer(buffer); + + struct rename_params params = { + .position.uri = doc.uri, + .position.position = location, + .new_name = new_name, + }; + + struct s8 json_payload = rename_params_to_json(¶ms); + lsp_send(lsp_backend(server), + lsp_create_request(id, s8("textDocument/rename"), json_payload)); + + versioned_text_document_identifier_free(&doc); + s8delete(json_payload); +} + +int32_t lsp_rename_cmd(struct command_ctx ctx, int argc, const char **argv) { + if (argc == 0) { + return minibuffer_prompt(ctx, "rename to: "); + } + + struct buffer_view *bv = window_buffer_view(ctx.active_window); + struct lsp_server *server = lsp_server_for_lang_id(bv->buffer->lang.id); + if (server == NULL) { + minibuffer_echo_timeout(4, "no lsp server associated with %s", + bv->buffer->name); + return 0; + } + + lsp_rename(server, bv->buffer, bv->dot, s8(argv[0])); + + return 0; +} diff --git a/src/main/lsp/rename.h b/src/main/lsp/rename.h new file mode 100644 index 0000000..4fb8396 --- /dev/null +++ b/src/main/lsp/rename.h @@ -0,0 +1,16 @@ +#ifndef _LSP_RENAME_H +#define _LSP_RENAME_H + +#include "dged/command.h" +#include "dged/location.h" +#include "dged/s8.h" + +struct lsp_server; +struct buffer; + +void lsp_rename(struct lsp_server *, struct buffer *, struct location, + struct s8); + +int32_t lsp_rename_cmd(struct command_ctx, int, const char **); + +#endif diff --git a/src/main/lsp/types.c b/src/main/lsp/types.c new file mode 100644 index 0000000..bd87377 --- /dev/null +++ b/src/main/lsp/types.c @@ -0,0 +1,1081 @@ +#include "types.h" + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include "dged/buffer.h" +#include "dged/display.h" +#include "dged/path.h" +#include "dged/s8.h" + +struct s8 initialize_params_to_json(struct initialize_params *params) { + char *cwd = getcwd(NULL, 0); + const char *fmt = + "{ \"processId\": %d, \"clientInfo\": { \"name\": " + "\"%.*s\", \"version\": \"%.*s\" }," + "\"capabilities\": { \"textDocument\": { " + "\"publishDiagnostics\": { }," + "\"hover\": { \"dynamicRegistration\": false, \"contentFormat\": [ " + "\"plaintext\", \"markdown\" ] }," + "\"signatureHelp\" : { \"dynamicRegistration\": false, " + "\"signatureInformation\": {" + " \"documentationFormat\": [ \"plaintext\", \"markdown\" ], " + "\"activeParameterSupport\": true } }," + "\"codeAction\": { \"codeActionLiteralSupport\": { \"codeActionKind\":" + "{ \"valueSet\": [ \"quickfix\", \"refactor\", \"source\", " + "\"refactor.extract\", " + "\"refactor.inline\", \"refactor.rewrite\", \"source.organizeImports\" ] " + "} } } }," + "\"general\": { \"positionEncodings\": [ \"utf-8\", " + "\"utf-32\", \"utf-16\" ] }," + "\"offsetEncoding\": [ \"utf-8\", \"utf-32\" ,\"utf-16\" ]" + "}," + "\"workspaceFolders\": [ { \"uri\": \"file://%s\", " + "\"name\": \"cwd\" } ] }"; + + struct s8 s = + s8from_fmt(fmt, params->process_id, params->client_info.name.l, + params->client_info.name.s, params->client_info.version.l, + params->client_info.version.s, cwd); + + free(cwd); + return s; +} + +static enum position_encoding_kind +position_encoding_from_str(struct s8 encoding_kind) { + if (s8eq(encoding_kind, s8("utf-8"))) { + return PositionEncoding_Utf8; + } else if (s8eq(encoding_kind, s8("utf-32"))) { + return PositionEncoding_Utf32; + } + + return PositionEncoding_Utf16; +} + +struct s8 position_encoding_kind_str(enum position_encoding_kind kind) { + switch (kind) { + case PositionEncoding_Utf8: + return s8("utf-8"); + + case PositionEncoding_Utf32: + return s8("utf-32"); + + default: + break; + } + + return s8("utf-16"); +} + +static struct server_capabilities +parse_capabilities(struct json_object *root, struct json_value *capabilities) { + struct server_capabilities caps = { + .text_document_sync.kind = TextDocumentSync_Full, + .text_document_sync.open_close = false, + .text_document_sync.save = false, + .position_encoding = PositionEncoding_Utf16, + }; + + // clang has this legacy attribute for positionEncoding + // use with a lower prio than positionEncoding in capabilities + struct json_value *offset_encoding = json_get(root, s8("offsetEncoding")); + if (offset_encoding != NULL && offset_encoding->type == Json_String) { + caps.position_encoding = + position_encoding_from_str(offset_encoding->value.string); + } + + if (capabilities == NULL || capabilities->type != Json_Object) { + return caps; + } + + struct json_object *obj = capabilities->value.object; + // text document sync caps + struct json_value *text_doc_sync = json_get(obj, s8("textDocumentSync")); + if (text_doc_sync != NULL) { + if (text_doc_sync->type == Json_Number) { + caps.text_document_sync.kind = + (enum text_document_sync_kind)text_doc_sync->value.number; + } else { + struct json_object *tsync = text_doc_sync->value.object; + caps.text_document_sync.kind = + (enum text_document_sync_kind)json_get(tsync, s8("change")) + ->value.number; + + struct json_value *open_close = json_get(tsync, s8("openClose")); + caps.text_document_sync.open_close = + open_close != NULL ? open_close->value.boolean : false; + + struct json_value *save = json_get(tsync, s8("save")); + caps.text_document_sync.save = + save != NULL ? open_close->value.boolean : false; + } + } + + // position encoding + struct json_value *pos_enc = json_get(obj, s8("positionEncoding")); + if (pos_enc != NULL && pos_enc->type == Json_String) { + caps.position_encoding = position_encoding_from_str(pos_enc->value.string); + } + + struct json_value *completion_opts = json_get(obj, s8("completionProvider")); + caps.supports_completion = false; + if (completion_opts != NULL && completion_opts->type == Json_Object) { + caps.supports_completion = true; + + // trigger chars + struct json_value *trigger_chars = + json_get(completion_opts->value.object, s8("triggerCharacters")); + if (trigger_chars != NULL && trigger_chars->type == Json_Array) { + uint64_t arrlen = json_array_len(trigger_chars->value.array); + VEC_INIT(&caps.completion_options.trigger_characters, arrlen); + for (uint32_t i = 0; i < arrlen; ++i) { + struct json_value *val = json_array_get(trigger_chars->value.array, i); + VEC_PUSH(&caps.completion_options.trigger_characters, + s8dup(val->value.string)); + } + } + + // all commit characters + struct json_value *commit_chars = + json_get(completion_opts->value.object, s8("allCommitCharacters")); + if (commit_chars != NULL && commit_chars->type == Json_Array) { + uint64_t arrlen = json_array_len(commit_chars->value.array); + VEC_INIT(&caps.completion_options.all_commit_characters, arrlen); + for (uint32_t i = 0; i < arrlen; ++i) { + struct json_value *val = json_array_get(commit_chars->value.array, i); + VEC_PUSH(&caps.completion_options.all_commit_characters, + s8dup(val->value.string)); + } + } + + // resolve provider + struct json_value *resolve_provider = + json_get(completion_opts->value.object, s8("resolveProvider")); + if (resolve_provider != NULL && resolve_provider->type == Json_Bool) { + caps.completion_options.resolve_provider = + resolve_provider->value.boolean; + } + } + + return caps; +} + +struct initialize_result initialize_result_from_json(struct json_value *json) { + struct json_object *obj = json->value.object; + struct json_object *server_info = + json_get(obj, s8("serverInfo"))->value.object; + return (struct initialize_result){ + .capabilities = + parse_capabilities(obj, json_get(obj, s8("capabilities"))), + .server_info.name = + s8dup(json_get(server_info, s8("name"))->value.string), + .server_info.version = + s8dup(json_get(server_info, s8("version"))->value.string), + }; +} + +void initialize_result_free(struct initialize_result *res) { + s8delete(res->server_info.name); + s8delete(res->server_info.version); + if (res->capabilities.supports_completion) { + VEC_FOR_EACH(&res->capabilities.completion_options.trigger_characters, + struct s8 * s) { + s8delete(*s); + } + + VEC_DESTROY(&res->capabilities.completion_options.trigger_characters); + + VEC_FOR_EACH(&res->capabilities.completion_options.all_commit_characters, + struct s8 * s) { + s8delete(*s); + } + + VEC_DESTROY(&res->capabilities.completion_options.all_commit_characters); + } +} + +static struct s8 uri_from_buffer(struct buffer *buffer) { + if (buffer->filename != NULL) { + char *abspath = to_abspath(buffer->filename); + struct s8 ret = s8from_fmt("file://%s", abspath); + free(abspath); + return ret; + } + + return s8from_fmt("file://invalid-file"); +} + +struct text_document_item +text_document_item_from_buffer(struct buffer *buffer) { + struct text_chunk buffer_text = + buffer_region(buffer, region_new((struct location){.line = 0, .col = 0}, + buffer_end(buffer))); + struct text_document_item item = { + .uri = uri_from_buffer(buffer), + .language_id = s8new(buffer->lang.id, strlen(buffer->lang.id)), + .version = buffer->version, + .text = + (struct s8){ + .s = buffer_text.text, + .l = buffer_text.nbytes, + }, + }; + + return item; +} + +void text_document_item_free(struct text_document_item *item) { + s8delete(item->uri); + s8delete(item->language_id); + s8delete(item->text); +} + +struct versioned_text_document_identifier +versioned_identifier_from_buffer(struct buffer *buffer) { + struct versioned_text_document_identifier identifier = { + .uri = uri_from_buffer(buffer), + .version = buffer->version, + }; + + return identifier; +} + +void versioned_text_document_identifier_free( + struct versioned_text_document_identifier *identifier) { + s8delete(identifier->uri); +} + +struct s8 did_change_text_document_params_to_json( + struct did_change_text_document_params *params) { + size_t event_buf_size = 0; + for (size_t i = 0; i < params->ncontent_changes; ++i) { + struct text_document_content_change_event *ev = ¶ms->content_changes[i]; + struct s8 escaped = escape_json_string(ev->text); + if (!ev->full_document) { + const char *item_fmt = + "{ \"range\": { \"start\": { \"line\": %d, \"character\": %d}, " + "\"end\": { \"line\": %d, \"character\": %d } }, " + "\"text\": \"%.*s\" }%s"; + + ssize_t num = + snprintf(NULL, 0, item_fmt, ev->range.begin.line, ev->range.begin.col, + ev->range.end.line, ev->range.end.col, escaped.l, escaped.s, + i == params->ncontent_changes - 1 ? "" : ", "); + + if (num < 0) { + return s8(""); + } + + event_buf_size += num; + } else { + const char *item_fmt = "{ \"text\", \"%.*s\" }%s"; + ssize_t num = snprintf(NULL, 0, item_fmt, escaped.l, escaped.s, + i == params->ncontent_changes - 1 ? "" : ", "); + + if (num < 0) { + return s8(""); + } + + event_buf_size += num; + } + + s8delete(escaped); + } + + ++event_buf_size; + char *buf = calloc(event_buf_size, 1); + size_t offset = 0; + for (size_t i = 0; i < params->ncontent_changes; ++i) { + struct text_document_content_change_event *ev = ¶ms->content_changes[i]; + struct s8 escaped = escape_json_string(ev->text); + if (!ev->full_document) { + const char *item_fmt = + "{ \"range\": { \"start\": { \"line\": %d, \"character\": %d}, " + "\"end\": { \"line\": %d, \"character\": %d } }, " + "\"text\": \"%.*s\" }%s"; + + ssize_t num = snprintf( + &buf[offset], event_buf_size - offset, item_fmt, ev->range.begin.line, + ev->range.begin.col, ev->range.end.line, ev->range.end.col, escaped.l, + escaped.s, i == params->ncontent_changes - 1 ? "" : ", "); + + if (num < 0) { + return s8(""); + } + + offset += num; + } else { + const char *item_fmt = "{ \"text\", \"%.*s\" }%s"; + ssize_t num = + snprintf(&buf[offset], event_buf_size - offset, item_fmt, escaped.l, + escaped.s, i == params->ncontent_changes - 1 ? "" : ", "); + + if (num < 0) { + return s8(""); + } + + offset += num; + } + + s8delete(escaped); + } + + const char *fmt = + "{ \"textDocument\": { \"uri\": \"%.*s\", \"version\": %d }, " + "\"contentChanges\": [ %s ]" + "}"; + + struct versioned_text_document_identifier *doc = ¶ms->text_document; + struct s8 json = s8from_fmt(fmt, doc->uri.l, doc->uri.s, doc->version, buf); + + free(buf); + return json; +} + +struct s8 did_open_text_document_params_to_json( + struct did_open_text_document_params *params) { + const char *fmt = + "{ \"textDocument\": { \"uri\": \"%.*s\", \"languageId\": \"%.*s\", " + "\"version\": %d, \"text\": \"%.*s\" }}"; + + struct text_document_item *item = ¶ms->text_document; + + struct s8 escaped_content = escape_json_string(item->text); + struct s8 json = s8from_fmt( + fmt, item->uri.l, item->uri.s, item->language_id.l, item->language_id.s, + item->version, escaped_content.l, escaped_content.s); + + s8delete(escaped_content); + return json; +} + +struct s8 did_save_text_document_params_to_json( + struct did_save_text_document_params *params) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" } }"; + + struct text_document_identifier *item = ¶ms->text_document; + struct s8 json = s8from_fmt(fmt, item->uri.l, item->uri.s); + return json; +} + +static struct region parse_region(struct json_object *obj) { + struct json_object *start = json_get(obj, s8("start"))->value.object; + struct json_object *end = json_get(obj, s8("end"))->value.object; + + return region_new( + (struct location){.line = json_get(start, s8("line"))->value.number, + .col = json_get(start, s8("character"))->value.number}, + (struct location){.line = json_get(end, s8("line"))->value.number, + .col = json_get(end, s8("character"))->value.number}); +} + +static void parse_diagnostic(uint64_t id, struct json_value *elem, + void *userdata) { + (void)id; + diagnostic_vec *vec = (diagnostic_vec *)userdata; + struct json_object *obj = elem->value.object; + struct json_value *severity = json_get(obj, s8("severity")); + struct json_value *source = json_get(obj, s8("source")); + + struct diagnostic diag; + diag.message = + unescape_json_string(json_get(obj, s8("message"))->value.string); + diag.region = parse_region(json_get(obj, s8("range"))->value.object); + diag.severity = severity != NULL + ? (enum diagnostic_severity)severity->value.number + : LspDiagnostic_Error; + diag.source = source != NULL ? unescape_json_string(source->value.string) + : (struct s8){.l = 0, .s = NULL}; + + VEC_PUSH(vec, diag); +} + +const char *diag_severity_to_str(enum diagnostic_severity severity) { + + switch (severity) { + case LspDiagnostic_Error: + return "error"; + case LspDiagnostic_Warning: + return "warning"; + case LspDiagnostic_Information: + return "info"; + case LspDiagnostic_Hint: + return "hint"; + } + + return ""; +} + +struct publish_diagnostics_params +diagnostics_from_json(struct json_value *json) { + struct json_object *obj = json->value.object; + struct json_value *version = json_get(obj, s8("version")); + struct publish_diagnostics_params params = { + .uri = unescape_json_string(json_get(obj, s8("uri"))->value.string), + .version = version != NULL ? version->value.number : 0, + }; + + struct json_array *diagnostics = + json_get(obj, s8("diagnostics"))->value.array; + VEC_INIT(¶ms.diagnostics, json_array_len(diagnostics)); + json_array_foreach(diagnostics, ¶ms.diagnostics, parse_diagnostic); + + return params; +} + +void diagnostic_free(struct diagnostic *diag) { + s8delete(diag->message); + s8delete(diag->source); +} + +struct s8 document_position_to_json(struct text_document_position *position) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, " + "\"position\": { \"line\": %d, \"character\": %d } }"; + + struct s8 json = s8from_fmt(fmt, position->uri.l, position->uri.s, + position->position.line, position->position.col); + return json; +} + +static struct text_document_location +location_from_json(struct json_value *json) { + struct text_document_location loc = {0}; + if (json->type != Json_Object) { + return loc; + } + + struct json_object *obj = json->value.object; + loc.uri = unescape_json_string(json_get(obj, s8("uri"))->value.string); + loc.range = parse_region(json_get(obj, s8("range"))->value.object); + + return loc; +} + +static void parse_text_doc_location(uint64_t id, struct json_value *elem, + void *userdata) { + (void)id; + location_vec *vec = (location_vec *)userdata; + VEC_PUSH(vec, location_from_json(elem)); +} + +struct location_result location_result_from_json(struct json_value *json) { + if (json->type == Json_Null) { + return (struct location_result){ + .type = Location_Null, + }; + } else if (json->type == Json_Object) { + return (struct location_result){ + .type = Location_Single, + .location.single = location_from_json(json), + }; + } else if (json->type == Json_Array) { + // location link or location + struct location_result res = {}; + res.type = Location_Array; + struct json_array *locations = json->value.array; + VEC_INIT(&res.location.array, json_array_len(locations)); + json_array_foreach(locations, &res.location.array, parse_text_doc_location); + return res; + } + + return (struct location_result){.type = Location_Null}; +} + +void location_result_free(struct location_result *res) { + switch (res->type) { + case Location_Null: + break; + case Location_Single: + s8delete(res->location.single.uri); + break; + case Location_Array: + VEC_FOR_EACH(&res->location.array, struct text_document_location * loc) { + s8delete(loc->uri); + } + VEC_DESTROY(&res->location.array); + break; + case Location_Link: + // TODO + break; + } +} + +static uint32_t severity_to_json(enum diagnostic_severity severity) { + return (uint32_t)severity; +} + +static struct s8 region_to_json(struct region region) { + const char *fmt = "{ \"start\": { \"line\": %d, \"character\": %d }, " + "\"end\": { \"line\": %d, \"character\": %d } }"; + return s8from_fmt(fmt, region.begin.line, region.begin.col, region.end.line, + region.end.col); +} + +static struct s8 diagnostic_to_json(struct diagnostic *diag) { + const char *fmt = + "{ \"range\": %.*s, \"message\": \"%.*s\", \"severity\": %d }"; + + struct s8 range = region_to_json(diag->region); + struct s8 json = + s8from_fmt(fmt, range.l, range.s, diag->message.l, diag->message.s, + severity_to_json(diag->severity)); + + s8delete(range); + return json; +} + +static struct s8 diagnostic_vec_to_json(diagnostic_vec diagnostics) { + size_t ndiags = VEC_SIZE(&diagnostics); + if (ndiags == 0) { + return s8new("[]", 2); + } + + struct s8 *strings = calloc(ndiags, sizeof(struct s8)); + + size_t len = 1; + VEC_FOR_EACH_INDEXED(&diagnostics, struct diagnostic * diag, i) { + strings[i] = diagnostic_to_json(diag); + len += strings[i].l + 1; + } + + uint8_t *final = (uint8_t *)calloc(len, 1); + struct s8 json = { + .s = final, + .l = len, + }; + + final[0] = '['; + + size_t offset = 1; + for (uint32_t i = 0; i < ndiags; ++i) { + memcpy(&final[offset], strings[i].s, strings[i].l); + offset += strings[i].l; + + s8delete(strings[i]); + } + + final[len - 1] = ']'; + + free(strings); + + return json; +} + +struct s8 code_action_params_to_json(struct code_action_params *params) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, " + " \"range\": %.*s, " + " \"context\": { \"diagnostics\": %.*s } }"; + + struct s8 json_diags = diagnostic_vec_to_json(params->context.diagnostics); + struct s8 range = region_to_json(params->range); + + struct s8 json = + s8from_fmt(fmt, params->text_document.uri.l, params->text_document.uri.s, + range.l, range.s, json_diags.l, json_diags.s); + + s8delete(json_diags); + s8delete(range); + return json; +} + +static struct lsp_command lsp_command_from_json(struct json_value *json) { + struct json_object *obj = json->value.object; + struct lsp_command command = { + .title = unescape_json_string(json_get(obj, s8("title"))->value.string), + .command = + unescape_json_string(json_get(obj, s8("command"))->value.string), + .arguments = s8(""), + }; + + struct json_value *arguments = json_get(obj, s8("arguments")); + if (arguments != NULL && arguments->type == Json_Array) { + size_t len = arguments->end - arguments->start; + command.arguments = s8new((const char *)arguments->start, len); + } + + return command; +} + +static void lsp_action_from_json(uint64_t id, struct json_value *json, + void *userdata) { + (void)id; + struct code_actions *actions = (struct code_actions *)userdata; + + struct json_object *obj = json->value.object; + struct json_value *command_val = json_get(obj, s8("command")); + if (command_val != NULL && command_val->type == Json_String) { + VEC_PUSH(&actions->commands, lsp_command_from_json(json)); + } else { + VEC_APPEND(&actions->code_actions, struct code_action * action); + action->title = + unescape_json_string(json_get(obj, s8("title"))->value.string); + action->kind = s8(""); + action->has_edit = false; + action->has_command = false; + + struct json_value *kind_val = json_get(obj, s8("kind")); + if (kind_val != NULL && kind_val->type == Json_String) { + action->kind = unescape_json_string(kind_val->value.string); + } + + struct json_value *edit_val = json_get(obj, s8("edit")); + if (edit_val != NULL && edit_val->type == Json_Object) { + action->has_edit = true; + action->edit = workspace_edit_from_json(edit_val); + } + + command_val = json_get(obj, s8("command")); + if (command_val != NULL && command_val->type == Json_Object) { + action->has_command = true; + action->command = lsp_command_from_json(command_val); + } + } +} + +struct code_actions lsp_code_actions_from_json(struct json_value *json) { + struct code_actions actions; + + if (json->type == Json_Array) { + struct json_array *jcmds = json->value.array; + VEC_INIT(&actions.commands, json_array_len(jcmds)); + VEC_INIT(&actions.code_actions, json_array_len(jcmds)); + json_array_foreach(jcmds, &actions, lsp_action_from_json); + } else { /* NULL or wrong type */ + VEC_INIT(&actions.commands, 0); + VEC_INIT(&actions.code_actions, 0); + } + + return actions; +} + +static void lsp_command_free(struct lsp_command *command) { + s8delete(command->title); + s8delete(command->command); + + if (command->arguments.l > 0) { + s8delete(command->arguments); + } +} + +void lsp_code_actions_free(struct code_actions *actions) { + VEC_FOR_EACH(&actions->commands, struct lsp_command * command) { + lsp_command_free(command); + } + + VEC_DESTROY(&actions->commands); + + VEC_FOR_EACH(&actions->code_actions, struct code_action * action) { + s8delete(action->title); + s8delete(action->kind); + + if (action->has_edit) { + workspace_edit_free(&action->edit); + } + + if (action->has_command) { + lsp_command_free(&action->command); + } + } + + VEC_DESTROY(&actions->code_actions); +} + +struct s8 lsp_command_to_json(struct lsp_command *command) { + const char *fmt = "{ \"command\": \"%.*s\", \"arguments\": %.*s }"; + + return s8from_fmt(fmt, command->command.l, command->command.s, + command->arguments.l, command->arguments.s); +} + +static void text_edit_from_json(uint64_t id, struct json_value *val, + void *userdata) { + (void)id; + text_edit_vec *vec = (text_edit_vec *)userdata; + struct json_object *obj = val->value.object; + struct text_edit edit = { + .range = parse_region(json_get(obj, s8("range"))->value.object), + .new_text = + unescape_json_string(json_get(obj, s8("newText"))->value.string), + }; + VEC_PUSH(vec, edit); +} + +text_edit_vec text_edits_from_json(struct json_value *json) { + text_edit_vec vec = {0}; + + if (json->type == Json_Array) { + struct json_array *arr = json->value.array; + + VEC_INIT(&vec, json_array_len(arr)); + json_array_foreach(arr, &vec, text_edit_from_json); + } + + return vec; +} + +static void changes_from_json(struct s8 key, struct json_value *json, + void *userdata) { + change_vec *vec = (change_vec *)userdata; + + struct text_edit_pair pair = { + .uri = s8dup(key), + }; + + // pick out the edits for this key and create array + struct json_array *edits = json->value.array; + VEC_INIT(&pair.edits, json_array_len(edits)); + json_array_foreach(edits, &pair.edits, text_edit_from_json); + VEC_PUSH(vec, pair); +} + +struct workspace_edit workspace_edit_from_json(struct json_value *json) { + struct workspace_edit edit; + struct json_object *obj = json->value.object; + struct json_value *edit_container = json_get(obj, s8("edit")); + if (edit_container != NULL && edit_container->type == Json_Object) { + obj = edit_container->value.object; + } + + struct json_value *changes = json_get(obj, s8("changes")); + if (changes != NULL) { + struct json_object *changes_obj = changes->value.object; + VEC_INIT(&edit.changes, json_len(changes_obj)); + json_foreach(changes_obj, changes_from_json, &edit.changes); + } else { + VEC_INIT(&edit.changes, 0); + } + + return edit; +} + +void workspace_edit_free(struct workspace_edit *edit) { + VEC_FOR_EACH(&edit->changes, struct text_edit_pair * pair) { + s8delete(pair->uri); + VEC_FOR_EACH(&pair->edits, struct text_edit * edit) { + s8delete(edit->new_text); + } + VEC_DESTROY(&pair->edits); + } + VEC_DESTROY(&edit->changes); +} + +uint32_t diag_severity_color(enum diagnostic_severity severity) { + switch (severity) { + case LspDiagnostic_Error: + return Color_BrightRed; + case LspDiagnostic_Warning: + return Color_BrightYellow; + default: + return Color_BrightBlack; + } + + return Color_BrightBlack; +} + +struct s8 +document_formatting_params_to_json(struct document_formatting_params *params) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, \"options\": { " + "\"tabSize\": %d, \"insertSpaces\": %s } }"; + + return s8from_fmt(fmt, params->text_document.uri.l, + params->text_document.uri.s, params->options.tab_size, + params->options.use_spaces ? "true" : "false"); +} + +struct s8 document_range_formatting_params_to_json( + struct document_range_formatting_params *params) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, \"range\": " + "%.*s, \"options\": { " + "\"tabSize\": %d, \"insertSpaces\": %s } }"; + + struct s8 range = region_to_json(params->range); + struct s8 json = + s8from_fmt(fmt, params->text_document.uri.l, params->text_document.uri.s, + range.l, range.s, params->options.tab_size, + params->options.use_spaces ? "true" : "false"); + + s8delete(range); + return json; +} + +void text_edits_free(text_edit_vec edits) { + VEC_FOR_EACH(&edits, struct text_edit * edit) { s8delete(edit->new_text); } + VEC_DESTROY(&edits); +} + +static void parse_completion_item(uint64_t id, struct json_value *json, + void *userdata) { + + (void)id; + completions_vec *vec = (completions_vec *)userdata; + + struct json_object *obj = json->value.object; + + struct lsp_completion_item item = {0}; + item.label = s8dup(json_get(obj, s8("label"))->value.string); + + struct json_value *kind_val = json_get(obj, s8("kind")); + if (kind_val != NULL && kind_val->type == Json_Number) { + item.kind = (enum completion_item_kind)kind_val->value.number; + } + + struct json_value *detail_val = json_get(obj, s8("detail")); + if (detail_val != NULL && detail_val->type == Json_String) { + item.detail = s8dup(detail_val->value.string); + } + + struct json_value *sort_txt_val = json_get(obj, s8("sortText")); + if (sort_txt_val != NULL && sort_txt_val->type == Json_String) { + item.sort_text = s8dup(sort_txt_val->value.string); + } + + struct json_value *filter_txt_val = json_get(obj, s8("filterText")); + if (filter_txt_val != NULL && filter_txt_val->type == Json_String) { + item.filter_text = s8dup(filter_txt_val->value.string); + } + + struct json_value *insert_txt_val = json_get(obj, s8("insertText")); + if (insert_txt_val != NULL && insert_txt_val->type == Json_String) { + item.insert_text = s8dup(insert_txt_val->value.string); + } + + // determine type of edit + struct json_value *edit_val = json_get(obj, s8("textEdit")); + item.edit_type = TextEdit_None; + if (edit_val != NULL && edit_val->type == Json_Object) { + struct json_object *edit_obj = edit_val->value.object; + + struct json_value *insert_val = json_get(edit_obj, s8("insert")); + + if (insert_val != NULL) { + item.edit_type = TextEdit_InsertReplaceEdit; + item.edit.insert_replace_edit = (struct insert_replace_edit){ + .insert = + parse_region(json_get(edit_obj, s8("insert"))->value.object), + .replace = + parse_region(json_get(edit_obj, s8("replace"))->value.object), + .new_text = unescape_json_string( + json_get(edit_obj, s8("newText"))->value.string), + }; + } else { + item.edit_type = TextEdit_TextEdit; + item.edit.text_edit = (struct text_edit){ + .range = parse_region(json_get(edit_obj, s8("range"))->value.object), + .new_text = unescape_json_string( + json_get(edit_obj, s8("newText"))->value.string), + }; + } + } + + struct json_value *additional_txt_edits_val = + json_get(obj, s8("additionalTextEdits")); + if (additional_txt_edits_val != NULL && + additional_txt_edits_val->type == Json_Array) { + item.additional_text_edits = text_edits_from_json(additional_txt_edits_val); + } + + struct json_value *command_val = json_get(obj, s8("command")); + if (command_val != NULL && command_val->type == Json_Object) { + item.command = lsp_command_from_json(command_val); + } + + VEC_PUSH(vec, item); +} + +struct completion_list completion_list_from_json(struct json_value *json) { + + if (json->type == Json_Null) { + return (struct completion_list){ + .incomplete = false, + }; + } + + struct completion_list complist; + complist.incomplete = false; + + struct json_array *js_items = NULL; + if (json->type == Json_Object) { + struct json_object *obj = json->value.object; + complist.incomplete = json_get(obj, s8("isIncomplete"))->value.boolean; + js_items = json_get(obj, s8("items"))->value.array; + } else if (json->type == Json_Array) { + js_items = json->value.array; + } else { + return (struct completion_list){ + .incomplete = false, + }; + } + + // parse the list + VEC_INIT(&complist.items, json_array_len(js_items)); + json_array_foreach(js_items, &complist.items, parse_completion_item); + + return complist; +} + +void completion_list_free(struct completion_list *complist) { + VEC_FOR_EACH(&complist->items, struct lsp_completion_item * item) { + s8delete(item->label); + s8delete(item->detail); + s8delete(item->sort_text); + s8delete(item->filter_text); + s8delete(item->insert_text); + + if (item->edit_type == TextEdit_TextEdit) { + s8delete(item->edit.text_edit.new_text); + } else { + s8delete(item->edit.insert_replace_edit.new_text); + } + + text_edits_free(item->additional_text_edits); + lsp_command_free(&item->command); + } + + VEC_DESTROY(&complist->items); +} + +struct s8 rename_params_to_json(struct rename_params *params) { + + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, " + "\"position\": { \"line\": %d, \"character\": %d }, " + "\"newName\": \"%.*s\" }"; + + struct text_document_position *position = ¶ms->position; + struct s8 escaped = escape_json_string(params->new_name); + struct s8 json = + s8from_fmt(fmt, position->uri.l, position->uri.s, position->position.line, + position->position.col, escaped.l, escaped.s); + + s8delete(escaped); + return json; +} + +static void parse_parameter(uint64_t id, struct json_value *json, + void *userdata) { + + (void)id; + param_info_vec *vec = (param_info_vec *)userdata; + struct json_object *obj = json->value.object; + + struct parameter_information info; + struct json_value *label = json_get(obj, s8("label")); + if (label != NULL && label->type == Json_String) { + info.label = s8dup(label->value.string); + } + + struct json_value *doc = json_get(obj, s8("documentation")); + if (doc != NULL && doc->type == Json_String) { + info.documentation = s8dup(doc->value.string); + } + VEC_PUSH(vec, info); +} + +static void parse_signature(uint64_t id, struct json_value *json, + void *userdata) { + + (void)id; + signature_info_vec *vec = (signature_info_vec *)userdata; + + struct json_object *obj = json->value.object; + + struct signature_information info; + struct json_value *label = json_get(obj, s8("label")); + if (label != NULL && label->type == Json_String) { + info.label = s8dup(label->value.string); + } + + struct json_value *doc = json_get(obj, s8("documentation")); + if (doc != NULL && doc->type == Json_String) { + info.documentation = s8dup(doc->value.string); + } + + struct json_value *params = json_get(obj, s8("parameters")); + if (params != NULL && params->type == Json_Array) { + struct json_array *arr = params->value.array; + VEC_INIT(&info.parameters, json_array_len(arr)); + json_array_foreach(arr, &info.parameters, parse_parameter); + } + + VEC_PUSH(vec, info); +} + +struct signature_help signature_help_from_json(struct json_value *value) { + struct signature_help help = {0}; + struct json_object *obj = value->value.object; + + struct json_value *active_sig = json_get(obj, s8("activeSignature")); + if (active_sig != NULL && active_sig->type == Json_Number) { + help.active_signature = active_sig->value.number; + } + + struct json_value *sigs = json_get(obj, s8("signatures")); + if (sigs != NULL && sigs->type == Json_Array) { + struct json_array *arr = sigs->value.array; + VEC_INIT(&help.signatures, json_array_len(arr)); + json_array_foreach(arr, &help.signatures, parse_signature); + } + + return help; +} + +void signature_help_free(struct signature_help *help) { + VEC_FOR_EACH(&help->signatures, struct signature_information * info) { + s8delete(info->label); + s8delete(info->documentation); + + VEC_FOR_EACH(&info->parameters, struct parameter_information * pinfo) { + s8delete(pinfo->label); + s8delete(pinfo->documentation); + } + + VEC_DESTROY(&info->parameters); + } + + VEC_DESTROY(&help->signatures); +} + +struct hover hover_from_json(struct json_value *value) { + struct hover hover = {0}; + struct json_object *obj = value->value.object; + + struct json_value *contents = json_get(obj, s8("contents")); + if (contents != NULL) { + switch (contents->type) { + case Json_String: + hover.contents = unescape_json_string(contents->value.string); + break; + case Json_Object: { + struct json_value *val = json_get(contents->value.object, s8("value")); + if (val != NULL && val->type == Json_String) { + hover.contents = unescape_json_string(val->value.string); + } + } break; + default: + break; + } + } + + struct json_value *range = json_get(obj, s8("range")); + if (range != NULL && range->type == Json_Object) { + hover.range = parse_region(range->value.object); + } + + return hover; +} + +void hover_free(struct hover *hover) { s8delete(hover->contents); } + +struct s8 reference_params_to_json(struct reference_params *params) { + const char *fmt = "{ \"textDocument\": { \"uri\": \"%.*s\" }, " + "\"position\": { \"line\": %d, \"character\": %d }, " + "\"includeDeclaration\": \"%s\" }"; + + struct text_document_position *position = ¶ms->position; + struct s8 json = s8from_fmt(fmt, position->uri.l, position->uri.s, + position->position.line, position->position.col, + params->include_declaration ? "true" : "false"); + + return json; +} diff --git a/src/main/lsp/types.h b/src/main/lsp/types.h new file mode 100644 index 0000000..7b6ba1a --- /dev/null +++ b/src/main/lsp/types.h @@ -0,0 +1,385 @@ +#ifndef _LSP_TYPES_H +#define _LSP_TYPES_H + +#include "dged/json.h" +#include "dged/location.h" +#include "dged/s8.h" +#include "dged/vec.h" + +struct buffer; + +struct client_capabilities {}; + +struct workspace_folder { + struct s8 uri; + struct s8 name; +}; + +struct initialize_params { + int process_id; + struct client_info { + struct s8 name; + struct s8 version; + } client_info; + + struct client_capabilities client_capabilities; + + struct workspace_folder *workspace_folders; + size_t nworkspace_folders; +}; + +enum text_document_sync_kind { + TextDocumentSync_None = 0, + TextDocumentSync_Full = 1, + TextDocumentSync_Incremental = 2, +}; + +struct text_document_sync { + enum text_document_sync_kind kind; + bool open_close; + bool save; +}; + +enum position_encoding_kind { + PositionEncoding_Utf8, + PositionEncoding_Utf16, + PositionEncoding_Utf32, +}; + +struct completion_options { + VEC(struct s8) trigger_characters; + VEC(struct s8) all_commit_characters; + bool resolve_provider; +}; + +struct server_capabilities { + struct text_document_sync text_document_sync; + enum position_encoding_kind position_encoding; + bool supports_completion; + struct completion_options completion_options; +}; + +struct initialize_result { + struct server_capabilities capabilities; + struct server_info { + struct s8 name; + struct s8 version; + } server_info; +}; + +struct s8 initialize_params_to_json(struct initialize_params *params); +struct initialize_result initialize_result_from_json(struct json_value *json); +void initialize_result_free(struct initialize_result *); +struct s8 position_encoding_kind_str(enum position_encoding_kind); + +struct text_document_item { + struct s8 uri; + struct s8 language_id; + uint64_t version; + struct s8 text; +}; + +struct text_document_identifier { + struct s8 uri; +}; + +struct text_document_position { + struct s8 uri; + struct location position; +}; + +struct text_document_location { + struct s8 uri; + struct region range; +}; + +struct versioned_text_document_identifier { + struct s8 uri; + uint64_t version; +}; + +struct did_open_text_document_params { + struct text_document_item text_document; +}; + +enum location_type { + Location_Single, + Location_Array, + Location_Link, + Location_Null, +}; + +typedef VEC(struct text_document_location) location_vec; + +struct location_result { + enum location_type type; + union location_data { + struct text_document_location single; + location_vec array; + } location; +}; + +struct did_change_text_document_params { + struct versioned_text_document_identifier text_document; + struct text_document_content_change_event *content_changes; + size_t ncontent_changes; +}; + +struct did_save_text_document_params { + struct text_document_identifier text_document; +}; + +struct text_document_content_change_event { + struct region range; + struct s8 text; + bool full_document; +}; + +enum diagnostic_severity { + LspDiagnostic_Error = 1, + LspDiagnostic_Warning = 2, + LspDiagnostic_Information = 3, + LspDiagnostic_Hint = 4, +}; + +struct diagnostic { + struct s8 message; + struct s8 source; + struct region region; + enum diagnostic_severity severity; +}; + +typedef VEC(struct diagnostic) diagnostic_vec; + +struct publish_diagnostics_params { + struct s8 uri; + uint64_t version; + diagnostic_vec diagnostics; +}; + +struct code_action_context { + diagnostic_vec diagnostics; +}; + +struct code_action_params { + struct text_document_identifier text_document; + struct region range; + struct code_action_context context; +}; + +struct text_edit { + struct region range; + struct s8 new_text; +}; + +typedef VEC(struct text_edit) text_edit_vec; + +struct text_edit_pair { + struct s8 uri; + text_edit_vec edits; +}; + +typedef VEC(struct text_edit_pair) change_vec; + +struct workspace_edit { + change_vec changes; +}; + +struct lsp_command { + struct s8 title; + struct s8 command; + struct s8 arguments; +}; + +struct code_action { + struct s8 title; + struct s8 kind; + + bool has_edit; + struct workspace_edit edit; + + bool has_command; + struct lsp_command command; +}; + +typedef VEC(struct lsp_command) lsp_command_vec; +typedef VEC(struct code_action) code_action_vec; + +struct code_actions { + lsp_command_vec commands; + code_action_vec code_actions; +}; + +struct formatting_options { + size_t tab_size; + bool use_spaces; +}; + +struct document_formatting_params { + struct text_document_identifier text_document; + struct formatting_options options; +}; + +struct document_range_formatting_params { + struct text_document_identifier text_document; + struct region range; + struct formatting_options options; +}; + +enum completion_item_kind { + CompletionItem_Text = 1, + CompletionItem_Method = 2, + CompletionItem_Function = 3, + CompletionItem_Constructor = 4, + CompletionItem_Field = 5, + CompletionItem_Variable = 6, + CompletionItem_Class = 7, + CompletionItem_Interface = 8, + CompletionItem_Module = 9, + CompletionItem_Property = 10, + CompletionItem_Unit = 11, + CompletionItem_Value = 12, + CompletionItem_Enum = 13, + CompletionItem_Keyword = 14, + CompletionItem_Snippet = 15, + CompletionItem_Color = 16, + CompletionItem_File = 17, + CompletionItem_Reference = 18, + CompletionItem_Folder = 19, + CompletionItem_EnumMember = 20, + CompletionItem_Constant = 21, + CompletionItem_Struct = 22, + CompletionItem_Event = 23, + CompletionItem_Operator = 24, + CompletionItem_TypeParameter = 25, +}; + +enum text_edit_type { + TextEdit_None, + TextEdit_TextEdit, + TextEdit_InsertReplaceEdit, +}; + +struct insert_replace_edit { + struct s8 new_text; + struct region insert; + struct region replace; +}; + +struct lsp_completion_item { + struct s8 label; + enum completion_item_kind kind; + struct s8 detail; + struct s8 sort_text; + struct s8 filter_text; + struct s8 insert_text; + + enum text_edit_type edit_type; + union edit_ { + struct text_edit text_edit; + struct insert_replace_edit insert_replace_edit; + } edit; + + text_edit_vec additional_text_edits; + + struct lsp_command command; +}; + +typedef VEC(struct lsp_completion_item) completions_vec; + +struct completion_list { + bool incomplete; + completions_vec items; +}; + +struct rename_params { + struct text_document_position position; + struct s8 new_name; +}; + +struct parameter_information { + struct s8 label; + struct s8 documentation; +}; + +typedef VEC(struct parameter_information) param_info_vec; + +struct signature_information { + struct s8 label; + struct s8 documentation; + param_info_vec parameters; +}; + +typedef VEC(struct signature_information) signature_info_vec; + +struct signature_help { + uint32_t active_signature; + signature_info_vec signatures; +}; + +struct hover { + struct s8 contents; + struct region range; +}; + +struct reference_params { + struct text_document_position position; + bool include_declaration; +}; + +struct text_document_item text_document_item_from_buffer(struct buffer *buffer); +struct versioned_text_document_identifier +versioned_identifier_from_buffer(struct buffer *buffer); + +void versioned_text_document_identifier_free( + struct versioned_text_document_identifier *); +void text_document_item_free(struct text_document_item *); + +struct s8 did_change_text_document_params_to_json( + struct did_change_text_document_params *); +struct s8 +did_open_text_document_params_to_json(struct did_open_text_document_params *); +struct s8 +did_save_text_document_params_to_json(struct did_save_text_document_params *); + +struct publish_diagnostics_params +diagnostics_from_json(struct json_value *json); + +const char *diag_severity_to_str(enum diagnostic_severity severity); +uint32_t diag_severity_color(enum diagnostic_severity severity); +void diagnostic_free(struct diagnostic *); + +struct s8 document_position_to_json(struct text_document_position *position); +struct location_result location_result_from_json(struct json_value *json); +void location_result_free(struct location_result *res); + +struct s8 code_action_params_to_json(struct code_action_params *); + +struct code_actions lsp_code_actions_from_json(struct json_value *); +void lsp_code_actions_free(struct code_actions *); +struct s8 lsp_command_to_json(struct lsp_command *); + +text_edit_vec text_edits_from_json(struct json_value *); +void text_edits_free(text_edit_vec); +struct workspace_edit workspace_edit_from_json(struct json_value *); +void workspace_edit_free(struct workspace_edit *); + +struct s8 +document_formatting_params_to_json(struct document_formatting_params *); +struct s8 document_range_formatting_params_to_json( + struct document_range_formatting_params *); + +struct completion_list completion_list_from_json(struct json_value *); +void completion_list_free(struct completion_list *); + +struct s8 rename_params_to_json(struct rename_params *); + +struct signature_help signature_help_from_json(struct json_value *); +void signature_help_free(struct signature_help *); + +struct hover hover_from_json(struct json_value *); +void hover_free(struct hover *); + +struct s8 reference_params_to_json(struct reference_params *); + +#endif diff --git a/src/main/main.c b/src/main/main.c index fa740e8..12ed1ec 100644 --- a/src/main/main.c +++ b/src/main/main.c @@ -38,6 +38,7 @@ #include "bindings.h" #include "cmds.h" #include "completion.h" +#include "frame-hooks.h" #include "version.h" /* welcome.h is generated from welcome.inc with @@ -86,12 +87,21 @@ void segfault(int sig) { abort(); } +/* void __asan_on_error() { + if (display != NULL) { + display_clear(display); + display_destroy(display); + } +} */ + #define INVALID_WATCH (uint32_t) - 1 static void clear_buffer_props(struct buffer *buffer, void *userdata) { (void)userdata; - buffer_clear_text_properties(buffer); + if (!buffer->retain_properties) { + buffer_clear_text_properties(buffer); + } } struct watched_file { @@ -275,6 +285,10 @@ int main(int argc, char *argv[]) { buffers_add_add_hook(&buflist, watch_file, (void *)reactor); + init_bindings(); + + init_completion(&buflist); + #ifdef SYNTAX_ENABLE char *treesitter_path_env = getenv("TREESITTER_GRAMMARS"); struct setting *path_setting = settings_get("editor.grammars-path"); @@ -324,7 +338,7 @@ int main(int argc, char *argv[]) { #endif #ifdef LSP_ENABLE - lang_servers_init(reactor, &buflist); + lang_servers_init(reactor, &buflist, &commands); #endif struct buffer initial_buffer = buffer_create("welcome"); @@ -361,20 +375,21 @@ int main(int argc, char *argv[]) { register_settings_commands(&commands); struct keymap *current_keymap = NULL; - init_bindings(); - - init_completion(&buflist, &commands); timers_init(); + init_frame_hooks(); float frame_time = 0.f; static char keyname[64] = {0}; static uint32_t nkeychars = 0; + bool needs_render = true; + while (running) { timers_start_frame(); if (display_resized) { windows_resize(display_height(display), display_width(display)); display_resized = false; + needs_render = true; } // TODO: maybe this should be hidden behind something @@ -383,7 +398,7 @@ int main(int argc, char *argv[]) { /* Update all windows together with the buffers in them. */ struct timer *update_windows = timer_start("update-windows"); - windows_update(frame_alloc, frame_time); + needs_render |= windows_update(frame_alloc, frame_time); timer_stop(update_windows); struct window *active_window = windows_get_active(); @@ -392,26 +407,36 @@ int main(int argc, char *argv[]) { * from updating the buffers. */ struct timer *update_display = timer_start("display"); - display_begin_render(display); - windows_render(display); - struct buffer_view *view = window_buffer_view(active_window); - struct location cursor = buffer_view_dot_to_visual(view); - struct window_position winpos = window_position(active_window); - display_move_cursor(display, winpos.y + cursor.line, winpos.x + cursor.col); - display_end_render(display); + if (needs_render) { + display_begin_render(display); + windows_render(display); + struct buffer_view *view = window_buffer_view(active_window); + struct location cursor = buffer_view_dot_to_visual(view); + struct window_position winpos = window_position(active_window); + display_move_cursor(display, winpos.y + cursor.line, + winpos.x + cursor.col); + display_end_render(display); + needs_render = false; + } timer_stop(update_display); - /* This blocks for events, so if nothing has happened we block here and let - * the CPU do something more useful than updating this editor for no reason. - * This is also the reason that there is no timed scope around this, it - * simply makes no sense. + /* if we have dispatched frame hooks, they need a + * full cycle of updates. */ - reactor_update(reactor); + if (dispatch_next_frame_hooks() == 0) { + /* This blocks for events, so if nothing has happened we block here and + * let the CPU do something more useful than updating this editor for no + * reason. This is also the reason that there is no timed scope around + * this, it simply makes no sense. + */ + reactor_update(reactor); + } struct timer *update_keyboard = timer_start("update-keyboard"); struct keyboard_update kbd_upd = keyboard_update(&kbd, reactor, frame_alloc); + needs_render |= kbd_upd.nkeys > 0; for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) { struct key *k = &kbd_upd.keys[ki]; @@ -457,7 +482,7 @@ int main(int argc, char *argv[]) { if (nkeychars < 64) { nkeychars += key_name(k, keyname + nkeychars, 64 - nkeychars); - minibuffer_echo("%s", keyname); + minibuffer_display("%s", keyname); } current_keymap = res.data.keymap; @@ -472,10 +497,10 @@ int main(int argc, char *argv[]) { char keyname[16]; key_name(k, keyname, 16); if (current_keymap == NULL) { - minibuffer_echo_timeout(4, "key \"%s\" is not bound!", keyname); + minibuffer_display_timeout(4, "key \"%s\" is not bound!", keyname); } else { - minibuffer_echo_timeout(4, "key \"%s %s\" is not bound!", - current_keymap->name, keyname); + minibuffer_display_timeout(4, "key \"%s %s\" is not bound!", + current_keymap->name, keyname); } current_keymap = NULL; nkeychars = 0; @@ -498,13 +523,10 @@ int main(int argc, char *argv[]) { frame_allocator_clear(&frame_allocator); } + teardown_frame_hooks(); timers_destroy(); teardown_global_commands(); - destroy_completion(); windows_destroy(); - minibuffer_destroy(); - buffer_destroy(&minibuffer); - buffers_destroy(&buflist); #ifdef SYNTAX_ENABLE syntax_teardown(); @@ -514,6 +536,11 @@ int main(int argc, char *argv[]) { lang_servers_teardown(); #endif + destroy_completion(); + minibuffer_destroy(); + buffer_destroy(&minibuffer); + buffers_destroy(&buflist); + display_clear(display); display_destroy(display); destroy_bindings(); diff --git a/test/assert.c b/test/assert.c index b252d36..2fa8a89 100644 --- a/test/assert.c +++ b/test/assert.c @@ -1,20 +1,47 @@ #include "assert.h" #include <signal.h> +#include <stdarg.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -void assert(bool cond, const char *cond_str, const char *file, int line, - const char *msg) { +static void assert_internal(bool cond, const char *cond_str, const char *file, + int line, const char *msg, va_list args) { if (!cond) { - printf("\n%s:%d: assert failed (%s): %s\n", file, line, cond_str, msg); + va_list args2; + va_copy(args2, args); + + ssize_t res = vsnprintf(NULL, 0, msg, args); + char *buf = (char *)msg; + + if (res != -1) { + buf = malloc(res + 1); + vsnprintf(buf, res + 1, msg, args2); + } + + va_end(args); + + printf("\n%s:%d: assert failed (%s): %s\n", file, line, cond_str, buf); + + if (buf != msg) { + free(buf); + } raise(SIGABRT); } } +void assert(bool cond, const char *cond_str, const char *file, int line, + const char *msg, ...) { + va_list args; + va_start(args, msg); + assert_internal(cond, cond_str, file, line, msg, args); +} + void assert_streq(const char *left, const char *right, const char *file, - int line, const char *msg) { - assert(strcmp(left, right) == 0, "<left string> == <right string>", file, - line, msg); + int line, const char *msg, ...) { + va_list args; + va_start(args, msg); + assert_internal(strcmp(left, right) == 0, "<left string> == <right string>", + file, line, msg, args); } diff --git a/test/assert.h b/test/assert.h index 8b730b2..b9b5719 100644 --- a/test/assert.h +++ b/test/assert.h @@ -1,10 +1,10 @@ #include <stdbool.h> -#define ASSERT(cond, msg) assert(cond, #cond, __FILE__, __LINE__, msg) -#define ASSERT_STR_EQ(left, right, msg) \ - assert_streq(left, right, __FILE__, __LINE__, msg) +#define ASSERT(cond, msg, ...) assert(cond, #cond, __FILE__, __LINE__, msg, ##__VA_ARGS__) +#define ASSERT_STR_EQ(left, right, msg, ...) \ + assert_streq(left, right, __FILE__, __LINE__, msg, ##__VA_ARGS__) void assert(bool cond, const char *cond_str, const char *file, int line, - const char *msg); + const char *msg, ...); void assert_streq(const char *left, const char *right, const char *file, - int line, const char *msg); + int line, const char *msg, ...); diff --git a/test/buffer.c b/test/buffer.c index 514671b..a8268da 100644 --- a/test/buffer.c +++ b/test/buffer.c @@ -103,12 +103,19 @@ static void test_delete(void) { static void test_word_at(void) { struct buffer b = buffer_create("test-word-at-buffer"); - const char *txt = "word1 (word2). Another"; - buffer_add(&b, (struct location){.line = 0, .col = 0}, (uint8_t *)txt, - strlen(txt)); + const char *txt = "word1"; + struct location at = buffer_add(&b, (struct location){.line = 0, .col = 0}, + (uint8_t *)txt, strlen(txt)); struct region word1 = - buffer_word_at(&b, (struct location){.line = 0, .col = 0}); + buffer_word_at(&b, (struct location){.line = 0, .col = 5}); + ASSERT(region_has_size(word1), "expected 0,0 to be a word even if only word"); + ASSERT(word1.begin.col == 0 && word1.end.col == 5, + "Expected only word to end at col 5"); + + const char *txt2 = " (word2). Another"; + buffer_add(&b, at, (uint8_t *)txt2, strlen(txt2)); + word1 = buffer_word_at(&b, (struct location){.line = 0, .col = 0}); ASSERT(region_has_size(word1), "expected 0,0 to be a word"); ASSERT(word1.begin.col == 0 && word1.end.col == 5, "Expected word to end at col 5"); @@ -179,7 +186,7 @@ static void test_char_movement(void) { static void test_word_movement(void) { struct buffer b = buffer_create("test-word-movement-buffer"); - const char *txt = " word1, word2 \"word3\" word4"; + const char *txt = " word1, word2 \"word3\" word4"; buffer_add(&b, buffer_end(&b), (uint8_t *)txt, strlen(txt)); struct location next = buffer_next_word(&b, (struct location){.line = 0, .col = 0}); @@ -192,19 +199,29 @@ static void test_word_movement(void) { ASSERT(next.col == 15, "Expected next word to start at col 15"); next = buffer_next_word(&b, (struct location){.line = 0, .col = 15}); - ASSERT(next.col == 22, "Expected next word to start at col 22"); + ASSERT(next.col == 24, "Expected next word to start at col 24"); struct location prev = buffer_previous_word(&b, (struct location){.line = 0, .col = 26}); - ASSERT(prev.col == 22, "Expected previous word to start at col 22"); + ASSERT(prev.col == 24, "Expected previous word to start at col 24"); - prev = buffer_previous_word(&b, (struct location){.line = 0, .col = 22}); + prev = buffer_previous_word(&b, (struct location){.line = 0, .col = 24}); ASSERT(prev.col == 15, "Expected previous word to start at col 15"); + prev = buffer_previous_word(&b, (struct location){.line = 0, .col = 3}); + ASSERT(prev.col == 0 && prev.line == 0, + "Expected first word to start at start of first line"); + prev = buffer_previous_word(&b, (struct location){.line = 0, .col = 0}); ASSERT(prev.col == 0 && prev.line == 0, "Expected previous word to not go before beginning of buffer"); + const char *txt2 = " word"; + buffer_add(&b, buffer_end(&b), (uint8_t *)txt2, strlen(txt2)); + prev = buffer_previous_word(&b, (struct location){.line = 1, .col = 8}); + ASSERT(prev.col == 0 && prev.line == 1, + "Expected to be at start of line if there are no words"); + buffer_destroy(&b); } diff --git a/test/bufread.c b/test/bufread.c new file mode 100644 index 0000000..d477946 --- /dev/null +++ b/test/bufread.c @@ -0,0 +1,63 @@ +#ifdef LINUX +#define _GNU_SOURCE +#endif + +#include "assert.h" +#include "test.h" + +#include "dged/bufread.h" + +#include <stdint.h> +#include <unistd.h> + +#ifdef LINUX +#include <sys/mman.h> +#endif + +static void test_read(void) { +#ifdef LINUX + int memfd = memfd_create("bufread-test", 0); + ASSERT(memfd >= 0, "Failed to create memfd"); +#endif + for (int i = 0; i < 256; ++i) { + int a = write(memfd, (uint8_t *)&i, 1); + (void)a; + } + lseek(memfd, 0, SEEK_SET); + + struct bufread *br = bufread_create(memfd, 128); + uint8_t buf[32]; + ssize_t read = bufread_read(br, buf, 32); + ASSERT(read > 0, "Expected to be able to read"); + for (int i = 0; i < 32; ++i) { + ASSERT(i == buf[i], "Expected buffer to be monotonically increasing"); + } + bufread_read(br, buf, 32); + bufread_read(br, buf, 32); + bufread_read(br, buf, 32); + + read = bufread_read(br, buf, 32); + ASSERT(read > 0, "Expected to be able to read"); + for (int i = 0; i < 32; ++i) { + ASSERT((i + 128) == buf[i], + "Expected buffer to be monotonically increasing"); + } + bufread_destroy(br); +} + +void test_empty_read(void) { +#ifdef LINUX + int memfd = memfd_create("bufread-test", 0); + ASSERT(memfd >= 0, "Failed to create memfd"); +#endif + struct bufread *br = bufread_create(memfd, 128); + uint8_t buf[32]; + ssize_t read = bufread_read(br, buf, 32); + ASSERT(read == 0, "Expected to not be able to read from empty stream"); + bufread_destroy(br); +} + +void run_bufread_tests(void) { + run_test(test_read); + run_test(test_empty_read); +} diff --git a/test/json.c b/test/json.c index c67fc75..0243ee3 100644 --- a/test/json.c +++ b/test/json.c @@ -3,16 +3,64 @@ #include "dged/json.h" +#include <stdlib.h> #include <string.h> +#include <sys/stat.h> + +struct parsed_json { + struct json_result result; + uint8_t *buf; +}; + +static struct parsed_json json_parse_error(const char *msg) { + return (struct parsed_json){ + .result = + (struct json_result){ + .ok = false, + .result.error = msg, + }, + .buf = NULL, + }; +} + +static struct parsed_json parse_json_file(const char *path) { + struct stat sb; + if (stat(path, &sb) != 0) { + return json_parse_error("file not found"); + } + + FILE *file = fopen(path, "r"); + if (fseek(file, 0, SEEK_END) != 0) { + return json_parse_error("fseek to end failed"); + } + + long sz = ftell(file); + if (sz == -1) { + return json_parse_error("ftell failed"); + } + + rewind(file); + + uint8_t *buff = malloc(sz); + int bytes = fread(buff, 1, sz, file); + if (bytes != sz) { + return json_parse_error("did not read whole file"); + } + + return (struct parsed_json){ + .result = json_parse(buff, sz), + .buf = buff, + }; +} -void test_empty_parse(void) { +static void test_empty_parse(void) { struct json_result res = json_parse((uint8_t *)"", 0); ASSERT(res.ok, "Expected empty parse to work"); json_destroy(&res.result.document); } -void test_empty_array(void) { +static void test_empty_array(void) { struct json_result res = json_parse((uint8_t *)"[]", 2); ASSERT(res.ok, "Expected parse of empty array to work"); @@ -23,7 +71,7 @@ void test_empty_array(void) { json_destroy(&root); } -void test_array(void) { +static void test_array(void) { struct json_result res = json_parse((uint8_t *)"[ 1, 2, 4 ]", 11); ASSERT(res.ok, "Expected parse of number array to work"); @@ -33,7 +81,7 @@ void test_array(void) { json_destroy(&root); - const char *jsn = "[ \"hello\", \"world\" ]"; + const char *jsn = "[ \"hello\", \"world\", \"\\\"\" ]"; res = json_parse((uint8_t *)jsn, strlen(jsn)); ASSERT(res.ok, "Expected parse of string array to work"); root = res.result.document; @@ -46,7 +94,7 @@ void test_array(void) { json_destroy(&root); } -void test_object(void) { +static void test_object(void) { struct json_result res = json_parse((uint8_t *)"{ }", 3); ASSERT(res.ok, "Expected parse of empty object to work"); struct json_value root = res.result.document; @@ -55,7 +103,8 @@ void test_object(void) { json_destroy(&root); - const char *jsn = "{ \"name\": \"Kalle Kula\", \"age\": 33, }"; + const char *jsn = "{ \"name\": \"Kalle Kula\", \"age\": 33, \"ball\": true, " + "\"square\": false }"; res = json_parse((uint8_t *)jsn, strlen(jsn)); ASSERT(res.ok, "Expected parse of simple object to work"); root = res.result.document; @@ -67,13 +116,17 @@ void test_object(void) { ASSERT(age->type == Json_Number, "Expected age to (just?) be a number"); ASSERT(age->value.number == 33, "Expected age to be 33"); + struct json_value *ball = json_get(root.value.object, s8("ball")); + ASSERT(ball->type == Json_Bool, "Expected ball to be a boolean."); + ASSERT(ball->value.boolean, "Expected Kalle Kulla to be a ball."); + json_destroy(&root); jsn = "{ \"name\": \"Kalle Kula\", \"age\": 33, \"kids\": " "[ " "{ \"name\": \"Sune Kula\", \"age\": 10, }, " - "{ \"name\": \"Suna Kula\", \"age\": 7 } " - "] }"; + "{ \"name\": \"Suna Kula\", \"age\": 7 }, " + "], \"pet_names\": [ \"fido\", \"fado\" ], \"ball\": true }"; res = json_parse((uint8_t *)jsn, strlen(jsn)); ASSERT(res.ok, "Expected parse of nested object to work"); root = res.result.document; @@ -84,12 +137,67 @@ void test_object(void) { struct json_value *kids = json_get(root.value.object, s8("kids")); ASSERT(kids->type == Json_Array, "Expected kids to be array"); + ball = json_get(root.value.object, s8("ball")); + ASSERT(ball->type == Json_Bool, "Expected ball to be a boolean."); + ASSERT(ball->value.boolean, "Expected Kalle Kulla to be a ball."); + json_destroy(&root); } +#define xstr(s) str(s) +#define str(s) #s + +#define test_parse_file(path) \ + { \ + struct parsed_json parsed = parse_json_file(xstr(TEST_ROOT) "/" path); \ + ASSERT(parsed.result.ok, "Expected parsing of " #path " to work: %s", \ + parsed.result.result.error); \ + json_destroy(&parsed.result.result.document); \ + free(parsed.buf); \ + } + +static void test_files(void) { test_parse_file("json/diag.json"); } + +static void test_brackets_in_strings(void) { + struct parsed_json parsed = + parse_json_file(xstr(TEST_ROOT) "/json/diag2.json"); + ASSERT(parsed.result.ok, "Expected parsing of diag2.json to work"); + + struct json_value *params = + json_get(parsed.result.result.document.value.object, s8("params")); + ASSERT(params != NULL, "Expected JSON object to contain params"); + ASSERT(params->type == Json_Object, "Expected params to be a JSON object"); + + struct json_value *diagnostics = + json_get(params->value.object, s8("diagnostics")); + ASSERT(diagnostics != NULL, "Expected params to contain diagnostics"); + ASSERT(diagnostics->type == Json_Array, "Expected params to be a JSON array"); + + struct json_array *diags = diagnostics->value.array; + ASSERT(json_array_len(diags) == 5, + "Expected diagnostics array to contain five items"); + + json_destroy(&parsed.result.result.document); + free(parsed.buf); +} + +static void test_str_escape(void) { + struct s8 res = unescape_json_string(s8("a\n\\n\r\\rb")); + ASSERT(s8eq(res, s8("a\n\n\r\rb")), "Expected \\n and \\r to not be removed"); + + s8delete(res); + + struct s8 res2 = unescape_json_string(s8(" \\\\\\n")); + ASSERT(s8eq(res2, s8(" \\\n")), "Expected \\ and \\n to not be removed"); + s8delete(res2); +} + void run_json_tests(void) { run_test(test_empty_parse); run_test(test_empty_array); run_test(test_array); run_test(test_object); + run_test(test_files); + run_test(test_brackets_in_strings); + run_test(test_str_escape); } diff --git a/test/json/diag.json b/test/json/diag.json new file mode 100644 index 0000000..4384ea3 --- /dev/null +++ b/test/json/diag.json @@ -0,0 +1,54 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/publishDiagnostics", + "params": { + "diagnostics": [ + { + "code": "unused-includes", + "codeDescription": { + "href": "https://clangd.llvm.org/guides/include-cleaner" + }, + "message": "Included header errno.h is not used directly (fixes available)", + "range": { + "end": { + "character": 18, + "line": 4 + }, + "start": { + "character": 0, + "line": 4 + } + }, + "severity": 2, + "source": "clangd", + "tags": [ + 1 + ] + }, + { + "code": "unused-includes", + "codeDescription": { + "href": "https://clangd.llvm.org/guides/include-cleaner" + }, + "message": "Included header hash.h is not used directly (fixes available)", + "range": { + "end": { + "character": 17, + "line": 16 + }, + "start": { + "character": 0, + "line": 16 + } + }, + "severity": 2, + "source": "clangd", + "tags": [ + 1 + ] + } + ], + "uri": "file:///home/abbe/code/dged/src/dged/syntax.c", + "version": 0 + } +} diff --git a/test/json/diag2.json b/test/json/diag2.json new file mode 100644 index 0000000..3c0989d --- /dev/null +++ b/test/json/diag2.json @@ -0,0 +1 @@ +{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{"diagnostics":[{"code":"expected_after","message":"Expected ';' after struct (fix available)","range":{"end":{"character":3,"line":4},"start":{"character":0,"line":4}},"severity":1,"source":"clang"},{"code":"-Wmissing-declarations","message":"Declaration does not declare anything","range":{"end":{"character":6,"line":1},"start":{"character":0,"line":1}},"severity":1,"source":"clang"},{"code":"expected","message":"Expected '}'\n\ntest.c:9:13: note: to match this '{'","range":{"end":{"character":13,"line":8},"start":{"character":13,"line":8}},"severity":1,"source":"clang"},{"message":"To match this '{'\n\ntest.c:9:14: error: expected '}'","range":{"end":{"character":13,"line":8},"start":{"character":12,"line":8}},"severity":3},{"code":"expected_after","message":"Expected ';' after struct (fix available)","range":{"end":{"character":13,"line":8},"start":{"character":13,"line":8}},"severity":1,"source":"clang"}],"uri":"file:///home/abbe/code/dged/test.c","version":33}} diff --git a/test/main.c b/test/main.c index f8e1eca..eba54a6 100644 --- a/test/main.c +++ b/test/main.c @@ -53,6 +53,9 @@ int main(void) { printf("\nš \x1b[1;36mRunning container tests...\x1b[0m\n"); run_container_tests(); + printf("\nš \x1b[1;36mRunning bufread tests...\x1b[0m\n"); + run_bufread_tests(); + #if defined(LSP_ENABLED) printf("\nš \x1b[1;36mRunning JSON tests...\x1b[0m\n"); run_json_tests(); diff --git a/test/test.h b/test/test.h index d099cc3..3f734ad 100644 --- a/test/test.h +++ b/test/test.h @@ -21,5 +21,6 @@ void run_minibuffer_tests(void); void run_settings_tests(void); void run_container_tests(void); void run_json_tests(void); +void run_bufread_tests(void); #endif |
