summaryrefslogtreecommitdiff
path: root/src/main/lsp/completion.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/lsp/completion.c')
-rw-r--r--src/main/lsp/completion.c405
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(&params);
+ 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);
+}