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/completion.c | |
| 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/completion.c')
| -rw-r--r-- | src/main/lsp/completion.c | 405 |
1 files changed, 405 insertions, 0 deletions
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); +} |
