From 9ee67ef5097451705b4ae9cb28895f726843cdf3 Mon Sep 17 00:00:00 2001 From: pommicket Date: Sun, 25 Dec 2022 15:58:30 -0500 Subject: handle complete vs incomplete lists --- autocomplete.c | 90 ++++++++++++++++++++++++++++++++++++++-------------------- command.c | 2 +- json.c | 14 +++++++++ lsp-parse.c | 3 ++ lsp.h | 1 + main.c | 1 - ted.h | 18 +++++++++--- 7 files changed, 93 insertions(+), 36 deletions(-) diff --git a/autocomplete.c b/autocomplete.c index 8f3df10..2955183 100644 --- a/autocomplete.c +++ b/autocomplete.c @@ -99,39 +99,65 @@ static bool autocomplete_using_lsp(Ted *ted) { static void autocomplete_no_suggestions(Ted *ted) { Autocomplete *ac = &ted->autocomplete; - if (ac->trigger_char == 0) + if (ac->trigger == TRIGGER_INVOKED) ted->cursor_error_time = time_get_seconds(); autocomplete_close(ted); } -static void autocomplete_find_completions(Ted *ted, char32_t trigger_char) { +static void autocomplete_send_completion_request(Ted *ted, TextBuffer *buffer, BufferPos pos, uint32_t trigger) { + LSP *lsp = buffer_lsp(buffer); + Autocomplete *ac = &ted->autocomplete; + + LSPRequest request = { + .type = LSP_REQUEST_COMPLETION + }; + + LSPCompletionTriggerKind lsp_trigger = LSP_TRIGGER_CHARACTER; + switch (trigger) { + case TRIGGER_INVOKED: lsp_trigger = LSP_TRIGGER_INVOKED; break; + case TRIGGER_INCOMPLETE: lsp_trigger = LSP_TRIGGER_INCOMPLETE; break; + } + + request.data.completion = (LSPRequestCompletion) { + .position = { + .document = lsp_document_id(lsp, buffer->filename), + .pos = buffer_pos_to_lsp(buffer, pos) + }, + .context = { + .trigger_kind = lsp_trigger, + .trigger_character = {0}, + } + }; + if (trigger < UNICODE_CODE_POINTS) + unicode_utf32_to_utf8(request.data.completion.context.trigger_character, trigger); + lsp_send_request(lsp, &request); + ac->waiting_for_lsp = true; + ac->lsp_request_time = ted->frame_time; + // *technically sepaking* this can mess things up if a complete + // list arrives only after the user has typed some stuff + // (in that case we'll send a TriggerKind = incomplete request even though it makes no sense). + // but i don't think any servers will have a problem with that. + ac->is_list_complete = false; +} + +static void autocomplete_find_completions(Ted *ted, uint32_t trigger) { Autocomplete *ac = &ted->autocomplete; TextBuffer *buffer = ted->active_buffer; BufferPos pos = buffer->cursor_pos; if (buffer_pos_eq(pos, ac->last_pos)) return; // no need to update completions. - ac->trigger_char = trigger_char; + ac->trigger = trigger; ac->last_pos = pos; LSP *lsp = buffer_lsp(buffer); if (lsp) { - LSPRequest request = { - .type = LSP_REQUEST_COMPLETION - }; - bool invoked = ac->trigger_char == 0 || is_word(ac->trigger_char); - request.data.completion = (LSPRequestCompletion) { - .position = { - .document = lsp_document_id(lsp, buffer->filename), - .pos = buffer_pos_to_lsp(buffer, pos) - }, - .context = { - .trigger_kind = invoked ? LSP_TRIGGER_INVOKED : LSP_TRIGGER_CHARACTER, - .trigger_character = {0}, - } - }; - unicode_utf32_to_utf8(request.data.completion.context.trigger_character, ac->trigger_char); - lsp_send_request(lsp, &request); - ac->waiting_for_lsp = true; + if (ac->is_list_complete && trigger == TRIGGER_INCOMPLETE) { + // the list of completions we got from the LSP server is complete, + // so we just need to call autocomplete_update_suggested, + // we don't need to send a new request. + } else { + autocomplete_send_completion_request(ted, buffer, pos, trigger); + } } else { // tag completion autocomplete_clear_completions(ted); @@ -150,6 +176,10 @@ static void autocomplete_find_completions(Ted *ted, char32_t trigger_char) { arr_add(ac->suggested, (u32)i); } free(completions); + + // if we got the full list of tags beginning with `word_at_cursor`, + // then we don't need to call `tags_beginning_with` again. + ac->is_list_complete = ncompletions == TAGS_MAX_COMPLETIONS; } autocomplete_update_suggested(ted); @@ -157,7 +187,6 @@ static void autocomplete_find_completions(Ted *ted, char32_t trigger_char) { static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response) { Autocomplete *ac = &ted->autocomplete; - bool was_waiting = ac->waiting_for_lsp; ac->waiting_for_lsp = false; if (!ac->open) { // user hit escape or down or something before completions arrived. @@ -186,6 +215,7 @@ static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *respo ted_completion->documentation = *documentation ? str_dup(documentation) : NULL; } + ac->is_list_complete = completion->is_complete; } autocomplete_update_suggested(ted); switch (arr_len(ac->suggested)) { @@ -193,15 +223,16 @@ static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *respo autocomplete_no_suggestions(ted); return; case 1: - // if we just finished loading suggestions, and there's only one suggestion, use it - if (was_waiting) + // if autocomplete was invoked by Ctrl+Space, and there's only one completion, select it. + if (ac->trigger == TRIGGER_INVOKED) autocomplete_complete(ted, ac->completions[ac->suggested[0]]); return; } } -// open autocomplete, or just do the completion if there's only one suggestion -static void autocomplete_open(Ted *ted, char32_t trigger_character) { +// open autocomplete +// trigger should either be a character (e.g. '.') or one of the TRIGGER_* constants. +static void autocomplete_open(Ted *ted, uint32_t trigger) { Autocomplete *ac = &ted->autocomplete; if (ac->open) return; if (!ted->active_buffer) return; @@ -212,8 +243,7 @@ static void autocomplete_open(Ted *ted, char32_t trigger_character) { ted->cursor_error_time = 0; ac->last_pos = (BufferPos){0,0}; ac->cursor = 0; - ac->open_time = ted->frame_time; - autocomplete_find_completions(ted, trigger_character); + autocomplete_find_completions(ted, trigger); switch (arr_len(ac->completions)) { case 0: @@ -261,13 +291,13 @@ static void autocomplete_frame(Ted *ted) { u32 const *colors = settings->colors; float const padding = settings->padding; - autocomplete_find_completions(ted, 0); - + autocomplete_find_completions(ted, TRIGGER_INCOMPLETE); + size_t ncompletions = arr_len(ac->suggested); if (ac->waiting_for_lsp && ncompletions == 0) { struct timespec now = ted->frame_time; - if (timespec_sub(now, ac->open_time) < 0.2) { + if (timespec_sub(now, ac->lsp_request_time) < 0.2) { // don't show "Loading..." unless we've actually been loading for a bit of time return; } diff --git a/command.c b/command.c index 82b096b..47dabef 100644 --- a/command.c +++ b/command.c @@ -287,7 +287,7 @@ void command_execute(Ted *ted, Command c, i64 argument) { if (ted->autocomplete.open) autocomplete_next(ted); else - autocomplete_open(ted, 0); + autocomplete_open(ted, TRIGGER_INVOKED); break; case CMD_AUTOCOMPLETE_BACK: if (ted->autocomplete.open) diff --git a/json.c b/json.c index 1174490..2dfb368 100644 --- a/json.c +++ b/json.c @@ -478,6 +478,20 @@ double json_array_get_number(const JSON *json, JSONArray array, size_t i) { return json_force_number(json_array_get(json, array, i)); } +bool json_force_bool(JSONValue x, bool default_value) { + if (x.type == JSON_TRUE) return true; + if (x.type == JSON_FALSE) return false; + return default_value; +} + +bool json_object_get_bool(const JSON *json, JSONObject object, const char *name, bool default_value) { + return json_force_bool(json_object_get(json, object, name), default_value); +} + +bool json_array_get_bool(const JSON *json, JSONArray array, size_t i, bool default_value) { + return json_force_bool(json_array_get(json, array, i), default_value); +} + // returns (JSONString){0} (which is interpreted as an empty string) if `x` is not a string JSONString json_force_string(JSONValue x) { if (x.type == JSON_STRING) { diff --git a/lsp-parse.c b/lsp-parse.c index 84b44e2..b5a05d0 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -113,6 +113,8 @@ static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) JSONValue result = json_get(json, "result"); JSONValue items_value = {0}; + completion->is_complete = true; // default + switch (result.type) { case JSON_NULL: // no completions @@ -122,6 +124,7 @@ static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) break; case JSON_OBJECT: items_value = json_object_get(json, result.val.object, "items"); + completion->is_complete = !json_object_get_bool(json, result.val.object, "isIncomplete", false); break; default: lsp_set_error(lsp, "Weird result type for textDocument/completion response: %s.", json_type_to_str(result.type)); diff --git a/lsp.h b/lsp.h index d68456a..4301636 100644 --- a/lsp.h +++ b/lsp.h @@ -220,6 +220,7 @@ typedef struct { } LSPCompletionItem; typedef struct { + bool is_complete; // dynamic array LSPCompletionItem *items; } LSPResponseCompletion; diff --git a/main.c b/main.c index 0781490..03d8be5 100644 --- a/main.c +++ b/main.c @@ -1,6 +1,5 @@ /* @TODO: -- stop typing from completing when there's only one copmletion left - in jdtls, opening an empty java file gives an exception. is this my fault? - what's wrong with pylsp (try "f.w") in generate.py (might be fixed now) - what's wrong with gopls? diff --git a/ted.h b/ted.h index 55a97eb..18defc4 100644 --- a/ted.h +++ b/ted.h @@ -379,16 +379,26 @@ typedef struct { SymbolKind kind; } Autocompletion; +enum { + // autocomplete was triggered by :autocomplete command + TRIGGER_INVOKED = 0x12000, + // autocomplete list needs to be updated because more characters were typed + TRIGGER_INCOMPLETE = 0x12001, +}; + typedef struct { bool open; // is the autocomplete window open? bool waiting_for_lsp; + bool is_list_complete; // should the completions array be updated when more characters are typed? - // which trigger character invoked this (0 if autocomplete was manually invoked) - char32_t trigger_char; + // what trigger caused the last request for completions: + // either a character code (for trigger characters), + // or one of the TRIGGER_* constants above + uint32_t trigger; - // when autocomplete menu was opened + // when we sent the request to the LSP for completions // (this is used to figure out when we should display "Loading...") - struct timespec open_time; + struct timespec lsp_request_time; Autocompletion *completions; // dynamic array of all completions u32 *suggested; // dynamic array of completions to be suggested (indices into completions) -- cgit v1.2.3