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/lsp | |
| 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/lsp')
| -rw-r--r-- | src/main/lsp/actions.c | 129 | ||||
| -rw-r--r-- | src/main/lsp/actions.h | 10 | ||||
| -rw-r--r-- | src/main/lsp/choice-buffer.c | 201 | ||||
| -rw-r--r-- | src/main/lsp/choice-buffer.h | 23 | ||||
| -rw-r--r-- | src/main/lsp/completion.c | 405 | ||||
| -rw-r--r-- | src/main/lsp/completion.h | 18 | ||||
| -rw-r--r-- | src/main/lsp/diagnostics.c | 386 | ||||
| -rw-r--r-- | src/main/lsp/diagnostics.h | 26 | ||||
| -rw-r--r-- | src/main/lsp/format.c | 149 | ||||
| -rw-r--r-- | src/main/lsp/format.h | 18 | ||||
| -rw-r--r-- | src/main/lsp/goto.c | 297 | ||||
| -rw-r--r-- | src/main/lsp/goto.h | 23 | ||||
| -rw-r--r-- | src/main/lsp/help.c | 101 | ||||
| -rw-r--r-- | src/main/lsp/help.h | 16 | ||||
| -rw-r--r-- | src/main/lsp/references.c | 248 | ||||
| -rw-r--r-- | src/main/lsp/references.h | 19 | ||||
| -rw-r--r-- | src/main/lsp/rename.c | 61 | ||||
| -rw-r--r-- | src/main/lsp/rename.h | 16 | ||||
| -rw-r--r-- | src/main/lsp/types.c | 1081 | ||||
| -rw-r--r-- | src/main/lsp/types.h | 385 |
20 files changed, 3612 insertions, 0 deletions
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 |
