diff options
| author | Albert Cervin <albert@acervin.com> | 2024-09-17 08:47:03 +0200 |
|---|---|---|
| committer | Albert Cervin <albert@acervin.com> | 2025-11-01 22:11:14 +0100 |
| commit | 4459b8b3aa9d73895391785a99dcc87134e80601 (patch) | |
| tree | a5204f447a0b2b05f63504c7fe958ef9bbf1918a /src/main | |
| parent | 4689f3f38277bb64981fc960e8e384e2d065d659 (diff) | |
| download | dged-4459b8b3aa9d73895391785a99dcc87134e80601.tar.gz dged-4459b8b3aa9d73895391785a99dcc87134e80601.tar.xz dged-4459b8b3aa9d73895391785a99dcc87134e80601.zip | |
More lsp support
This makes the LSP support complete for now:
- Completion
- Diagnostics
- Goto implementation/declaration
- Rename
- Documentation
- Find references
Diffstat (limited to 'src/main')
36 files changed, 5690 insertions, 715 deletions
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(); |
