From d77782564bf0a090a4e7fa4f4c4bb685383275dc Mon Sep 17 00:00:00 2001 From: pommicket Date: Thu, 29 Dec 2022 18:44:37 -0500 Subject: textDocument/definition for ctrl+click --- autocomplete.c | 545 ------------------------------------ buffer.c | 4 +- hover.c | 195 ------------- ide-autocomplete.c | 545 ++++++++++++++++++++++++++++++++++++ ide-definitions.c | 41 +++ ide-hover.c | 195 +++++++++++++ ide-signature-help.c | 153 ++++++++++ json.c | 764 -------------------------------------------------- lsp-json.c | 768 +++++++++++++++++++++++++++++++++++++++++++++++++++ lsp-parse.c | 73 ++++- lsp-write.c | 13 + lsp.c | 25 +- lsp.h | 22 +- main.c | 13 +- signature-help.c | 153 ---------- ted.c | 33 +++ ted.h | 19 +- 17 files changed, 1891 insertions(+), 1670 deletions(-) delete mode 100644 autocomplete.c delete mode 100644 hover.c create mode 100644 ide-autocomplete.c create mode 100644 ide-definitions.c create mode 100644 ide-hover.c create mode 100644 ide-signature-help.c delete mode 100644 json.c create mode 100644 lsp-json.c delete mode 100644 signature-help.c diff --git a/autocomplete.c b/autocomplete.c deleted file mode 100644 index f1c3c13..0000000 --- a/autocomplete.c +++ /dev/null @@ -1,545 +0,0 @@ -#define TAGS_MAX_COMPLETIONS 200 // max # of tag completions to scroll through -#define AUTOCOMPLETE_NCOMPLETIONS_VISIBLE 10 // max # of completions to show at once - -static void autocomplete_clear_completions(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - arr_foreach_ptr(ac->completions, Autocompletion, completion) { - free(completion->label); - free(completion->text); - free(completion->filter); - free(completion->detail); - free(completion->documentation); - } - arr_clear(ac->completions); - arr_clear(ac->suggested); -} - -// do the actual completion -static void autocomplete_complete(Ted *ted, Autocompletion completion) { - TextBuffer *buffer = ted->active_buffer; - buffer_start_edit_chain(buffer); // don't merge with other edits - if (is_word(buffer_char_before_cursor(buffer))) - buffer_backspace_words_at_cursor(buffer, 1); // delete whatever text was already typed - buffer_insert_utf8_at_cursor(buffer, completion.text); - buffer_end_edit_chain(buffer); - autocomplete_close(ted); -} - -static void autocomplete_select_cursor_completion(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - if (ac->open) { - size_t nsuggestions = arr_len(ac->suggested); - if (nsuggestions) { - i64 cursor = mod_i64(ac->cursor, (i64)nsuggestions); - autocomplete_complete(ted, ac->completions[ac->suggested[cursor]]); - autocomplete_close(ted); - } - } -} - -static void autocomplete_correct_scroll(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - i32 scroll = ac->scroll; - scroll = min_i32(scroll, (i32)arr_len(ac->suggested) - AUTOCOMPLETE_NCOMPLETIONS_VISIBLE); - scroll = max_i32(scroll, 0); - ac->scroll = scroll; -} - -void autocomplete_scroll(Ted *ted, i32 by) { - Autocomplete *ac = &ted->autocomplete; - ac->scroll += by; - autocomplete_correct_scroll(ted); -} - -static void autocomplete_move_cursor(Ted *ted, i32 by) { - Autocomplete *ac = &ted->autocomplete; - u32 ncompletions = arr_len(ac->suggested); - if (ncompletions == 0) - return; - i32 cursor = ac->cursor; - cursor += by; - cursor = (i32)mod_i32(cursor, (i32)ncompletions); - ac->cursor = cursor; - ac->scroll = ac->cursor - AUTOCOMPLETE_NCOMPLETIONS_VISIBLE / 2; - autocomplete_correct_scroll(ted); -} - -static void autocomplete_next(Ted *ted) { - autocomplete_move_cursor(ted, 1); -} - -static void autocomplete_prev(Ted *ted) { - autocomplete_move_cursor(ted, -1); -} - -void autocomplete_close(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - if (ac->open) { - ac->open = false; - ac->waiting_for_lsp = false; - autocomplete_clear_completions(ted); - } -} - -void autocomplete_update_suggested(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - arr_clear(ac->suggested); - char *word = str32_to_utf8_cstr( - buffer_word_at_cursor(ted->active_buffer) - ); - for (u32 i = 0; i < arr_len(ac->completions); ++i) { - Autocompletion *completion = &ac->completions[i]; - if (str_has_prefix(completion->filter, word)) - arr_add(ac->suggested, i); // suggest this one - } - free(word); -} - -static bool autocomplete_using_lsp(Ted *ted) { - return ted->active_buffer && buffer_lsp(ted->active_buffer) != NULL; -} - -static void autocomplete_no_suggestions(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - if (ac->trigger == TRIGGER_INVOKED) - ted->cursor_error_time = time_get_seconds(); - autocomplete_close(ted); -} - -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_position(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); - if (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 = trigger; - ac->last_pos = pos; - - LSP *lsp = buffer_lsp(buffer); - if (lsp) { - 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); - - char *word_at_cursor = str32_to_utf8_cstr(buffer_word_at_cursor(buffer)); - char **completions = calloc(TAGS_MAX_COMPLETIONS, sizeof *completions); - u32 ncompletions = (u32)tags_beginning_with(ted, word_at_cursor, completions, TAGS_MAX_COMPLETIONS); - free(word_at_cursor); - - arr_set_len(ac->completions, ncompletions); - - for (size_t i = 0; i < ncompletions; ++i) { - ac->completions[i].label = completions[i]; - ac->completions[i].text = str_dup(completions[i]); - ac->completions[i].filter = str_dup(completions[i]); - 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); -} - -static SymbolKind lsp_completion_kind_to_ted(LSPCompletionKind kind) { - switch (kind) { - case LSP_COMPLETION_TEXT: - case LSP_COMPLETION_MODULE: - case LSP_COMPLETION_UNIT: - case LSP_COMPLETION_COLOR: - case LSP_COMPLETION_FILE: - case LSP_COMPLETION_REFERENCE: - case LSP_COMPLETION_FOLDER: - case LSP_COMPLETION_OPERATOR: - return SYMBOL_OTHER; - - case LSP_COMPLETION_METHOD: - case LSP_COMPLETION_FUNCTION: - case LSP_COMPLETION_CONSTRUCTOR: - return SYMBOL_FUNCTION; - - case LSP_COMPLETION_FIELD: - case LSP_COMPLETION_PROPERTY: - return SYMBOL_FIELD; - - case LSP_COMPLETION_VARIABLE: - return SYMBOL_VARIABLE; - - case LSP_COMPLETION_CLASS: - case LSP_COMPLETION_INTERFACE: - case LSP_COMPLETION_ENUM: - case LSP_COMPLETION_STRUCT: - case LSP_COMPLETION_EVENT: - case LSP_COMPLETION_TYPEPARAMETER: - return SYMBOL_TYPE; - - case LSP_COMPLETION_VALUE: - case LSP_COMPLETION_ENUMMEMBER: - case LSP_COMPLETION_CONSTANT: - return SYMBOL_CONSTANT; - - case LSP_COMPLETION_KEYWORD: - case LSP_COMPLETION_SNIPPET: - return SYMBOL_KEYWORD; - } - assert(0); - return SYMBOL_OTHER; -} - - -static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response) { - const LSPRequest *request = &response->request; - if (request->type != LSP_REQUEST_COMPLETION) - return; - - Autocomplete *ac = &ted->autocomplete; - ac->waiting_for_lsp = false; - if (!ac->open) { - // user hit escape or down or something before completions arrived. - return; - } - - - const LSPResponseCompletion *completion = &response->data.completion; - size_t ncompletions = arr_len(completion->items); - arr_set_len(ac->completions, ncompletions); - for (size_t i = 0; i < ncompletions; ++i) { - const LSPCompletionItem *lsp_completion = &completion->items[i]; - Autocompletion *ted_completion = &ac->completions[i]; - ted_completion->label = str_dup(lsp_response_string(response, lsp_completion->label)); - ted_completion->filter = str_dup(lsp_response_string(response, lsp_completion->filter_text)); - // NOTE: here we don't deal with snippets. - // right now we are sending "snippetSupport: false" in the capabilities, - // so this should be okay. - ted_completion->text = str_dup(lsp_response_string(response, lsp_completion->text_edit.new_text)); - const char *detail = lsp_response_string(response, lsp_completion->detail); - ted_completion->detail = *detail ? str_dup(detail) : NULL; - ted_completion->kind = lsp_completion_kind_to_ted(lsp_completion->kind); - ted_completion->deprecated = lsp_completion->deprecated; - const char *documentation = lsp_response_string(response, lsp_completion->documentation); - ted_completion->documentation = *documentation ? str_dup(documentation) : NULL; - - } - ac->is_list_complete = completion->is_complete; - - autocomplete_update_suggested(ted); - switch (arr_len(ac->suggested)) { - case 0: - autocomplete_no_suggestions(ted); - return; - case 1: - // 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 -// 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; - TextBuffer *buffer = ted->active_buffer; - if (!buffer->filename) return; - if (buffer->view_only) return; - - ted->cursor_error_time = 0; - ac->last_pos = (BufferPos){0,0}; - ac->cursor = 0; - autocomplete_find_completions(ted, trigger); - - switch (arr_len(ac->completions)) { - case 0: - if (autocomplete_using_lsp(ted)) { - ac->open = true; - } else { - autocomplete_no_suggestions(ted); - } - break; - case 1: - autocomplete_complete(ted, ac->completions[0]); - // (^ this calls autocomplete_close) - break; - default: - // open autocomplete menu - ac->open = true; - break; - } -} - -static char symbol_kind_icon(SymbolKind k) { - switch (k) { - case SYMBOL_FUNCTION: - return 'f'; - case SYMBOL_FIELD: - return 'm'; - case SYMBOL_TYPE: - return 't'; - case SYMBOL_CONSTANT: - return 'c'; - case SYMBOL_VARIABLE: - return 'v'; - case SYMBOL_KEYWORD: - case SYMBOL_OTHER: - return ' '; - } - assert(0); - return ' '; -} - -static void autocomplete_frame(Ted *ted) { - Autocomplete *ac = &ted->autocomplete; - if (!ac->open) return; - - TextBuffer *buffer = ted->active_buffer; - Font *font = ted->font; - float char_height = text_font_char_height(font); - Settings const *settings = buffer_settings(buffer); - u32 const *colors = settings->colors; - float const padding = settings->padding; - - 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->lsp_request_time) < 0.2) { - // don't show "Loading..." unless we've actually been loading for a bit of time - return; - } - } - - if (!ac->waiting_for_lsp && ncompletions == 0) { - // no completions. close menu. - autocomplete_close(ted); - return; - } - - ac->cursor = ncompletions ? (i32)mod_i64(ac->cursor, (i64)ncompletions) : 0; - - autocomplete_correct_scroll(ted); - i32 scroll = ac->scroll; - u32 ncompletions_visible = min_u32((u32)ncompletions, AUTOCOMPLETE_NCOMPLETIONS_VISIBLE); - - float menu_width = 400, menu_height = (float)ncompletions_visible * char_height; - - if (ac->waiting_for_lsp && ncompletions == 0) { - menu_height = 200.f; - } - - v2 cursor_pos = buffer_pos_to_pixels(buffer, buffer->cursor_pos); - bool open_up = cursor_pos.y > 0.5f * (buffer->y1 + buffer->y2); // should the completion menu open upwards? - bool open_left = cursor_pos.x > 0.5f * (buffer->x1 + buffer->x2); - float x = cursor_pos.x, start_y = cursor_pos.y; - if (open_left) x -= menu_width; - if (open_up) - start_y -= menu_height; - else - start_y += char_height; // put menu below cursor - { - Rect menu_rect = rect(V2(x, start_y), V2(menu_width, menu_height)); - gl_geometry_rect(menu_rect, colors[COLOR_AUTOCOMPLETE_BG]); - gl_geometry_rect_border(menu_rect, 1, colors[COLOR_AUTOCOMPLETE_BORDER]); - ac->rect = menu_rect; - } - - i32 mouse_entry = scroll + (i32)((ted->mouse_pos.y - start_y) / char_height); - - Autocompletion *document = NULL; - if (ncompletions) { - assert(ac->cursor >= 0 && ac->cursor < (i32)ncompletions); - // highlight cursor entry - Rect r = rect(V2(x, start_y + (float)(ac->cursor - scroll) * char_height), V2(menu_width, char_height)); - if (rect_contains_point(ac->rect, rect_center(r))) { - gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_HL]); - document = &ac->completions[ac->suggested[ac->cursor]]; - } - } - if (mouse_entry >= 0 && mouse_entry < (i32)ncompletions - && rect_contains_point(ac->rect, ted->mouse_pos)) { - // highlight moused over entry - Rect r = rect(V2(x, start_y + (float)(mouse_entry - scroll) * char_height), V2(menu_width, char_height)); - gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_HL]); - ted->cursor = ted->cursor_hand; - document = &ac->completions[ac->suggested[mouse_entry]]; - } - - float border_thickness = settings->border_thickness; - - - if (document && document->documentation) { - // document that entry!! - - // we've got some wacky calculations to figure out the - // bounding rect for the documentation - float doc_width = open_left ? ac->rect.pos.x - 2*padding - : buffer->x2 - (ac->rect.pos.x + ac->rect.size.x + 2*padding); - if (doc_width > 800) doc_width = 800; - float doc_height = buffer->y2 - (ac->rect.pos.y + 2*padding); - if (doc_height > char_height * 20) doc_height = char_height * 20; - - // if the rect is too small, there's no point in showing it - if (doc_width >= 200) { - float doc_x = open_left ? ac->rect.pos.x - doc_width - padding - : ac->rect.pos.x + ac->rect.size.x + padding; - float doc_y = ac->rect.pos.y; - Rect r = rect(V2(doc_x, doc_y), V2(doc_width, doc_height)); - gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_BG]); - gl_geometry_rect_border(r, border_thickness, colors[COLOR_AUTOCOMPLETE_BORDER]); - - // draw the text! - TextRenderState text_state = text_render_state_default; - text_state.min_x = doc_x + padding; - text_state.max_x = doc_x + doc_width - padding; - text_state.max_y = doc_y + doc_height; - text_state.x = doc_x + padding; - text_state.y = doc_y + padding; - text_state.wrap = true; - rgba_u32_to_floats(colors[COLOR_TEXT], text_state.color); - text_utf8_with_state(font, &text_state, document->documentation); - } - } - - - for (uint i = 0; i < ted->nmouse_clicks[SDL_BUTTON_LEFT]; ++i) { - v2 click = ted->mouse_clicks[SDL_BUTTON_LEFT][i]; - if (rect_contains_point(ac->rect, click)) { - i32 entry = scroll + (i32)((click.y - start_y) / char_height); - if (entry >= 0 && entry < (i32)ncompletions) { - // entry was clicked on! use this completion. - autocomplete_complete(ted, ac->completions[ac->suggested[entry]]); - return; - } - } - } - - float y = start_y; - TextRenderState state = text_render_state_default; - state.min_x = x + padding; state.min_y = y; state.max_x = x + menu_width - padding; state.max_y = y + menu_height; - rgba_u32_to_floats(colors[COLOR_TEXT], state.color); - - if (ac->waiting_for_lsp && ncompletions == 0) { - state.x = x + padding; state.y = y; - text_utf8_with_state(font, &state, "Loading..."); - } else { - for (size_t i = 0; i < ncompletions_visible; ++i) { - const Autocompletion *completion = &ac->completions[ac->suggested[(i32)i + scroll]]; - - state.x = x; state.y = y; - if (i != ncompletions_visible-1) { - gl_geometry_rect(rect(V2(x, y + char_height), - V2(menu_width, border_thickness)), - colors[COLOR_AUTOCOMPLETE_BORDER]); - } - - ColorSetting label_color = color_for_symbol_kind(completion->kind); - if (!settings->syntax_highlighting) - label_color = COLOR_TEXT; - - rgba_u32_to_floats(colors[label_color], state.color); - - // draw icon - char icon_text[2] = {symbol_kind_icon(completion->kind), 0}; - state.x += padding; - text_utf8_with_state(font, &state, icon_text); - state.x += padding; - gl_geometry_rect(rect(V2((float)state.x, (float)state.y), V2(border_thickness, char_height)), - colors[COLOR_AUTOCOMPLETE_BORDER]); - state.x += padding; - - float label_x = (float)state.x; - text_utf8_with_state(font, &state, completion->label); - - const char *detail = completion->detail; - if (detail) { - double label_end_x = state.x; - - char show_text[128] = {0}; - - int amount_detail = 0; - for (; ; ++amount_detail) { - if (unicode_is_continuation_byte((u8)detail[amount_detail])) - continue; // don't cut off text in the middle of a code point. - - char text[128] = {0}; - strbuf_printf(text, "%.*s%s", amount_detail, detail, - (size_t)amount_detail == strlen(detail) ? "" : "..."); - double width = text_get_size_v2(font, text).x; - if (label_end_x + width + 2 * padding < state.max_x) { - strbuf_cpy(show_text, text); - } - // don't break if not, since we want to use "blabla" - // even if "blabl..." is too long - - if (!detail[amount_detail]) break; - } - if (amount_detail >= 3) { - //rgba_u32_to_floats(colors[COLOR_COMMENT], state.color); - text_utf8_anchored(font, show_text, state.max_x, state.y, - colors[COLOR_COMMENT], ANCHOR_TOP_RIGHT); - } - } - - if (completion->deprecated) { - gl_geometry_rect(rect(V2(label_x, y + (char_height - border_thickness) * 0.5f), - V2((float)state.x - label_x, 1)), - colors[label_color]); - } - - y += char_height; - } - } - - gl_geometry_draw(); - text_render(font); -} diff --git a/buffer.c b/buffer.c index 4562219..679da65 100644 --- a/buffer.c +++ b/buffer.c @@ -2489,12 +2489,14 @@ bool buffer_handle_click(Ted *ted, TextBuffer *buffer, v2 click, u8 times) { break; case KEY_MODIFIER_CTRL: if (!buffer->is_line_buffer) { + // go to definition buffer_cursor_move_to_pos(buffer, buffer_pos); String32 word = buffer_word_at_cursor(buffer); if (word.len) { char *tag = str32_to_utf8_cstr(word); if (tag) { - tag_goto(buffer->ted, tag); + LSPDocumentPosition pos = buffer_pos_to_lsp_document_position(buffer, buffer_pos); + definition_goto(buffer->ted, buffer_lsp(buffer), tag, pos); free(tag); } } diff --git a/hover.c b/hover.c deleted file mode 100644 index 9742e58..0000000 --- a/hover.c +++ /dev/null @@ -1,195 +0,0 @@ -// LSP hover information (textDocument/hover request) - -void hover_close(Ted *ted) { - Hover *hover = &ted->hover; - hover->open = false; - free(hover->text); - hover->text = NULL; -} - -static bool get_hover_position(Ted *ted, LSPDocumentPosition *pos, TextBuffer **pbuffer, LSP **lsp) { - // find the buffer where the mouse is - for (int i = 0; i < TED_MAX_BUFFERS; ++i) { - TextBuffer *buffer = &ted->buffers[i]; - if (!buffer->filename) continue; - LSP *l = buffer_lsp(buffer); - if (!l) continue; - BufferPos mouse_pos = {0}; - if (buffer_pixels_to_pos(buffer, ted->mouse_pos, &mouse_pos)) { - if (pos) *pos = buffer_pos_to_lsp_document_position(buffer, mouse_pos); - if (pbuffer) *pbuffer = buffer; - if (lsp) *lsp = l; - return true; - } - } - return false; -} - -void hover_send_request(Ted *ted) { - Hover *hover = &ted->hover; - LSPRequest request = {.type = LSP_REQUEST_HOVER}; - LSPRequestHover *h = &request.data.hover; - LSP *lsp = NULL; - if (get_hover_position(ted, &h->position, NULL, &lsp)) { - hover->requested_position = h->position; - hover->requested_lsp = lsp->id; - lsp_send_request(lsp, &request); - } -} - -void hover_process_lsp_response(Ted *ted, LSPResponse *response) { - if (!response) return; - if (response->request.type != LSP_REQUEST_HOVER) return; - - Hover *hover = &ted->hover; - LSPResponseHover *hover_response = &response->data.hover; - - TextBuffer *buffer=0; - LSPDocumentPosition pos={0}; - LSP *lsp=0; - get_hover_position(ted, &pos, &buffer, &lsp); - - if (hover->text // we already have hover text - && ( - lsp->id != hover->requested_lsp // this request is from a different LSP - || !lsp_document_position_eq(response->request.data.hover.position, pos) // this request is for a different position - )) { - // this is a stale request. ignore it - return; - } - - free(hover->text); hover->text = NULL; - - if (buffer) { - hover->range_start = buffer_pos_from_lsp(buffer, hover_response->range.start); - hover->range_end = buffer_pos_from_lsp(buffer, hover_response->range.end); - } - const char *contents = lsp_response_string(response, hover_response->contents); - if (*contents) { - hover->text = str_dup(contents); - char *p = hover->text + strlen(hover->text) - 1; - // remove trailing whitespace - // (rust-analyzer gives us trailing newlines for local variables) - for (; p > hover->text && isspace(*p); --p) - *p = '\0'; - } -} - -void hover_frame(Ted *ted, double dt) { - const Settings *settings = ted_active_settings(ted); - if (!settings->hover_enabled) - return; - Hover *hover = &ted->hover; - - bool shift_down = SDL_GetKeyboardState(NULL)[SDL_SCANCODE_LSHIFT] - || SDL_GetKeyboardState(NULL)[SDL_SCANCODE_RSHIFT]; - - if (!shift_down) { - hover_close(ted); - } - - (void)dt; - if (!hover->open) { - if (shift_down) { - hover_send_request(ted); - hover->open = true; - } - return; - } - - TextBuffer *buffer=0; - { - LSPDocumentPosition pos={0}; - LSP *lsp=0; - if (get_hover_position(ted, &pos, &buffer, &lsp)) { - if (lsp->id != hover->requested_lsp - || !lsp_document_position_eq(pos, hover->requested_position)) { - // refresh hover - hover_send_request(ted); - } - } else { - hover_close(ted); - return; - } - } - - - - const float padding = settings->padding; - const float border = settings->border_thickness; - const u32 *colors = settings->colors; - const char *text = hover->text; - Font *font = ted->font; - float x = ted->mouse_pos.x, y = ted->mouse_pos.y + font->char_height; - float char_height = font->char_height; - - BufferPos range_start = hover->range_start, range_end = hover->range_end; - if (!buffer_pos_eq(range_start, range_end)) { - // draw the highlight - if (range_start.line == range_end.line) { - v2 a = buffer_pos_to_pixels(buffer, range_start); - v2 b = buffer_pos_to_pixels(buffer, range_end); - b.y += char_height; - gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); - } else if (range_end.line - range_start.line < 1000) { // prevent gigantic highlights from slowing things down - // multiple lines. - v2 a = buffer_pos_to_pixels(buffer, range_start); - v2 b = buffer_pos_to_pixels(buffer, buffer_pos_end_of_line(buffer, range_start.line)); - b.y += char_height; - gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); - - for (u32 line = range_start.line + 1; line < range_end.line; ++line) { - // these lines are fully contained in the range. - BufferPos start = buffer_pos_start_of_line(buffer, line); - BufferPos end = buffer_pos_end_of_line(buffer, line); - a = buffer_pos_to_pixels(buffer, start); - b = buffer_pos_to_pixels(buffer, end); - b.y += char_height; - gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); - } - - // last line - a = buffer_pos_to_pixels(buffer, buffer_pos_start_of_line(buffer, range_end.line)); - b = buffer_pos_to_pixels(buffer, range_end); - b.y += char_height; - gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); - } - - } - if (hover->text) { - float max_width = 400; - TextRenderState state = text_render_state_default; - state.x = state.min_x = x; - state.y = state.min_y = y; - state.render = false; - state.wrap = true; - state.max_x = x + max_width; - state.max_y = ted->window_height; - text_utf8_with_state(font, &state, text); - float width = (float)(state.x_largest - x); - float height = (float)(state.y_largest - y) + char_height; - if (height > 300) { - height = 300; - } - - if (x + width > ted->window_width) - x -= width; // open left - if (y + height > ted->window_height) - y -= height + char_height * 2; // open up - state.x = state.min_x = x; - state.y = state.min_y = y; - state.max_x = x + max_width; - state.y = y; - state.render = true; - state.max_y = y + height; - - Rect rect = rect_xywh(x - padding, y - padding, width + 2*padding, height + 2*padding); - gl_geometry_rect(rect, colors[COLOR_HOVER_BG]); - gl_geometry_rect_border(rect, border, colors[COLOR_HOVER_BORDER]); - rgba_u32_to_floats(colors[COLOR_HOVER_TEXT], state.color); - text_utf8_with_state(font, &state, text); - } - - gl_geometry_draw(); - text_render(font); -} diff --git a/ide-autocomplete.c b/ide-autocomplete.c new file mode 100644 index 0000000..f1c3c13 --- /dev/null +++ b/ide-autocomplete.c @@ -0,0 +1,545 @@ +#define TAGS_MAX_COMPLETIONS 200 // max # of tag completions to scroll through +#define AUTOCOMPLETE_NCOMPLETIONS_VISIBLE 10 // max # of completions to show at once + +static void autocomplete_clear_completions(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + arr_foreach_ptr(ac->completions, Autocompletion, completion) { + free(completion->label); + free(completion->text); + free(completion->filter); + free(completion->detail); + free(completion->documentation); + } + arr_clear(ac->completions); + arr_clear(ac->suggested); +} + +// do the actual completion +static void autocomplete_complete(Ted *ted, Autocompletion completion) { + TextBuffer *buffer = ted->active_buffer; + buffer_start_edit_chain(buffer); // don't merge with other edits + if (is_word(buffer_char_before_cursor(buffer))) + buffer_backspace_words_at_cursor(buffer, 1); // delete whatever text was already typed + buffer_insert_utf8_at_cursor(buffer, completion.text); + buffer_end_edit_chain(buffer); + autocomplete_close(ted); +} + +static void autocomplete_select_cursor_completion(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + if (ac->open) { + size_t nsuggestions = arr_len(ac->suggested); + if (nsuggestions) { + i64 cursor = mod_i64(ac->cursor, (i64)nsuggestions); + autocomplete_complete(ted, ac->completions[ac->suggested[cursor]]); + autocomplete_close(ted); + } + } +} + +static void autocomplete_correct_scroll(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + i32 scroll = ac->scroll; + scroll = min_i32(scroll, (i32)arr_len(ac->suggested) - AUTOCOMPLETE_NCOMPLETIONS_VISIBLE); + scroll = max_i32(scroll, 0); + ac->scroll = scroll; +} + +void autocomplete_scroll(Ted *ted, i32 by) { + Autocomplete *ac = &ted->autocomplete; + ac->scroll += by; + autocomplete_correct_scroll(ted); +} + +static void autocomplete_move_cursor(Ted *ted, i32 by) { + Autocomplete *ac = &ted->autocomplete; + u32 ncompletions = arr_len(ac->suggested); + if (ncompletions == 0) + return; + i32 cursor = ac->cursor; + cursor += by; + cursor = (i32)mod_i32(cursor, (i32)ncompletions); + ac->cursor = cursor; + ac->scroll = ac->cursor - AUTOCOMPLETE_NCOMPLETIONS_VISIBLE / 2; + autocomplete_correct_scroll(ted); +} + +static void autocomplete_next(Ted *ted) { + autocomplete_move_cursor(ted, 1); +} + +static void autocomplete_prev(Ted *ted) { + autocomplete_move_cursor(ted, -1); +} + +void autocomplete_close(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + if (ac->open) { + ac->open = false; + ac->waiting_for_lsp = false; + autocomplete_clear_completions(ted); + } +} + +void autocomplete_update_suggested(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + arr_clear(ac->suggested); + char *word = str32_to_utf8_cstr( + buffer_word_at_cursor(ted->active_buffer) + ); + for (u32 i = 0; i < arr_len(ac->completions); ++i) { + Autocompletion *completion = &ac->completions[i]; + if (str_has_prefix(completion->filter, word)) + arr_add(ac->suggested, i); // suggest this one + } + free(word); +} + +static bool autocomplete_using_lsp(Ted *ted) { + return ted->active_buffer && buffer_lsp(ted->active_buffer) != NULL; +} + +static void autocomplete_no_suggestions(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + if (ac->trigger == TRIGGER_INVOKED) + ted->cursor_error_time = time_get_seconds(); + autocomplete_close(ted); +} + +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_position(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); + if (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 = trigger; + ac->last_pos = pos; + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + 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); + + char *word_at_cursor = str32_to_utf8_cstr(buffer_word_at_cursor(buffer)); + char **completions = calloc(TAGS_MAX_COMPLETIONS, sizeof *completions); + u32 ncompletions = (u32)tags_beginning_with(ted, word_at_cursor, completions, TAGS_MAX_COMPLETIONS); + free(word_at_cursor); + + arr_set_len(ac->completions, ncompletions); + + for (size_t i = 0; i < ncompletions; ++i) { + ac->completions[i].label = completions[i]; + ac->completions[i].text = str_dup(completions[i]); + ac->completions[i].filter = str_dup(completions[i]); + 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); +} + +static SymbolKind lsp_completion_kind_to_ted(LSPCompletionKind kind) { + switch (kind) { + case LSP_COMPLETION_TEXT: + case LSP_COMPLETION_MODULE: + case LSP_COMPLETION_UNIT: + case LSP_COMPLETION_COLOR: + case LSP_COMPLETION_FILE: + case LSP_COMPLETION_REFERENCE: + case LSP_COMPLETION_FOLDER: + case LSP_COMPLETION_OPERATOR: + return SYMBOL_OTHER; + + case LSP_COMPLETION_METHOD: + case LSP_COMPLETION_FUNCTION: + case LSP_COMPLETION_CONSTRUCTOR: + return SYMBOL_FUNCTION; + + case LSP_COMPLETION_FIELD: + case LSP_COMPLETION_PROPERTY: + return SYMBOL_FIELD; + + case LSP_COMPLETION_VARIABLE: + return SYMBOL_VARIABLE; + + case LSP_COMPLETION_CLASS: + case LSP_COMPLETION_INTERFACE: + case LSP_COMPLETION_ENUM: + case LSP_COMPLETION_STRUCT: + case LSP_COMPLETION_EVENT: + case LSP_COMPLETION_TYPEPARAMETER: + return SYMBOL_TYPE; + + case LSP_COMPLETION_VALUE: + case LSP_COMPLETION_ENUMMEMBER: + case LSP_COMPLETION_CONSTANT: + return SYMBOL_CONSTANT; + + case LSP_COMPLETION_KEYWORD: + case LSP_COMPLETION_SNIPPET: + return SYMBOL_KEYWORD; + } + assert(0); + return SYMBOL_OTHER; +} + + +static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response) { + const LSPRequest *request = &response->request; + if (request->type != LSP_REQUEST_COMPLETION) + return; + + Autocomplete *ac = &ted->autocomplete; + ac->waiting_for_lsp = false; + if (!ac->open) { + // user hit escape or down or something before completions arrived. + return; + } + + + const LSPResponseCompletion *completion = &response->data.completion; + size_t ncompletions = arr_len(completion->items); + arr_set_len(ac->completions, ncompletions); + for (size_t i = 0; i < ncompletions; ++i) { + const LSPCompletionItem *lsp_completion = &completion->items[i]; + Autocompletion *ted_completion = &ac->completions[i]; + ted_completion->label = str_dup(lsp_response_string(response, lsp_completion->label)); + ted_completion->filter = str_dup(lsp_response_string(response, lsp_completion->filter_text)); + // NOTE: here we don't deal with snippets. + // right now we are sending "snippetSupport: false" in the capabilities, + // so this should be okay. + ted_completion->text = str_dup(lsp_response_string(response, lsp_completion->text_edit.new_text)); + const char *detail = lsp_response_string(response, lsp_completion->detail); + ted_completion->detail = *detail ? str_dup(detail) : NULL; + ted_completion->kind = lsp_completion_kind_to_ted(lsp_completion->kind); + ted_completion->deprecated = lsp_completion->deprecated; + const char *documentation = lsp_response_string(response, lsp_completion->documentation); + ted_completion->documentation = *documentation ? str_dup(documentation) : NULL; + + } + ac->is_list_complete = completion->is_complete; + + autocomplete_update_suggested(ted); + switch (arr_len(ac->suggested)) { + case 0: + autocomplete_no_suggestions(ted); + return; + case 1: + // 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 +// 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; + TextBuffer *buffer = ted->active_buffer; + if (!buffer->filename) return; + if (buffer->view_only) return; + + ted->cursor_error_time = 0; + ac->last_pos = (BufferPos){0,0}; + ac->cursor = 0; + autocomplete_find_completions(ted, trigger); + + switch (arr_len(ac->completions)) { + case 0: + if (autocomplete_using_lsp(ted)) { + ac->open = true; + } else { + autocomplete_no_suggestions(ted); + } + break; + case 1: + autocomplete_complete(ted, ac->completions[0]); + // (^ this calls autocomplete_close) + break; + default: + // open autocomplete menu + ac->open = true; + break; + } +} + +static char symbol_kind_icon(SymbolKind k) { + switch (k) { + case SYMBOL_FUNCTION: + return 'f'; + case SYMBOL_FIELD: + return 'm'; + case SYMBOL_TYPE: + return 't'; + case SYMBOL_CONSTANT: + return 'c'; + case SYMBOL_VARIABLE: + return 'v'; + case SYMBOL_KEYWORD: + case SYMBOL_OTHER: + return ' '; + } + assert(0); + return ' '; +} + +static void autocomplete_frame(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + if (!ac->open) return; + + TextBuffer *buffer = ted->active_buffer; + Font *font = ted->font; + float char_height = text_font_char_height(font); + Settings const *settings = buffer_settings(buffer); + u32 const *colors = settings->colors; + float const padding = settings->padding; + + 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->lsp_request_time) < 0.2) { + // don't show "Loading..." unless we've actually been loading for a bit of time + return; + } + } + + if (!ac->waiting_for_lsp && ncompletions == 0) { + // no completions. close menu. + autocomplete_close(ted); + return; + } + + ac->cursor = ncompletions ? (i32)mod_i64(ac->cursor, (i64)ncompletions) : 0; + + autocomplete_correct_scroll(ted); + i32 scroll = ac->scroll; + u32 ncompletions_visible = min_u32((u32)ncompletions, AUTOCOMPLETE_NCOMPLETIONS_VISIBLE); + + float menu_width = 400, menu_height = (float)ncompletions_visible * char_height; + + if (ac->waiting_for_lsp && ncompletions == 0) { + menu_height = 200.f; + } + + v2 cursor_pos = buffer_pos_to_pixels(buffer, buffer->cursor_pos); + bool open_up = cursor_pos.y > 0.5f * (buffer->y1 + buffer->y2); // should the completion menu open upwards? + bool open_left = cursor_pos.x > 0.5f * (buffer->x1 + buffer->x2); + float x = cursor_pos.x, start_y = cursor_pos.y; + if (open_left) x -= menu_width; + if (open_up) + start_y -= menu_height; + else + start_y += char_height; // put menu below cursor + { + Rect menu_rect = rect(V2(x, start_y), V2(menu_width, menu_height)); + gl_geometry_rect(menu_rect, colors[COLOR_AUTOCOMPLETE_BG]); + gl_geometry_rect_border(menu_rect, 1, colors[COLOR_AUTOCOMPLETE_BORDER]); + ac->rect = menu_rect; + } + + i32 mouse_entry = scroll + (i32)((ted->mouse_pos.y - start_y) / char_height); + + Autocompletion *document = NULL; + if (ncompletions) { + assert(ac->cursor >= 0 && ac->cursor < (i32)ncompletions); + // highlight cursor entry + Rect r = rect(V2(x, start_y + (float)(ac->cursor - scroll) * char_height), V2(menu_width, char_height)); + if (rect_contains_point(ac->rect, rect_center(r))) { + gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_HL]); + document = &ac->completions[ac->suggested[ac->cursor]]; + } + } + if (mouse_entry >= 0 && mouse_entry < (i32)ncompletions + && rect_contains_point(ac->rect, ted->mouse_pos)) { + // highlight moused over entry + Rect r = rect(V2(x, start_y + (float)(mouse_entry - scroll) * char_height), V2(menu_width, char_height)); + gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_HL]); + ted->cursor = ted->cursor_hand; + document = &ac->completions[ac->suggested[mouse_entry]]; + } + + float border_thickness = settings->border_thickness; + + + if (document && document->documentation) { + // document that entry!! + + // we've got some wacky calculations to figure out the + // bounding rect for the documentation + float doc_width = open_left ? ac->rect.pos.x - 2*padding + : buffer->x2 - (ac->rect.pos.x + ac->rect.size.x + 2*padding); + if (doc_width > 800) doc_width = 800; + float doc_height = buffer->y2 - (ac->rect.pos.y + 2*padding); + if (doc_height > char_height * 20) doc_height = char_height * 20; + + // if the rect is too small, there's no point in showing it + if (doc_width >= 200) { + float doc_x = open_left ? ac->rect.pos.x - doc_width - padding + : ac->rect.pos.x + ac->rect.size.x + padding; + float doc_y = ac->rect.pos.y; + Rect r = rect(V2(doc_x, doc_y), V2(doc_width, doc_height)); + gl_geometry_rect(r, colors[COLOR_AUTOCOMPLETE_BG]); + gl_geometry_rect_border(r, border_thickness, colors[COLOR_AUTOCOMPLETE_BORDER]); + + // draw the text! + TextRenderState text_state = text_render_state_default; + text_state.min_x = doc_x + padding; + text_state.max_x = doc_x + doc_width - padding; + text_state.max_y = doc_y + doc_height; + text_state.x = doc_x + padding; + text_state.y = doc_y + padding; + text_state.wrap = true; + rgba_u32_to_floats(colors[COLOR_TEXT], text_state.color); + text_utf8_with_state(font, &text_state, document->documentation); + } + } + + + for (uint i = 0; i < ted->nmouse_clicks[SDL_BUTTON_LEFT]; ++i) { + v2 click = ted->mouse_clicks[SDL_BUTTON_LEFT][i]; + if (rect_contains_point(ac->rect, click)) { + i32 entry = scroll + (i32)((click.y - start_y) / char_height); + if (entry >= 0 && entry < (i32)ncompletions) { + // entry was clicked on! use this completion. + autocomplete_complete(ted, ac->completions[ac->suggested[entry]]); + return; + } + } + } + + float y = start_y; + TextRenderState state = text_render_state_default; + state.min_x = x + padding; state.min_y = y; state.max_x = x + menu_width - padding; state.max_y = y + menu_height; + rgba_u32_to_floats(colors[COLOR_TEXT], state.color); + + if (ac->waiting_for_lsp && ncompletions == 0) { + state.x = x + padding; state.y = y; + text_utf8_with_state(font, &state, "Loading..."); + } else { + for (size_t i = 0; i < ncompletions_visible; ++i) { + const Autocompletion *completion = &ac->completions[ac->suggested[(i32)i + scroll]]; + + state.x = x; state.y = y; + if (i != ncompletions_visible-1) { + gl_geometry_rect(rect(V2(x, y + char_height), + V2(menu_width, border_thickness)), + colors[COLOR_AUTOCOMPLETE_BORDER]); + } + + ColorSetting label_color = color_for_symbol_kind(completion->kind); + if (!settings->syntax_highlighting) + label_color = COLOR_TEXT; + + rgba_u32_to_floats(colors[label_color], state.color); + + // draw icon + char icon_text[2] = {symbol_kind_icon(completion->kind), 0}; + state.x += padding; + text_utf8_with_state(font, &state, icon_text); + state.x += padding; + gl_geometry_rect(rect(V2((float)state.x, (float)state.y), V2(border_thickness, char_height)), + colors[COLOR_AUTOCOMPLETE_BORDER]); + state.x += padding; + + float label_x = (float)state.x; + text_utf8_with_state(font, &state, completion->label); + + const char *detail = completion->detail; + if (detail) { + double label_end_x = state.x; + + char show_text[128] = {0}; + + int amount_detail = 0; + for (; ; ++amount_detail) { + if (unicode_is_continuation_byte((u8)detail[amount_detail])) + continue; // don't cut off text in the middle of a code point. + + char text[128] = {0}; + strbuf_printf(text, "%.*s%s", amount_detail, detail, + (size_t)amount_detail == strlen(detail) ? "" : "..."); + double width = text_get_size_v2(font, text).x; + if (label_end_x + width + 2 * padding < state.max_x) { + strbuf_cpy(show_text, text); + } + // don't break if not, since we want to use "blabla" + // even if "blabl..." is too long + + if (!detail[amount_detail]) break; + } + if (amount_detail >= 3) { + //rgba_u32_to_floats(colors[COLOR_COMMENT], state.color); + text_utf8_anchored(font, show_text, state.max_x, state.y, + colors[COLOR_COMMENT], ANCHOR_TOP_RIGHT); + } + } + + if (completion->deprecated) { + gl_geometry_rect(rect(V2(label_x, y + (char_height - border_thickness) * 0.5f), + V2((float)state.x - label_x, 1)), + colors[label_color]); + } + + y += char_height; + } + } + + gl_geometry_draw(); + text_render(font); +} diff --git a/ide-definitions.c b/ide-definitions.c new file mode 100644 index 0000000..3a36a96 --- /dev/null +++ b/ide-definitions.c @@ -0,0 +1,41 @@ +void definition_goto(Ted *ted, LSP *lsp, const char *name, LSPDocumentPosition position) { + if (lsp) { + // send that request + LSPRequest request = {.type = LSP_REQUEST_DEFINITION}; + request.data.definition.position = position; + lsp_send_request(lsp, &request); + } else { + // just go to the tag + tag_goto(ted, name); + } +} + +void definitions_process_lsp_response(Ted *ted, LSP *lsp, const LSPResponse *response) { + if (response->request.type != LSP_REQUEST_DEFINITION) + return; + + const LSPResponseDefinition *response_def = &response->data.definition; + Definitions *defs = &ted->definitions; + + if (defs->last_response_lsp == lsp->id + && response->request.id < defs->last_response_id) { + // we just processed a later response, so let's ignore this + return; + } + defs->last_response_lsp = lsp->id; + defs->last_response_id = response->request.id; + + if (!arr_len(response_def->locations)) { + // no definition. do the error cursor. + ted_flash_error_cursor(ted); + return; + } + LSPLocation location = response_def->locations[0]; + const char *path = lsp_document_path(lsp, location.document); + if (!ted_open_file(ted, path)) { + ted_flash_error_cursor(ted); + return; + } + LSPDocumentPosition position = lsp_location_start_position(location); + ted_go_to_lsp_document_position(ted, lsp, position); +} diff --git a/ide-hover.c b/ide-hover.c new file mode 100644 index 0000000..9742e58 --- /dev/null +++ b/ide-hover.c @@ -0,0 +1,195 @@ +// LSP hover information (textDocument/hover request) + +void hover_close(Ted *ted) { + Hover *hover = &ted->hover; + hover->open = false; + free(hover->text); + hover->text = NULL; +} + +static bool get_hover_position(Ted *ted, LSPDocumentPosition *pos, TextBuffer **pbuffer, LSP **lsp) { + // find the buffer where the mouse is + for (int i = 0; i < TED_MAX_BUFFERS; ++i) { + TextBuffer *buffer = &ted->buffers[i]; + if (!buffer->filename) continue; + LSP *l = buffer_lsp(buffer); + if (!l) continue; + BufferPos mouse_pos = {0}; + if (buffer_pixels_to_pos(buffer, ted->mouse_pos, &mouse_pos)) { + if (pos) *pos = buffer_pos_to_lsp_document_position(buffer, mouse_pos); + if (pbuffer) *pbuffer = buffer; + if (lsp) *lsp = l; + return true; + } + } + return false; +} + +void hover_send_request(Ted *ted) { + Hover *hover = &ted->hover; + LSPRequest request = {.type = LSP_REQUEST_HOVER}; + LSPRequestHover *h = &request.data.hover; + LSP *lsp = NULL; + if (get_hover_position(ted, &h->position, NULL, &lsp)) { + hover->requested_position = h->position; + hover->requested_lsp = lsp->id; + lsp_send_request(lsp, &request); + } +} + +void hover_process_lsp_response(Ted *ted, LSPResponse *response) { + if (!response) return; + if (response->request.type != LSP_REQUEST_HOVER) return; + + Hover *hover = &ted->hover; + LSPResponseHover *hover_response = &response->data.hover; + + TextBuffer *buffer=0; + LSPDocumentPosition pos={0}; + LSP *lsp=0; + get_hover_position(ted, &pos, &buffer, &lsp); + + if (hover->text // we already have hover text + && ( + lsp->id != hover->requested_lsp // this request is from a different LSP + || !lsp_document_position_eq(response->request.data.hover.position, pos) // this request is for a different position + )) { + // this is a stale request. ignore it + return; + } + + free(hover->text); hover->text = NULL; + + if (buffer) { + hover->range_start = buffer_pos_from_lsp(buffer, hover_response->range.start); + hover->range_end = buffer_pos_from_lsp(buffer, hover_response->range.end); + } + const char *contents = lsp_response_string(response, hover_response->contents); + if (*contents) { + hover->text = str_dup(contents); + char *p = hover->text + strlen(hover->text) - 1; + // remove trailing whitespace + // (rust-analyzer gives us trailing newlines for local variables) + for (; p > hover->text && isspace(*p); --p) + *p = '\0'; + } +} + +void hover_frame(Ted *ted, double dt) { + const Settings *settings = ted_active_settings(ted); + if (!settings->hover_enabled) + return; + Hover *hover = &ted->hover; + + bool shift_down = SDL_GetKeyboardState(NULL)[SDL_SCANCODE_LSHIFT] + || SDL_GetKeyboardState(NULL)[SDL_SCANCODE_RSHIFT]; + + if (!shift_down) { + hover_close(ted); + } + + (void)dt; + if (!hover->open) { + if (shift_down) { + hover_send_request(ted); + hover->open = true; + } + return; + } + + TextBuffer *buffer=0; + { + LSPDocumentPosition pos={0}; + LSP *lsp=0; + if (get_hover_position(ted, &pos, &buffer, &lsp)) { + if (lsp->id != hover->requested_lsp + || !lsp_document_position_eq(pos, hover->requested_position)) { + // refresh hover + hover_send_request(ted); + } + } else { + hover_close(ted); + return; + } + } + + + + const float padding = settings->padding; + const float border = settings->border_thickness; + const u32 *colors = settings->colors; + const char *text = hover->text; + Font *font = ted->font; + float x = ted->mouse_pos.x, y = ted->mouse_pos.y + font->char_height; + float char_height = font->char_height; + + BufferPos range_start = hover->range_start, range_end = hover->range_end; + if (!buffer_pos_eq(range_start, range_end)) { + // draw the highlight + if (range_start.line == range_end.line) { + v2 a = buffer_pos_to_pixels(buffer, range_start); + v2 b = buffer_pos_to_pixels(buffer, range_end); + b.y += char_height; + gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); + } else if (range_end.line - range_start.line < 1000) { // prevent gigantic highlights from slowing things down + // multiple lines. + v2 a = buffer_pos_to_pixels(buffer, range_start); + v2 b = buffer_pos_to_pixels(buffer, buffer_pos_end_of_line(buffer, range_start.line)); + b.y += char_height; + gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); + + for (u32 line = range_start.line + 1; line < range_end.line; ++line) { + // these lines are fully contained in the range. + BufferPos start = buffer_pos_start_of_line(buffer, line); + BufferPos end = buffer_pos_end_of_line(buffer, line); + a = buffer_pos_to_pixels(buffer, start); + b = buffer_pos_to_pixels(buffer, end); + b.y += char_height; + gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); + } + + // last line + a = buffer_pos_to_pixels(buffer, buffer_pos_start_of_line(buffer, range_end.line)); + b = buffer_pos_to_pixels(buffer, range_end); + b.y += char_height; + gl_geometry_rect(rect_endpoints(a, b), colors[COLOR_HOVER_HL]); + } + + } + if (hover->text) { + float max_width = 400; + TextRenderState state = text_render_state_default; + state.x = state.min_x = x; + state.y = state.min_y = y; + state.render = false; + state.wrap = true; + state.max_x = x + max_width; + state.max_y = ted->window_height; + text_utf8_with_state(font, &state, text); + float width = (float)(state.x_largest - x); + float height = (float)(state.y_largest - y) + char_height; + if (height > 300) { + height = 300; + } + + if (x + width > ted->window_width) + x -= width; // open left + if (y + height > ted->window_height) + y -= height + char_height * 2; // open up + state.x = state.min_x = x; + state.y = state.min_y = y; + state.max_x = x + max_width; + state.y = y; + state.render = true; + state.max_y = y + height; + + Rect rect = rect_xywh(x - padding, y - padding, width + 2*padding, height + 2*padding); + gl_geometry_rect(rect, colors[COLOR_HOVER_BG]); + gl_geometry_rect_border(rect, border, colors[COLOR_HOVER_BORDER]); + rgba_u32_to_floats(colors[COLOR_HOVER_TEXT], state.color); + text_utf8_with_state(font, &state, text); + } + + gl_geometry_draw(); + text_render(font); +} diff --git a/ide-signature-help.c b/ide-signature-help.c new file mode 100644 index 0000000..97dc8c3 --- /dev/null +++ b/ide-signature-help.c @@ -0,0 +1,153 @@ +// deals with textDocument/signatureHelp LSP requests + +static void signature_help_clear(SignatureHelp *help) { + for (int i = 0; i < help->signature_count; ++i) { + Signature sig = help->signatures[i]; + free(sig.label_pre); + free(sig.label_active); + free(sig.label_post); + } + memset(help->signatures, 0, sizeof help->signatures); + help->signature_count = 0; +} + +void signature_help_send_request(Ted *ted) { + SignatureHelp *help = &ted->signature_help; + Settings *settings = ted_active_settings(ted); + if (!settings->signature_help_enabled) { + signature_help_clear(help); + return; + } + TextBuffer *buffer = ted->active_buffer; + if (!buffer) { + signature_help_clear(help); + return; + } + LSP *lsp = buffer_lsp(buffer); + if (!lsp) { + signature_help_clear(help); + return; + } + LSPRequest request = {.type = LSP_REQUEST_SIGNATURE_HELP}; + LSPRequestSignatureHelp *s = &request.data.signature_help; + s->position = buffer_cursor_pos_as_lsp_document_position(buffer); + lsp_send_request(lsp, &request); + help->retrigger = false; +} + +void signature_help_retrigger(Ted *ted) { + // don't just send the request here -- we don't want to send more than + // one request per frame. + ted->signature_help.retrigger = true; +} + +void signature_help_open(Ted *ted, char32_t trigger) { + (void)trigger; // for now we don't send context + signature_help_send_request(ted); +} + +bool signature_help_is_open(Ted *ted) { + return ted->signature_help.signature_count > 0; +} + + +void signature_help_close(Ted *ted) { + signature_help_clear(&ted->signature_help); +} + +void signature_help_process_lsp_response(Ted *ted, const LSPResponse *response) { + Settings *settings = ted_active_settings(ted); + if (!settings->signature_help_enabled) return; + + if (response->request.type != LSP_REQUEST_SIGNATURE_HELP) + return; + SignatureHelp *help = &ted->signature_help; + const LSPResponseSignatureHelp *lsp_help = &response->data.signature_help; + u32 signature_count = arr_len(lsp_help->signatures); + if (signature_count > SIGNATURE_HELP_MAX) + signature_count = SIGNATURE_HELP_MAX; + + signature_help_clear(help); + for (u32 s = 0; s < signature_count; ++s) { + Signature *signature = &help->signatures[s]; + LSPSignatureInformation *lsp_signature = &lsp_help->signatures[s]; + + const char *label = lsp_response_string(response, lsp_signature->label); + size_t start = unicode_utf16_to_utf8_offset(label, lsp_signature->active_start); + size_t end = unicode_utf16_to_utf8_offset(label, lsp_signature->active_end); + if (start == (size_t)-1) { + assert(0); + start = 0; + } + if (end == (size_t)-1) { + assert(0); + end = 0; + } + u32 active_start = (u32)start; + u32 active_end = (u32)end; + signature->label_pre = strn_dup(label, active_start); + signature->label_active = strn_dup(label + active_start, active_end - active_start); + signature->label_post = str_dup(label + active_end); + } + + help->signature_count = (u16)signature_count; +} + +void signature_help_frame(Ted *ted) { + Settings *settings = ted_active_settings(ted); + if (!settings->signature_help_enabled) + return; + + SignatureHelp *help = &ted->signature_help; + if (help->retrigger) + signature_help_send_request(ted); + u16 signature_count = help->signature_count; + if (!signature_count) + return; + Font *font = ted->font; + Font *font_bold = ted->font_bold; + TextBuffer *buffer = ted->active_buffer; + if (!buffer) + return; + + u32 *colors = settings->colors; + float border = settings->border_thickness; + + float width = buffer->x2 - buffer->x1; + float height = FLT_MAX; + // make sure signature help doesn't take up too much space + while (1) { + height = font->char_height * signature_count; + if (height < (buffer->y2 - buffer->y1) * 0.25f) + break; + --signature_count; + if (signature_count == 0) return; + } + float x = buffer->x1, y = buffer->y2 - height; + gl_geometry_rect(rect_xywh(x, y - border, width, border), + colors[COLOR_AUTOCOMPLETE_BORDER]); + gl_geometry_rect(rect_xywh(x, y, width, height), + colors[COLOR_AUTOCOMPLETE_BG]); + + // draw the signatures + for (int s = 0; s < signature_count; ++s) { + const Signature *signature = &help->signatures[s]; + TextRenderState state = text_render_state_default; + state.x = x; + state.y = y; + state.min_x = x; + state.min_y = y; + state.max_x = buffer->x2; + state.max_y = buffer->y2; + rgba_u32_to_floats(colors[COLOR_TEXT], state.color); + + text_utf8_with_state(font, &state, signature->label_pre); + text_utf8_with_state(font_bold, &state, signature->label_active); + text_utf8_with_state(font, &state, signature->label_post); + y += font->char_height; + } + + gl_geometry_draw(); + text_render(font); + text_render(font_bold); +} diff --git a/json.c b/json.c deleted file mode 100644 index 242eb1f..0000000 --- a/json.c +++ /dev/null @@ -1,764 +0,0 @@ -// JSON parser for LSP -// provides FAST(ish) parsing but SLOW lookup -// this is especially fast for small objects -// this actually supports "extended json", where objects can have arbitrary values as keys. -typedef struct { - u32 pos; - u32 len; -} JSONString; - -typedef struct JSONValue JSONValue; - -typedef struct { - u32 len; - // this is an index into the values array - // values[items..items+len] store the names - // values[items+len..items+2*len] store the values - u32 items; -} JSONObject; - -typedef struct { - u32 len; - // this is an index into the values array - // values[elements..elements+len] are the elements - u32 elements; -} JSONArray; - -typedef enum { - // note: json doesn't actually include undefined. - // this is only for returning things from json_get etc. - JSON_UNDEFINED, - JSON_NULL, - JSON_FALSE, - JSON_TRUE, - JSON_NUMBER, - JSON_STRING, - JSON_OBJECT, - JSON_ARRAY -} JSONValueType; - -struct JSONValue { - JSONValueType type; - union { - double number; - JSONString string; - JSONArray array; - JSONObject object; - } val; -}; - - -typedef struct { - char error[64]; - bool is_text_copied; // if this is true, then json_free will call free on text - const char *text; - // root = values[0] - JSONValue *values; -} JSON; - - -#define SKIP_WHITESPACE while (json_is_space(text[index])) ++index; - -const char *json_type_to_str(JSONValueType type) { - switch (type) { - case JSON_UNDEFINED: - return "undefined"; - case JSON_NULL: - return "null"; - case JSON_STRING: - return "string"; - case JSON_NUMBER: - return "number"; - case JSON_FALSE: - return "false"; - case JSON_TRUE: - return "true"; - case JSON_ARRAY: - return "array"; - case JSON_OBJECT: - return "object"; - } - assert(0); - return "???"; -} - -static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val); -void json_debug_print_value(const JSON *json, JSONValue value); - -// defining this instead of using isspace seems to be faster -// probably because isspace depends on the locale. -static inline bool json_is_space(char c) { - return c == ' ' || c == '\n' || c == '\r' || c == '\t'; -} - -void json_debug_print_array(const JSON *json, JSONArray array) { - printf("["); - for (u32 i = 0; i < array.len; ++i) { - json_debug_print_value(json, json->values[array.elements + i]); - printf(", "); - } - printf("]"); -} - -void json_debug_print_object(const JSON *json, JSONObject obj) { - printf("{"); - for (u32 i = 0; i < obj.len; ++i) { - json_debug_print_value(json, json->values[obj.items + i]); - printf(": "); - json_debug_print_value(json, json->values[obj.items + obj.len + i]); - printf(", "); - } - printf("}"); -} - -void json_debug_print_string(const JSON *json, JSONString string) { - printf("\"%.*s\"", - (int)string.len, - json->text + string.pos); -} - -void json_debug_print_value(const JSON *json, JSONValue value) { - switch (value.type) { - case JSON_UNDEFINED: printf("undefined"); break; - case JSON_NULL: printf("null"); break; - case JSON_FALSE: printf("false"); break; - case JSON_TRUE: printf("true"); break; - case JSON_NUMBER: printf("%g", value.val.number); break; - case JSON_STRING: { - json_debug_print_string(json, value.val.string); - } break; - case JSON_ARRAY: { - json_debug_print_array(json, value.val.array); - } break; - case JSON_OBJECT: { - json_debug_print_object(json, value.val.object); - } break; - } -} - -// count number of comma-separated values until -// closing ] or } -static u32 json_count(JSON *json, u32 index) { - int bracket_depth = 0; - int brace_depth = 0; - u32 count = 1; - const char *text = json->text; - SKIP_WHITESPACE; - // special case: empty object/array - if (text[index] == '}' || text[index] == ']') - return 0; - -// int mark[5000] = {0}; - for (; ; ++index) { - switch (text[index]) { - case '\0': - return 0; // bad no closing bracket - case '[': - ++bracket_depth; - break; - case ']': - --bracket_depth; - if (bracket_depth < 0) - return count; - break; - case '{': - ++brace_depth; -// mark[index] = 1; - break; - case '}': - --brace_depth; -// mark[index] = 1; - if (brace_depth < 0){ - // useful visualization for debugging -// for (int i = 0; text[i]; ++i ){ -// switch (mark[i]){ -// case 1: printf("\x1b[91m"); break; -// case 2: printf("\x1b[92m"); break; -// } -// printf("%c",text[i]); -// if (mark[i]) printf("\x1b[0m"); -// } -// printf("\n"); - - return count; - } - break; - case ',': - if (bracket_depth == 0 && brace_depth == 0) - ++count; - break; - case '"': { - ++index; // skip opening " - int escaped = 0; - for (; ; ++index) { -// mark[index] = 2; - switch (text[index]) { - case '\0': return 0; // bad no closing quote - case '\\': escaped = !escaped; break; - case '"': - if (!escaped) - goto done; - escaped = false; - break; - default: - escaped = false; - break; - } - } - done:; - } break; - } - } -} - -static bool json_parse_object(JSON *json, u32 *p_index, JSONObject *object) { - u32 index = *p_index; - const char *text = json->text; - ++index; // go past { - u32 count = json_count(json, index); - object->len = count; - object->items = arr_len(json->values); - arr_set_len(json->values, arr_len(json->values) + 2 * count); - - for (u32 i = 0; i < count; ++i) { - if (i > 0) { - if (text[index] != ',') { - strbuf_printf(json->error, "stuff after value in object"); - return false; - } - ++index; - } - SKIP_WHITESPACE; - JSONValue name = {0}, value = {0}; - if (!json_parse_value(json, &index, &name)) - return false; - SKIP_WHITESPACE; - if (text[index] != ':') { - strbuf_printf(json->error, "stuff after name in object"); - return false; - } - ++index; // skip : - SKIP_WHITESPACE; - if (!json_parse_value(json, &index, &value)) - return false; - SKIP_WHITESPACE; - json->values[object->items + i] = name; - json->values[object->items + count + i] = value; - } - - if (text[index] != '}') { - strbuf_printf(json->error, "mismatched brackets or quotes."); - return false; - } - ++index; // skip } - *p_index = index; - return true; -} - -static bool json_parse_array(JSON *json, u32 *p_index, JSONArray *array) { - u32 index = *p_index; - const char *text = json->text; - ++index; // go past [ - u32 count = json_count(json, index); - array->len = count; - array->elements = arr_len(json->values); - - arr_set_len(json->values, arr_len(json->values) + count); - - SKIP_WHITESPACE; - - for (u32 i = 0; i < count; ++i) { - if (i > 0) { - if (text[index] != ',') { - strbuf_printf(json->error, "stuff after element in array"); - return false; - } - ++index; - } - SKIP_WHITESPACE; - JSONValue element = {0}; - if (!json_parse_value(json, &index, &element)) - return false; - SKIP_WHITESPACE; - json->values[array->elements + i] = element; - } - - if (text[index] != ']') { - strbuf_printf(json->error, "mismatched brackets or quotes."); - return false; - } - ++index; // skip ] - *p_index = index; - return true; -} - -static bool json_parse_string(JSON *json, u32 *p_index, JSONString *string) { - u32 index = *p_index; - ++index; // skip opening " - string->pos = index; - const char *text = json->text; - bool escaped = false; - for (; ; ++index) { - switch (text[index]) { - case '"': - if (!escaped) - goto done; - escaped = false; - break; - case '\\': - escaped = !escaped; - break; - case '\0': - strbuf_printf(json->error, "string literal goes to end of JSON"); - return false; - default: - escaped = false; - break; - } - } - done: - string->len = index - string->pos; - ++index; // skip closing " - *p_index = index; - return true; -} - -static bool json_parse_number(JSON *json, u32 *p_index, double *number) { - char *endp = NULL; - const char *text = json->text; - u32 index = *p_index; - *number = strtod(text + index, &endp); - if (endp == text + index) { - strbuf_printf(json->error, "bad number"); - return false; - } - index = (u32)(endp - text); - *p_index = index; - return true; -} - -static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val) { - const char *text = json->text; - u32 index = *p_index; - SKIP_WHITESPACE; - switch (text[index]) { - case '{': - val->type = JSON_OBJECT; - if (!json_parse_object(json, &index, &val->val.object)) - return false; - break; - case '[': - val->type = JSON_ARRAY; - if (!json_parse_array(json, &index, &val->val.array)) - return false; - break; - case '"': - val->type = JSON_STRING; - if (!json_parse_string(json, &index, &val->val.string)) - return false; - break; - case ANY_DIGIT: - case '-': - case '+': - val->type = JSON_NUMBER; - if (!json_parse_number(json, &index, &val->val.number)) - return false; - break; - case 'f': - val->type = JSON_FALSE; - if (!str_has_prefix(&text[index], "false")) - return false; - index += 5; - break; - case 't': - val->type = JSON_TRUE; - if (!str_has_prefix(&text[index], "true")) - return false; - index += 4; - break; - case 'n': - val->type = JSON_NULL; - if (!str_has_prefix(&text[index], "null")) - return false; - index += 4; - break; - default: - strbuf_printf(json->error, "bad value"); - return false; - } - *p_index = index; - return true; -} - -void json_free(JSON *json) { - arr_free(json->values); - // important we don't zero json here because we want to preserve json->error. - if (json->is_text_copied) { - free((void*)json->text); - } - json->text = NULL; -} - -// NOTE: text must live as long as json!!! -bool json_parse(JSON *json, const char *text) { - memset(json, 0, sizeof *json); - json->text = text; - arr_reserve(json->values, strlen(text) / 8); - arr_addp(json->values); // add root - JSONValue val = {0}; - u32 index = 0; - if (!json_parse_value(json, &index, &val)) { - json_free(json); - return false; - } - SKIP_WHITESPACE; - if (text[index]) { - json_free(json); - strbuf_printf(json->error, "extra text after end of root object"); - return false; - } - json->values[0] = val; - return true; -} - -// like json_parse, but a copy of text is made, so you can free/overwrite it immediately. -bool json_parse_copy(JSON *json, const char *text) { - bool success = json_parse(json, str_dup(text)); - if (success) { - json->is_text_copied = true; - return true; - } else { - free((void*)json->text); - json->text = NULL; - return false; - } -} - -static bool json_streq(const JSON *json, const JSONString *string, const char *name) { - const char *p = &json->text[string->pos]; - const char *end = p + string->len; - for (; p < end; ++p, ++name) { - if (*name != *p) - return false; - } - return *name == '\0'; -} - -// returns undefined if the property `name` does not exist. -JSONValue json_object_get(const JSON *json, JSONObject object, const char *name) { - const JSONValue *items = &json->values[object.items]; - for (u32 i = 0; i < object.len; ++i) { - const JSONValue *this_name = items++; - if (this_name->type == JSON_STRING && json_streq(json, &this_name->val.string, name)) { - return json->values[object.items + object.len + i]; - } - } - return (JSONValue){0}; -} - -JSONValue json_array_get(const JSON *json, JSONArray array, u64 i) { - if (i < array.len) { - return json->values[array.elements + i]; - } - return (JSONValue){0}; -} - -// returns NaN if `x` is not a number (ha ha). -double json_force_number(JSONValue x) { - if (x.type == JSON_NUMBER) { - return x.val.number; - } else { - return NAN; - } -} - -double json_object_get_number(const JSON *json, JSONObject object, const char *name) { - return json_force_number(json_object_get(json, object, name)); -} - -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) { - return x.val.string; - } else { - return (JSONString){0}; - } -} - -JSONString json_object_get_string(const JSON *json, JSONObject object, const char *name) { - return json_force_string(json_object_get(json, object, name)); -} - -JSONString json_array_get_string(const JSON *json, JSONArray array, size_t i) { - return json_force_string(json_array_get(json, array, i)); -} - -// returns (JSONObject){0} (which is interpreted as an empty object) if `x` is not an object -JSONObject json_force_object(JSONValue x) { - if (x.type == JSON_OBJECT) { - return x.val.object; - } else { - return (JSONObject){0}; - } -} - -JSONObject json_object_get_object(const JSON *json, JSONObject object, const char *name) { - return json_force_object(json_object_get(json, object, name)); -} - -JSONObject json_array_get_object(const JSON *json, JSONArray array, size_t i) { - return json_force_object(json_array_get(json, array, i)); -} - -// returns (JSONArray){0} (which is interpreted as an empty array) if `x` is not an array -JSONArray json_force_array(JSONValue x) { - if (x.type == JSON_ARRAY) { - return x.val.array; - } else { - return (JSONArray){0}; - } -} - -JSONArray json_object_get_array(const JSON *json, JSONObject object, const char *name) { - return json_force_array(json_object_get(json, object, name)); -} - -JSONArray json_array_get_array(const JSON *json, JSONArray array, size_t i) { - return json_force_array(json_array_get(json, array, i)); -} - -// e.g. if json is { "a" : { "b": 3 }}, then json_get(json, "a.b") = 3. -// returns undefined if there is no such property -JSONValue json_get(const JSON *json, const char *path) { - char segment[128]; - const char *p = path; - if (!json->values) { - return (JSONValue){0}; - } - JSONValue curr_value = json->values[0]; - while (*p) { - size_t segment_len = strcspn(p, "."); - strn_cpy(segment, sizeof segment, p, segment_len); - if (curr_value.type != JSON_OBJECT) { - return (JSONValue){0}; - } - curr_value = json_object_get(json, curr_value.val.object, segment); - p += segment_len; - if (*p == '.') ++p; - } - return curr_value; -} - -// equivalent to json_get(json, path).type != JSON_UNDEFINED, but more readable -bool json_has(const JSON *json, const char *path) { - JSONValue value = json_get(json, path); - return value.type != JSON_UNDEFINED; -} - -// turn a json string into a null terminated string. -// this won't be nice if the json string includes \u0000 but that's rare. -// if buf_sz > string->len, the string will fit. -void json_string_get(const JSON *json, JSONString string, char *buf, size_t buf_sz) { - const char *text = json->text; - if (buf_sz == 0) { - assert(0); - return; - } - char *buf_end = buf + buf_sz - 1; - for (u32 i = string.pos, end = string.pos + string.len; i < end && buf < buf_end; ++i) { - if (text[i] != '\\') { - *buf++ = text[i]; - } else { - ++i; - if (i >= end) break; - // escape sequence - switch (text[i]) { - case 'n': *buf++ = '\n'; break; - case 'r': *buf++ = '\r'; break; - case 'b': *buf++ = '\b'; break; - case 't': *buf++ = '\t'; break; - case 'f': *buf++ = '\f'; break; - case '\\': *buf++ = '\\'; break; - case '/': *buf++ = '/'; break; - case '"': *buf++ = '"'; break; - case 'u': { - if ((buf_end - buf) < 4 || i + 5 > end) - goto brk; - ++i; - - char hex[5] = {0}; - hex[0] = text[i++]; - hex[1] = text[i++]; - hex[2] = text[i++]; - hex[3] = text[i++]; - unsigned code_point=0; - sscanf(hex, "%04x", &code_point); - // technically this won't deal with people writing out UTF-16 surrogate halves - // using \u. i dont care. - size_t n = unicode_utf32_to_utf8(buf, code_point); - if (n <= 4) buf += n; - } break; - } - } - } - brk: - *buf = '\0'; -} - -// returns a malloc'd null-terminated string. -static char *json_string_get_alloc(const JSON *json, JSONString string) { - u32 n = string.len + 1; - if (n == 0) --n; // extreme edge case - char *buf = calloc(1, n); - json_string_get(json, string, buf, n); - return buf; -} - - -#if __unix__ -static void json_test_time_large(const char *filename) { - struct timespec start={0},end={0}; - FILE *fp = fopen(filename,"rb"); - if (!fp) { - perror(filename); - return; - } - - fseek(fp,0,SEEK_END); - size_t sz = (size_t)ftell(fp); - char *buf = calloc(1,sz+1); - rewind(fp); - fread(buf, 1, sz, fp); - fclose(fp); - for (int trial = 0; trial < 5; ++trial) { - clock_gettime(CLOCK_MONOTONIC, &start); - JSON json={0}; - bool success = json_parse(&json, buf); - if (!success) { - printf("FAIL: %s\n",json.error); - return; - } - - json_free(&json); - clock_gettime(CLOCK_MONOTONIC, &end); - - - - printf("time: %.1fms\n", - ((double)end.tv_sec*1e3+(double)end.tv_nsec*1e-6) - -((double)start.tv_sec*1e3+(double)start.tv_nsec*1e-6)); - } - -} -static void json_test_time_small(void) { - struct timespec start={0},end={0}; - int trials = 50000000; - clock_gettime(CLOCK_MONOTONIC, &start); - for (int trial = 0; trial < trials; ++trial) { - JSON json={0}; - bool success = json_parse(&json, "{\"hello\":\"there\"}"); - if (!success) { - printf("FAIL: %s\n",json.error); - return; - } - - json_free(&json); - } - clock_gettime(CLOCK_MONOTONIC, &end); - printf("time per trial: %.1fns\n", - (((double)end.tv_sec*1e9+(double)end.tv_nsec) - -((double)start.tv_sec*1e9+(double)start.tv_nsec)) - / trials); - -} -#endif - -void json_debug_print(const JSON *json) { - printf("%u values (capacity %u, text length %zu)\n", - arr_len(json->values), arr_cap(json->values), strlen(json->text)); - json_debug_print_value(json, json->values[0]); -} - -// e.g. converts "Hello\nworld" to "Hello\\nworld" -// if out_sz is at least 2 * strlen(str) + 1, the string will fit. -// returns the number of bytes actually written, not including the null terminator. -size_t json_escape_to(char *out, size_t out_sz, const char *in) { - char *start = out; - char *end = out + out_sz; - assert(out_sz); - - --end; // leave room for null terminator - - for (; *in; ++in) { - if (out + 1 > end) { - break; - } - char esc = '\0'; - switch (*in) { - case '\0': goto brk; - case '\n': - esc = 'n'; - goto escape; - case '\\': - esc = '\\'; - goto escape; - case '"': - esc = '"'; - goto escape; - case '\t': - esc = 't'; - goto escape; - case '\r': - esc = 'r'; - goto escape; - case '\f': - esc = 'f'; - goto escape; - case '\b': - esc = 'b'; - goto escape; - escape: - if (out + 2 > end) - goto brk; - *out++ = '\\'; - *out++ = esc; - break; - default: - *out = *in; - ++out; - break; - } - } - brk: - *out = '\0'; - return (size_t)(out - start); -} - -// e.g. converts "Hello\nworld" to "Hello\\nworld" -// the resulting string should be free'd. -char *json_escape(const char *str) { - size_t out_sz = 2 * strlen(str) + 1; - char *out = calloc(1, out_sz); - json_escape_to(out, out_sz, str); - return out; -} - -#undef SKIP_WHITESPACE diff --git a/lsp-json.c b/lsp-json.c new file mode 100644 index 0000000..5bf50c6 --- /dev/null +++ b/lsp-json.c @@ -0,0 +1,768 @@ +// JSON parser for LSP +// provides FAST(ish) parsing but SLOW lookup +// this is especially fast for small objects +// this actually supports "extended json", where objects can have arbitrary values as keys. +typedef struct { + u32 pos; + u32 len; +} JSONString; + +typedef struct JSONValue JSONValue; + +typedef struct { + u32 len; + // this is an index into the values array + // values[items..items+len] store the names + // values[items+len..items+2*len] store the values + u32 items; +} JSONObject; + +typedef struct { + u32 len; + // this is an index into the values array + // values[elements..elements+len] are the elements + u32 elements; +} JSONArray; + +typedef enum { + // note: json doesn't actually include undefined. + // this is only for returning things from json_get etc. + JSON_UNDEFINED, + JSON_NULL, + JSON_FALSE, + JSON_TRUE, + JSON_NUMBER, + JSON_STRING, + JSON_OBJECT, + JSON_ARRAY +} JSONValueType; + +struct JSONValue { + JSONValueType type; + union { + double number; + JSONString string; + JSONArray array; + JSONObject object; + } val; +}; + + +typedef struct { + char error[64]; + bool is_text_copied; // if this is true, then json_free will call free on text + const char *text; + // root = values[0] + JSONValue *values; +} JSON; + + +#define SKIP_WHITESPACE while (json_is_space(text[index])) ++index; + +const char *json_type_to_str(JSONValueType type) { + switch (type) { + case JSON_UNDEFINED: + return "undefined"; + case JSON_NULL: + return "null"; + case JSON_STRING: + return "string"; + case JSON_NUMBER: + return "number"; + case JSON_FALSE: + return "false"; + case JSON_TRUE: + return "true"; + case JSON_ARRAY: + return "array"; + case JSON_OBJECT: + return "object"; + } + assert(0); + return "???"; +} + +static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val); +void json_debug_print_value(const JSON *json, JSONValue value); + +// defining this instead of using isspace seems to be faster +// probably because isspace depends on the locale. +static inline bool json_is_space(char c) { + return c == ' ' || c == '\n' || c == '\r' || c == '\t'; +} + +void json_debug_print_array(const JSON *json, JSONArray array) { + printf("["); + for (u32 i = 0; i < array.len; ++i) { + json_debug_print_value(json, json->values[array.elements + i]); + printf(", "); + } + printf("]"); +} + +void json_debug_print_object(const JSON *json, JSONObject obj) { + printf("{"); + for (u32 i = 0; i < obj.len; ++i) { + json_debug_print_value(json, json->values[obj.items + i]); + printf(": "); + json_debug_print_value(json, json->values[obj.items + obj.len + i]); + printf(", "); + } + printf("}"); +} + +void json_debug_print_string(const JSON *json, JSONString string) { + printf("\"%.*s\"", + (int)string.len, + json->text + string.pos); +} + +void json_debug_print_value(const JSON *json, JSONValue value) { + switch (value.type) { + case JSON_UNDEFINED: printf("undefined"); break; + case JSON_NULL: printf("null"); break; + case JSON_FALSE: printf("false"); break; + case JSON_TRUE: printf("true"); break; + case JSON_NUMBER: printf("%g", value.val.number); break; + case JSON_STRING: { + json_debug_print_string(json, value.val.string); + } break; + case JSON_ARRAY: { + json_debug_print_array(json, value.val.array); + } break; + case JSON_OBJECT: { + json_debug_print_object(json, value.val.object); + } break; + } +} + +// count number of comma-separated values until +// closing ] or } +static u32 json_count(JSON *json, u32 index) { + int bracket_depth = 0; + int brace_depth = 0; + u32 count = 1; + const char *text = json->text; + SKIP_WHITESPACE; + // special case: empty object/array + if (text[index] == '}' || text[index] == ']') + return 0; + +// int mark[5000] = {0}; + for (; ; ++index) { + switch (text[index]) { + case '\0': + return 0; // bad no closing bracket + case '[': + ++bracket_depth; + break; + case ']': + --bracket_depth; + if (bracket_depth < 0) + return count; + break; + case '{': + ++brace_depth; +// mark[index] = 1; + break; + case '}': + --brace_depth; +// mark[index] = 1; + if (brace_depth < 0){ + // useful visualization for debugging +// for (int i = 0; text[i]; ++i ){ +// switch (mark[i]){ +// case 1: printf("\x1b[91m"); break; +// case 2: printf("\x1b[92m"); break; +// } +// printf("%c",text[i]); +// if (mark[i]) printf("\x1b[0m"); +// } +// printf("\n"); + + return count; + } + break; + case ',': + if (bracket_depth == 0 && brace_depth == 0) + ++count; + break; + case '"': { + ++index; // skip opening " + int escaped = 0; + for (; ; ++index) { +// mark[index] = 2; + switch (text[index]) { + case '\0': return 0; // bad no closing quote + case '\\': escaped = !escaped; break; + case '"': + if (!escaped) + goto done; + escaped = false; + break; + default: + escaped = false; + break; + } + } + done:; + } break; + } + } +} + +static bool json_parse_object(JSON *json, u32 *p_index, JSONObject *object) { + u32 index = *p_index; + const char *text = json->text; + ++index; // go past { + u32 count = json_count(json, index); + object->len = count; + object->items = arr_len(json->values); + arr_set_len(json->values, arr_len(json->values) + 2 * count); + + for (u32 i = 0; i < count; ++i) { + if (i > 0) { + if (text[index] != ',') { + strbuf_printf(json->error, "stuff after value in object"); + return false; + } + ++index; + } + SKIP_WHITESPACE; + JSONValue name = {0}, value = {0}; + if (!json_parse_value(json, &index, &name)) + return false; + SKIP_WHITESPACE; + if (text[index] != ':') { + strbuf_printf(json->error, "stuff after name in object"); + return false; + } + ++index; // skip : + SKIP_WHITESPACE; + if (!json_parse_value(json, &index, &value)) + return false; + SKIP_WHITESPACE; + json->values[object->items + i] = name; + json->values[object->items + count + i] = value; + } + + if (text[index] != '}') { + strbuf_printf(json->error, "mismatched brackets or quotes."); + return false; + } + ++index; // skip } + *p_index = index; + return true; +} + +static bool json_parse_array(JSON *json, u32 *p_index, JSONArray *array) { + u32 index = *p_index; + const char *text = json->text; + ++index; // go past [ + u32 count = json_count(json, index); + array->len = count; + array->elements = arr_len(json->values); + + arr_set_len(json->values, arr_len(json->values) + count); + + SKIP_WHITESPACE; + + for (u32 i = 0; i < count; ++i) { + if (i > 0) { + if (text[index] != ',') { + strbuf_printf(json->error, "stuff after element in array"); + return false; + } + ++index; + } + SKIP_WHITESPACE; + JSONValue element = {0}; + if (!json_parse_value(json, &index, &element)) + return false; + SKIP_WHITESPACE; + json->values[array->elements + i] = element; + } + + if (text[index] != ']') { + strbuf_printf(json->error, "mismatched brackets or quotes."); + return false; + } + ++index; // skip ] + *p_index = index; + return true; +} + +static bool json_parse_string(JSON *json, u32 *p_index, JSONString *string) { + u32 index = *p_index; + ++index; // skip opening " + string->pos = index; + const char *text = json->text; + bool escaped = false; + for (; ; ++index) { + switch (text[index]) { + case '"': + if (!escaped) + goto done; + escaped = false; + break; + case '\\': + escaped = !escaped; + break; + case '\0': + strbuf_printf(json->error, "string literal goes to end of JSON"); + return false; + default: + escaped = false; + break; + } + } + done: + string->len = index - string->pos; + ++index; // skip closing " + *p_index = index; + return true; +} + +static bool json_parse_number(JSON *json, u32 *p_index, double *number) { + char *endp = NULL; + const char *text = json->text; + u32 index = *p_index; + *number = strtod(text + index, &endp); + if (endp == text + index) { + strbuf_printf(json->error, "bad number"); + return false; + } + index = (u32)(endp - text); + *p_index = index; + return true; +} + +static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val) { + const char *text = json->text; + u32 index = *p_index; + SKIP_WHITESPACE; + switch (text[index]) { + case '{': + val->type = JSON_OBJECT; + if (!json_parse_object(json, &index, &val->val.object)) + return false; + break; + case '[': + val->type = JSON_ARRAY; + if (!json_parse_array(json, &index, &val->val.array)) + return false; + break; + case '"': + val->type = JSON_STRING; + if (!json_parse_string(json, &index, &val->val.string)) + return false; + break; + case ANY_DIGIT: + case '-': + case '+': + val->type = JSON_NUMBER; + if (!json_parse_number(json, &index, &val->val.number)) + return false; + break; + case 'f': + val->type = JSON_FALSE; + if (!str_has_prefix(&text[index], "false")) + return false; + index += 5; + break; + case 't': + val->type = JSON_TRUE; + if (!str_has_prefix(&text[index], "true")) + return false; + index += 4; + break; + case 'n': + val->type = JSON_NULL; + if (!str_has_prefix(&text[index], "null")) + return false; + index += 4; + break; + default: + strbuf_printf(json->error, "bad value"); + return false; + } + *p_index = index; + return true; +} + +void json_free(JSON *json) { + arr_free(json->values); + // important we don't zero json here because we want to preserve json->error. + if (json->is_text_copied) { + free((void*)json->text); + } + json->text = NULL; +} + +// NOTE: text must live as long as json!!! +bool json_parse(JSON *json, const char *text) { + memset(json, 0, sizeof *json); + json->text = text; + arr_reserve(json->values, strlen(text) / 8); + arr_addp(json->values); // add root + JSONValue val = {0}; + u32 index = 0; + if (!json_parse_value(json, &index, &val)) { + json_free(json); + return false; + } + SKIP_WHITESPACE; + if (text[index]) { + json_free(json); + strbuf_printf(json->error, "extra text after end of root object"); + return false; + } + json->values[0] = val; + return true; +} + +// like json_parse, but a copy of text is made, so you can free/overwrite it immediately. +bool json_parse_copy(JSON *json, const char *text) { + bool success = json_parse(json, str_dup(text)); + if (success) { + json->is_text_copied = true; + return true; + } else { + free((void*)json->text); + json->text = NULL; + return false; + } +} + +static bool json_streq(const JSON *json, const JSONString *string, const char *name) { + const char *p = &json->text[string->pos]; + const char *end = p + string->len; + for (; p < end; ++p, ++name) { + if (*name != *p) + return false; + } + return *name == '\0'; +} + +// returns undefined if the property `name` does not exist. +JSONValue json_object_get(const JSON *json, JSONObject object, const char *name) { + const JSONValue *items = &json->values[object.items]; + for (u32 i = 0; i < object.len; ++i) { + const JSONValue *this_name = items++; + if (this_name->type == JSON_STRING && json_streq(json, &this_name->val.string, name)) { + return json->values[object.items + object.len + i]; + } + } + return (JSONValue){0}; +} + +JSONValue json_array_get(const JSON *json, JSONArray array, u64 i) { + if (i < array.len) { + return json->values[array.elements + i]; + } + return (JSONValue){0}; +} + +// returns NaN if `x` is not a number (ha ha). +double json_force_number(JSONValue x) { + if (x.type == JSON_NUMBER) { + return x.val.number; + } else { + return NAN; + } +} + +double json_object_get_number(const JSON *json, JSONObject object, const char *name) { + return json_force_number(json_object_get(json, object, name)); +} + +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) { + return x.val.string; + } else { + return (JSONString){0}; + } +} + +JSONString json_object_get_string(const JSON *json, JSONObject object, const char *name) { + return json_force_string(json_object_get(json, object, name)); +} + +JSONString json_array_get_string(const JSON *json, JSONArray array, size_t i) { + return json_force_string(json_array_get(json, array, i)); +} + +// returns (JSONObject){0} (which is interpreted as an empty object) if `x` is not an object +JSONObject json_force_object(JSONValue x) { + if (x.type == JSON_OBJECT) { + return x.val.object; + } else { + return (JSONObject){0}; + } +} + +JSONObject json_object_get_object(const JSON *json, JSONObject object, const char *name) { + return json_force_object(json_object_get(json, object, name)); +} + +JSONObject json_array_get_object(const JSON *json, JSONArray array, size_t i) { + return json_force_object(json_array_get(json, array, i)); +} + +// returns (JSONArray){0} (which is interpreted as an empty array) if `x` is not an array +JSONArray json_force_array(JSONValue x) { + if (x.type == JSON_ARRAY) { + return x.val.array; + } else { + return (JSONArray){0}; + } +} + +JSONArray json_object_get_array(const JSON *json, JSONObject object, const char *name) { + return json_force_array(json_object_get(json, object, name)); +} + +JSONArray json_array_get_array(const JSON *json, JSONArray array, size_t i) { + return json_force_array(json_array_get(json, array, i)); +} + +JSONValue json_root(const JSON *json) { + return json->values[0]; +} + +// e.g. if json is { "a" : { "b": 3 }}, then json_get(json, "a.b") = 3. +// returns undefined if there is no such property +JSONValue json_get(const JSON *json, const char *path) { + char segment[128]; + const char *p = path; + if (!json->values) { + return (JSONValue){0}; + } + JSONValue curr_value = json->values[0]; + while (*p) { + size_t segment_len = strcspn(p, "."); + strn_cpy(segment, sizeof segment, p, segment_len); + if (curr_value.type != JSON_OBJECT) { + return (JSONValue){0}; + } + curr_value = json_object_get(json, curr_value.val.object, segment); + p += segment_len; + if (*p == '.') ++p; + } + return curr_value; +} + +// equivalent to json_get(json, path).type != JSON_UNDEFINED, but more readable +bool json_has(const JSON *json, const char *path) { + JSONValue value = json_get(json, path); + return value.type != JSON_UNDEFINED; +} + +// turn a json string into a null terminated string. +// this won't be nice if the json string includes \u0000 but that's rare. +// if buf_sz > string->len, the string will fit. +void json_string_get(const JSON *json, JSONString string, char *buf, size_t buf_sz) { + const char *text = json->text; + if (buf_sz == 0) { + assert(0); + return; + } + char *buf_end = buf + buf_sz - 1; + for (u32 i = string.pos, end = string.pos + string.len; i < end && buf < buf_end; ++i) { + if (text[i] != '\\') { + *buf++ = text[i]; + } else { + ++i; + if (i >= end) break; + // escape sequence + switch (text[i]) { + case 'n': *buf++ = '\n'; break; + case 'r': *buf++ = '\r'; break; + case 'b': *buf++ = '\b'; break; + case 't': *buf++ = '\t'; break; + case 'f': *buf++ = '\f'; break; + case '\\': *buf++ = '\\'; break; + case '/': *buf++ = '/'; break; + case '"': *buf++ = '"'; break; + case 'u': { + if ((buf_end - buf) < 4 || i + 5 > end) + goto brk; + ++i; + + char hex[5] = {0}; + hex[0] = text[i++]; + hex[1] = text[i++]; + hex[2] = text[i++]; + hex[3] = text[i++]; + unsigned code_point=0; + sscanf(hex, "%04x", &code_point); + // technically this won't deal with people writing out UTF-16 surrogate halves + // using \u. i dont care. + size_t n = unicode_utf32_to_utf8(buf, code_point); + if (n <= 4) buf += n; + } break; + } + } + } + brk: + *buf = '\0'; +} + +// returns a malloc'd null-terminated string. +static char *json_string_get_alloc(const JSON *json, JSONString string) { + u32 n = string.len + 1; + if (n == 0) --n; // extreme edge case + char *buf = calloc(1, n); + json_string_get(json, string, buf, n); + return buf; +} + + +#if __unix__ +static void json_test_time_large(const char *filename) { + struct timespec start={0},end={0}; + FILE *fp = fopen(filename,"rb"); + if (!fp) { + perror(filename); + return; + } + + fseek(fp,0,SEEK_END); + size_t sz = (size_t)ftell(fp); + char *buf = calloc(1,sz+1); + rewind(fp); + fread(buf, 1, sz, fp); + fclose(fp); + for (int trial = 0; trial < 5; ++trial) { + clock_gettime(CLOCK_MONOTONIC, &start); + JSON json={0}; + bool success = json_parse(&json, buf); + if (!success) { + printf("FAIL: %s\n",json.error); + return; + } + + json_free(&json); + clock_gettime(CLOCK_MONOTONIC, &end); + + + + printf("time: %.1fms\n", + ((double)end.tv_sec*1e3+(double)end.tv_nsec*1e-6) + -((double)start.tv_sec*1e3+(double)start.tv_nsec*1e-6)); + } + +} +static void json_test_time_small(void) { + struct timespec start={0},end={0}; + int trials = 50000000; + clock_gettime(CLOCK_MONOTONIC, &start); + for (int trial = 0; trial < trials; ++trial) { + JSON json={0}; + bool success = json_parse(&json, "{\"hello\":\"there\"}"); + if (!success) { + printf("FAIL: %s\n",json.error); + return; + } + + json_free(&json); + } + clock_gettime(CLOCK_MONOTONIC, &end); + printf("time per trial: %.1fns\n", + (((double)end.tv_sec*1e9+(double)end.tv_nsec) + -((double)start.tv_sec*1e9+(double)start.tv_nsec)) + / trials); + +} +#endif + +void json_debug_print(const JSON *json) { + printf("%u values (capacity %u, text length %zu)\n", + arr_len(json->values), arr_cap(json->values), strlen(json->text)); + json_debug_print_value(json, json->values[0]); +} + +// e.g. converts "Hello\nworld" to "Hello\\nworld" +// if out_sz is at least 2 * strlen(str) + 1, the string will fit. +// returns the number of bytes actually written, not including the null terminator. +size_t json_escape_to(char *out, size_t out_sz, const char *in) { + char *start = out; + char *end = out + out_sz; + assert(out_sz); + + --end; // leave room for null terminator + + for (; *in; ++in) { + if (out + 1 > end) { + break; + } + char esc = '\0'; + switch (*in) { + case '\0': goto brk; + case '\n': + esc = 'n'; + goto escape; + case '\\': + esc = '\\'; + goto escape; + case '"': + esc = '"'; + goto escape; + case '\t': + esc = 't'; + goto escape; + case '\r': + esc = 'r'; + goto escape; + case '\f': + esc = 'f'; + goto escape; + case '\b': + esc = 'b'; + goto escape; + escape: + if (out + 2 > end) + goto brk; + *out++ = '\\'; + *out++ = esc; + break; + default: + *out = *in; + ++out; + break; + } + } + brk: + *out = '\0'; + return (size_t)(out - start); +} + +// e.g. converts "Hello\nworld" to "Hello\\nworld" +// the resulting string should be free'd. +char *json_escape(const char *str) { + size_t out_sz = 2 * strlen(str) + 1; + char *out = calloc(1, out_sz); + json_escape_to(out, out_sz, str); + return out; +} + +#undef SKIP_WHITESPACE diff --git a/lsp-parse.c b/lsp-parse.c index 07837b5..a892539 100644 --- a/lsp-parse.c +++ b/lsp-parse.c @@ -25,6 +25,7 @@ static WarnUnusedResult bool lsp_expect_number(LSP *lsp, JSONValue value, const return lsp_expect_type(lsp, value, JSON_NUMBER, what); } + static LSPString lsp_response_add_json_string(LSPResponse *response, const JSON *json, JSONString string) { u32 offset = arr_len(response->string_data); arr_set_len(response->string_data, offset + string.len + 1); @@ -80,6 +81,24 @@ static bool parse_range(LSP *lsp, const JSON *json, JSONValue range_value, LSPRa return success; } +static bool parse_document_uri(LSP *lsp, const JSON *json, JSONValue value, LSPDocumentID *id) { + if (value.type != JSON_STRING) { + lsp_set_error(lsp, "Expected string for URI, got %s", + json_type_to_str(value.type)); + return false; + } + char *string = json_string_get_alloc(json, value.val.string); + if (!str_has_prefix(string, "file://")) { + lsp_set_error(lsp, "Can't process non-local URI %s", + string); + free(string); + return false; + } + *id = lsp_document_id(lsp, string + strlen("file://")); + free(string); + return true; +} + static uint32_t *parse_trigger_characters(const JSON *json, JSONArray trigger_chars) { uint32_t *array = NULL; @@ -136,6 +155,12 @@ static void parse_capabilities(LSP *lsp, const JSON *json, JSONObject capabiliti cap->hover_support = true; } + // check for definition support + JSONValue definition_value = json_object_get(json, capabilities, "definitionProvider"); + if (definition_value.type != JSON_UNDEFINED) { + cap->definition_support = true; + } + JSONObject workspace = json_object_get_object(json, capabilities, "workspace"); // check WorkspaceFoldersServerCapabilities JSONObject workspace_folders = json_object_get_object(json, workspace, "workspaceFolders"); @@ -208,7 +233,7 @@ static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) item->text_edit = (LSPTextEdit) { .type = LSP_TEXT_EDIT_PLAIN, .at_cursor = true, - .range = {{0}}, + .range = {{0}, {0}}, .new_text = item->label }; @@ -462,6 +487,47 @@ static bool parse_hover(LSP *lsp, const JSON *json, LSPResponse *response) { return true; } +// parse a Location: {uri: DocumentUri, range: Range} +static bool parse_location(LSP *lsp, const JSON *json, JSONValue value, LSPLocation *location) { + if (value.type != JSON_OBJECT) { + lsp_set_error(lsp, "Expected object for location but got %s", + json_type_to_str(value.type)); + return false; + } + JSONObject object = value.val.object; + JSONValue uri = json_object_get(json, object, "uri"); + if (!parse_document_uri(lsp, json, uri, &location->document)) + return false; + JSONValue range = json_object_get(json, object, "range"); + if (!parse_range(lsp, json, range, &location->range)) + return false; + return true; +} + +static bool parse_definition(LSP *lsp, const JSON *json, LSPResponse *response) { + JSONValue result = json_get(json, "result"); + if (result.type == JSON_NULL) { + // no location + return true; + } + LSPResponseDefinition *definition = &response->data.definition; + if (result.type == JSON_ARRAY) { + JSONArray locations = result.val.array; + if (locations.len == 0) + return true; + for (u32 l = 0; l < locations.len; ++l) { + JSONValue location_json = json_array_get(json, locations, l); + LSPLocation *location = arr_addp(definition->locations); + if (!parse_location(lsp, json, location_json, location)) + return false; + } + return true; + } else { + LSPLocation *location = arr_addp(definition->locations); + return parse_location(lsp, json, result, location); + } +} + // fills request->id/id_string appropriately given the request's json // returns true on success static WarnUnusedResult bool parse_id(JSON *json, LSPRequest *request) { @@ -552,6 +618,8 @@ static bool parse_server2client_request(LSP *lsp, JSON *json, LSPRequest *reques return false; } + + static void process_message(LSP *lsp, JSON *json) { #if 0 @@ -601,6 +669,9 @@ static void process_message(LSP *lsp, JSON *json) { case LSP_REQUEST_HOVER: add_to_messages = parse_hover(lsp, json, &response); break; + case LSP_REQUEST_DEFINITION: + add_to_messages = parse_definition(lsp, json, &response); + break; case LSP_REQUEST_INITIALIZE: { // it's the response to our initialize request! if (result.type == JSON_OBJECT) { diff --git a/lsp-write.c b/lsp-write.c index 7db0f66..7d8a0f1 100644 --- a/lsp-write.c +++ b/lsp-write.c @@ -1,3 +1,5 @@ +#define write_bool lsp_write_bool // prevent naming conflict + static const char *lsp_language_id(Language lang) { switch (lang) { case LANG_CONFIG: @@ -267,6 +269,8 @@ static const char *lsp_request_method(LSPRequest *request) { return "textDocument/signatureHelp"; case LSP_REQUEST_HOVER: return "textDocument/hover"; + case LSP_REQUEST_DEFINITION: + return "textDocument/definition"; case LSP_REQUEST_WORKSPACE_FOLDERS: return "workspace/workspaceFolders"; case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: @@ -296,6 +300,7 @@ static bool request_type_is_notification(LSPRequestType type) { case LSP_REQUEST_COMPLETION: case LSP_REQUEST_SIGNATURE_HELP: case LSP_REQUEST_HOVER: + case LSP_REQUEST_DEFINITION: case LSP_REQUEST_WORKSPACE_FOLDERS: return false; } @@ -512,6 +517,12 @@ static void write_request(LSP *lsp, LSPRequest *request) { write_document_position(o, hover->position); write_obj_end(o); } break; + case LSP_REQUEST_DEFINITION: { + const LSPRequestDefinition *def = &request->data.definition; + write_key_obj_start(o, "params"); + write_document_position(o, def->position); + write_obj_end(o); + } break; case LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS: { const LSPRequestDidChangeWorkspaceFolders *w = &request->data.change_workspace_folders; write_key_obj_start(o, "params"); @@ -605,3 +616,5 @@ static void write_message(LSP *lsp, LSPMessage *message) { // (as i'm writing this, this won't do anything but it might in the future) lsp_message_free(message); } + +#undef write_bool diff --git a/lsp.c b/lsp.c index 50947ce..830f90d 100644 --- a/lsp.c +++ b/lsp.c @@ -3,8 +3,6 @@ // print client-to-server communication #define LSP_SHOW_C2S 0 -#define write_bool lsp_write_bool - static void lsp_request_free(LSPRequest *r); static void lsp_response_free(LSPResponse *r); @@ -13,6 +11,7 @@ static void lsp_response_free(LSPResponse *r); strbuf_printf(lsp->error, __VA_ARGS__);\ SDL_UnlockMutex(lsp->error_mutex);\ } while (0) +#include "lsp-json.c" #include "lsp-write.c" #include "lsp-parse.c" @@ -44,6 +43,7 @@ static void lsp_request_free(LSPRequest *r) { case LSP_REQUEST_COMPLETION: case LSP_REQUEST_SIGNATURE_HELP: case LSP_REQUEST_HOVER: + case LSP_REQUEST_DEFINITION: case LSP_REQUEST_DID_CLOSE: case LSP_REQUEST_WORKSPACE_FOLDERS: case LSP_REQUEST_JDTLS_CONFIGURATION: @@ -80,6 +80,9 @@ static void lsp_response_free(LSPResponse *r) { case LSP_REQUEST_SIGNATURE_HELP: arr_free(r->data.signature_help.signatures); break; + case LSP_REQUEST_DEFINITION: + arr_free(r->data.definition.locations); + break; default: break; } @@ -145,6 +148,8 @@ static bool lsp_supports_request(LSP *lsp, const LSPRequest *request) { return cap->workspace_folders_support; case LSP_REQUEST_HOVER: return cap->hover_support; + case LSP_REQUEST_DEFINITION: + return cap->definition_support; } assert(0); return false; @@ -542,4 +547,18 @@ bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b) { return a.document == b.document && lsp_position_eq(a.pos, b.pos); } -#undef write_bool + +LSPDocumentPosition lsp_location_start_position(LSPLocation location) { + return (LSPDocumentPosition) { + .document = location.document, + .pos = location.range.start + }; +} + +LSPDocumentPosition lsp_location_end_position(LSPLocation location) { + return (LSPDocumentPosition) { + .document = location.document, + .pos = location.range.end + }; +} + diff --git a/lsp.h b/lsp.h index 389f38e..b97df7d 100644 --- a/lsp.h +++ b/lsp.h @@ -26,6 +26,11 @@ typedef struct { LSPPosition end; } LSPRange; +typedef struct { + LSPDocumentID document; + LSPRange range; +} LSPLocation; + typedef enum { LSP_REQUEST_NONE, @@ -44,6 +49,7 @@ typedef enum { LSP_REQUEST_COMPLETION, // textDocument/completion LSP_REQUEST_SIGNATURE_HELP, // textDocument/signatureHelp LSP_REQUEST_HOVER, // textDocument/hover + LSP_REQUEST_DEFINITION, // textDocument/definition LSP_REQUEST_DID_CHANGE_WORKSPACE_FOLDERS, // workspace/didChangeWorkspaceFolders // server-to-client @@ -116,6 +122,10 @@ typedef struct { LSPDocumentPosition position; } LSPRequestHover; +typedef struct { + LSPDocumentPosition position; +} LSPRequestDefinition; + typedef struct { LSPDocumentID *removed; // dynamic array LSPDocumentID *added; // dynamic array @@ -134,6 +144,7 @@ typedef struct { LSPRequestCompletion completion; LSPRequestSignatureHelp signature_help; LSPRequestHover hover; + LSPRequestDefinition definition; // LSP_REQUEST_SHOW_MESSAGE or LSP_REQUEST_LOG_MESSAGE LSPRequestMessage message; LSPRequestDidChangeWorkspaceFolders change_workspace_folders; @@ -279,6 +290,10 @@ typedef struct { LSPString contents; } LSPResponseHover; +typedef struct { + // where the symbol is defined (dynamic array) + LSPLocation *locations; +} LSPResponseDefinition; typedef LSPRequestType LSPResponseType; typedef struct { @@ -292,6 +307,7 @@ typedef struct { LSPResponseCompletion completion; LSPResponseSignatureHelp signature_help; LSPResponseHover hover; + LSPResponseDefinition definition; } data; } LSPResponse; @@ -312,6 +328,7 @@ typedef struct { bool signature_help_support; bool completion_support; bool hover_support; + bool definition_support; // support for multiple root folders // sadly, as of me writing this, clangd and rust-analyzer don't support this // (but jdtls and gopls do) @@ -398,8 +415,11 @@ LSP *lsp_create(const char *root_dir, Language language, const char *analyzer_co // if this fails (i.e. if the LSP does not have workspace support), create a new LSP // with root directory `new_root_dir`. bool lsp_try_add_root_dir(LSP *lsp, const char *new_root_dir); +// report that this document has changed +void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); bool lsp_next_message(LSP *lsp, LSPMessage *message); bool lsp_position_eq(LSPPosition a, LSPPosition b); bool lsp_document_position_eq(LSPDocumentPosition a, LSPDocumentPosition b); -void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); +LSPDocumentPosition lsp_location_start_position(LSPLocation location); +LSPDocumentPosition lsp_location_end_position(LSPLocation location); void lsp_free(LSP *lsp); diff --git a/main.c b/main.c index 7fe7847..c02774f 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,7 @@ /* @TODO: +- check definition capabilities +- some way of showing that we're currently loading the definition location (different cursor color?) - more LSP stuff: - go to definition using LSP - find usages @@ -13,9 +15,11 @@ - what to do if initialize request takes a long time? - delete old sent requests? but make sure requests that just take a long time are okay. (if the server never sends a response) +- check that tags still works - TESTING: make rust-analyzer-slow (waits 10s before sending response) - run everything through valgrind ideally with leak checking - grep -i -n TODO *.[ch] +- when searching files/definitions, sort by length? or put exact matches at the top? --- LSP MERGE --- - improve structure of ted source code to make LSP completions better (make every c file a valid translation unit) @@ -140,13 +144,13 @@ bool tag_goto(Ted *ted, char const *tag); #include "build.c" #include "tags.c" #include "menu.c" -#include "autocomplete.c" -#include "signature-help.c" -#include "hover.c" +#include "ide-autocomplete.c" +#include "ide-signature-help.c" +#include "ide-hover.c" +#include "ide-definitions.c" #include "command.c" #include "config.c" #include "session.c" -#include "json.c" #include "lsp.c" #if PROFILE @@ -891,6 +895,7 @@ int main(int argc, char **argv) { autocomplete_process_lsp_response(ted, r); signature_help_process_lsp_response(ted, r); hover_process_lsp_response(ted, r); + definitions_process_lsp_response(ted, lsp, r); } break; } lsp_message_free(&message); diff --git a/signature-help.c b/signature-help.c deleted file mode 100644 index 97dc8c3..0000000 --- a/signature-help.c +++ /dev/null @@ -1,153 +0,0 @@ -// deals with textDocument/signatureHelp LSP requests - -static void signature_help_clear(SignatureHelp *help) { - for (int i = 0; i < help->signature_count; ++i) { - Signature sig = help->signatures[i]; - free(sig.label_pre); - free(sig.label_active); - free(sig.label_post); - } - memset(help->signatures, 0, sizeof help->signatures); - help->signature_count = 0; -} - -void signature_help_send_request(Ted *ted) { - SignatureHelp *help = &ted->signature_help; - Settings *settings = ted_active_settings(ted); - if (!settings->signature_help_enabled) { - signature_help_clear(help); - return; - } - TextBuffer *buffer = ted->active_buffer; - if (!buffer) { - signature_help_clear(help); - return; - } - LSP *lsp = buffer_lsp(buffer); - if (!lsp) { - signature_help_clear(help); - return; - } - LSPRequest request = {.type = LSP_REQUEST_SIGNATURE_HELP}; - LSPRequestSignatureHelp *s = &request.data.signature_help; - s->position = buffer_cursor_pos_as_lsp_document_position(buffer); - lsp_send_request(lsp, &request); - help->retrigger = false; -} - -void signature_help_retrigger(Ted *ted) { - // don't just send the request here -- we don't want to send more than - // one request per frame. - ted->signature_help.retrigger = true; -} - -void signature_help_open(Ted *ted, char32_t trigger) { - (void)trigger; // for now we don't send context - signature_help_send_request(ted); -} - -bool signature_help_is_open(Ted *ted) { - return ted->signature_help.signature_count > 0; -} - - -void signature_help_close(Ted *ted) { - signature_help_clear(&ted->signature_help); -} - -void signature_help_process_lsp_response(Ted *ted, const LSPResponse *response) { - Settings *settings = ted_active_settings(ted); - if (!settings->signature_help_enabled) return; - - if (response->request.type != LSP_REQUEST_SIGNATURE_HELP) - return; - SignatureHelp *help = &ted->signature_help; - const LSPResponseSignatureHelp *lsp_help = &response->data.signature_help; - u32 signature_count = arr_len(lsp_help->signatures); - if (signature_count > SIGNATURE_HELP_MAX) - signature_count = SIGNATURE_HELP_MAX; - - signature_help_clear(help); - for (u32 s = 0; s < signature_count; ++s) { - Signature *signature = &help->signatures[s]; - LSPSignatureInformation *lsp_signature = &lsp_help->signatures[s]; - - const char *label = lsp_response_string(response, lsp_signature->label); - size_t start = unicode_utf16_to_utf8_offset(label, lsp_signature->active_start); - size_t end = unicode_utf16_to_utf8_offset(label, lsp_signature->active_end); - if (start == (size_t)-1) { - assert(0); - start = 0; - } - if (end == (size_t)-1) { - assert(0); - end = 0; - } - u32 active_start = (u32)start; - u32 active_end = (u32)end; - signature->label_pre = strn_dup(label, active_start); - signature->label_active = strn_dup(label + active_start, active_end - active_start); - signature->label_post = str_dup(label + active_end); - } - - help->signature_count = (u16)signature_count; -} - -void signature_help_frame(Ted *ted) { - Settings *settings = ted_active_settings(ted); - if (!settings->signature_help_enabled) - return; - - SignatureHelp *help = &ted->signature_help; - if (help->retrigger) - signature_help_send_request(ted); - u16 signature_count = help->signature_count; - if (!signature_count) - return; - Font *font = ted->font; - Font *font_bold = ted->font_bold; - TextBuffer *buffer = ted->active_buffer; - if (!buffer) - return; - - u32 *colors = settings->colors; - float border = settings->border_thickness; - - float width = buffer->x2 - buffer->x1; - float height = FLT_MAX; - // make sure signature help doesn't take up too much space - while (1) { - height = font->char_height * signature_count; - if (height < (buffer->y2 - buffer->y1) * 0.25f) - break; - --signature_count; - if (signature_count == 0) return; - } - float x = buffer->x1, y = buffer->y2 - height; - gl_geometry_rect(rect_xywh(x, y - border, width, border), - colors[COLOR_AUTOCOMPLETE_BORDER]); - gl_geometry_rect(rect_xywh(x, y, width, height), - colors[COLOR_AUTOCOMPLETE_BG]); - - // draw the signatures - for (int s = 0; s < signature_count; ++s) { - const Signature *signature = &help->signatures[s]; - TextRenderState state = text_render_state_default; - state.x = x; - state.y = y; - state.min_x = x; - state.min_y = y; - state.max_x = buffer->x2; - state.max_y = buffer->y2; - rgba_u32_to_floats(colors[COLOR_TEXT], state.color); - - text_utf8_with_state(font, &state, signature->label_pre); - text_utf8_with_state(font_bold, &state, signature->label_active); - text_utf8_with_state(font, &state, signature->label_post); - y += font->char_height; - } - - gl_geometry_draw(); - text_render(font); - text_render(font_bold); -} diff --git a/ted.c b/ted.c index 6fd2906..4a89e8f 100644 --- a/ted.c +++ b/ted.c @@ -507,3 +507,36 @@ void ted_press_key(Ted *ted, SDL_Scancode scancode, SDL_Keymod modifier) { } } } + +// make the cursor red for a bit to indicate an error (e.g. no autocompletions) +void ted_flash_error_cursor(Ted *ted) { + ted->cursor_error_time = time_get_seconds(); +} + +void ted_go_to_position(Ted *ted, const char *path, u32 line, u32 index, bool is_lsp) { + if (ted_open_file(ted, path)) { + TextBuffer *buffer = ted->active_buffer; + BufferPos pos = {0}; + if (is_lsp) { + LSPPosition lsp_pos = { + .line = line, + .character = index + }; + pos = buffer_pos_from_lsp(buffer, lsp_pos); + } else { + pos.line = line; + pos.index = index; + } + buffer_cursor_move_to_pos(buffer, pos); + buffer->center_cursor_next_frame = true; + } else { + ted_flash_error_cursor(ted); + } +} + +void ted_go_to_lsp_document_position(Ted *ted, LSP *lsp, LSPDocumentPosition position) { + const char *path = lsp_document_path(lsp, position.document); + u32 line = position.pos.line; + u32 character = position.pos.character; + ted_go_to_position(ted, path, line, character, true); +} diff --git a/ted.h b/ted.h index 774b5f5..be8fcb0 100644 --- a/ted.h +++ b/ted.h @@ -413,6 +413,14 @@ typedef struct { BufferPos range_end; } Hover; +typedef struct { + + // LSPID and ID of the last response which was processed. + // used to process responses in chronological order (= ID order) + LSPID last_response_lsp; + u32 last_response_id; +} Definitions; + typedef struct Ted { struct LSP *lsps[TED_LSP_MAX + 1]; @@ -470,6 +478,7 @@ typedef struct Ted { Autocomplete autocomplete; SignatureHelp signature_help; Hover hover; + Definitions definitions; FILE *log; @@ -530,9 +539,6 @@ typedef struct Ted { char error[512]; char error_shown[512]; // error display in box on screen } Ted; - -void autocomplete_close(Ted *ted); -void signature_help_retrigger(Ted *ted); char *buffer_contents_utf8_alloc(TextBuffer *buffer); Command command_from_str(char const *str); void command_execute(Ted *ted, Command c, i64 argument); @@ -566,3 +572,10 @@ char *settings_get_root_dir(Settings *settings, const char *path); void menu_open(Ted *ted, Menu menu); void menu_close(Ted *ted); void find_update(Ted *ted, bool force); +void autocomplete_close(Ted *ted); +void signature_help_retrigger(Ted *ted); +// go to the definition of `name`. +// if `lsp` is NULL, tags will be used. +// Note: the document position is required for LSP requests because of overloading (where the name +// alone isn't sufficient) +void definition_goto(Ted *ted, LSP *lsp, const char *name, LSPDocumentPosition pos); -- cgit v1.2.3