diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | autocomplete.c | 467 | ||||
-rw-r--r-- | base.h | 19 | ||||
-rw-r--r-- | buffer.c | 214 | ||||
-rw-r--r-- | build.c | 2 | ||||
-rw-r--r-- | colors.c | 69 | ||||
-rw-r--r-- | colors.h | 60 | ||||
-rw-r--r-- | command.c | 14 | ||||
-rw-r--r-- | ds.c (renamed from arr.c) | 120 | ||||
-rw-r--r-- | json.c | 713 | ||||
-rw-r--r-- | lsp-parse.c | 337 | ||||
-rw-r--r-- | lsp-write.c | 342 | ||||
-rw-r--r-- | lsp.c | 413 | ||||
-rw-r--r-- | lsp.h | 282 | ||||
-rw-r--r-- | main.c | 139 | ||||
-rw-r--r-- | menu.c | 2 | ||||
-rw-r--r-- | process-posix.c | 168 | ||||
-rw-r--r-- | process-win.c | 2 | ||||
-rw-r--r-- | process.h | 22 | ||||
-rw-r--r-- | ted.c | 15 | ||||
-rw-r--r-- | ted.cfg | 3 | ||||
-rw-r--r-- | ted.h | 39 | ||||
-rw-r--r-- | test.rs | 1 | ||||
-rw-r--r-- | unicode.h | 3 | ||||
-rw-r--r-- | util.c | 170 |
25 files changed, 3254 insertions, 366 deletions
@@ -2,9 +2,9 @@ ALL_CFLAGS=$(CFLAGS) -Wall -Wextra -Wshadow -Wconversion -Wpedantic -pedantic -s -Wno-unused-function -Wno-fixed-enum-extension -Wimplicit-fallthrough -Wno-format-truncation -Wno-unknown-warning-option \ -Ipcre2 LIBS=-lSDL2 -lGL -lm libpcre2-32.a -DEBUG_CFLAGS=$(ALL_CFLAGS) -DDEBUG -O0 -g +DEBUG_CFLAGS=$(ALL_CFLAGS) -Wno-unused-parameter -DDEBUG -O0 -g RELEASE_CFLAGS=$(ALL_CFLAGS) -O3 -PROFILE_CFLAGS=$(ALL_CFLAGS) -O3 -DPROFILE=1 +PROFILE_CFLAGS=$(ALL_CFLAGS) -O3 -g -DPROFILE=1 # if you change the directories below, ted won't work. # we don't yet have support for using different data directories GLOBAL_DATA_DIR=/usr/share/ted diff --git a/autocomplete.c b/autocomplete.c index 23523bd..930cd90 100644 --- a/autocomplete.c +++ b/autocomplete.c @@ -1,168 +1,357 @@ +#define TAGS_MAX_COMPLETIONS 200 // max # of tag completions to scroll through +#define AUTOCOMPLETE_NCOMPLETIONS_VISIBLE 10 // max # of completions to show at once -#define AUTOCOMPLETE_NCOMPLETIONS 10 // max # of completions to show - -// get the thing to be completed (and what buffer it's in) -// returns false if this is a read only buffer or something -// call free() on *startp when you're done with it -static Status autocomplete_get(Ted *ted, char **startp, TextBuffer **bufferp) { - TextBuffer *buffer = ted->active_buffer; - if (buffer && !buffer->view_only && !buffer->is_line_buffer) { - buffer->selection = false; - if (is_word(buffer_char_after_cursor(buffer))) - buffer_cursor_move_right_words(buffer, 1); - else - buffer_scroll_to_cursor(buffer); - char *start = str32_to_utf8_cstr(buffer_word_at_cursor(buffer)); - if (*start) { - *startp = start; - *bufferp = buffer; - return true; - } else { - // no word at cursor - free(start); - return false; - } - } else { - return false; - } +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); + } + arr_clear(ac->completions); + arr_clear(ac->suggested); } // do the actual completion -static void autocomplete_complete(Ted *ted, char *start, TextBuffer *buffer, char *completion) { - char *str = completion + strlen(start); +static void autocomplete_complete(Ted *ted, Autocompletion completion) { + TextBuffer *buffer = ted->active_buffer; buffer_start_edit_chain(buffer); // don't merge with other edits - buffer_insert_utf8_at_cursor(buffer, str); + 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); - ted->autocomplete = false; + autocomplete_close(ted); } static void autocomplete_select_cursor_completion(Ted *ted) { - char *start; TextBuffer *buffer; - if (autocomplete_get(ted, &start, &buffer)) { - char *completions[AUTOCOMPLETE_NCOMPLETIONS]; - size_t ncompletions = tags_beginning_with(ted, start, completions, arr_count(completions)); - if (ncompletions) { - i64 cursor = mod_i64(ted->autocomplete_cursor, (i64)ncompletions); - autocomplete_complete(ted, start, buffer, completions[cursor]); - for (size_t i = 0; i < ncompletions; ++i) - free(completions[i]); + 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); } - free(start); } } -// open autocomplete, or just do the completion if there's only one suggestion -static void autocomplete_open(Ted *ted) { - char *start; - TextBuffer *buffer; - ted->cursor_error_time = 0; - ted->autocomplete_cursor = 0; - if (autocomplete_get(ted, &start, &buffer)) { - char *completions[2] = {0}; - size_t ncompletions = tags_beginning_with(ted, start, completions, 2); - switch (ncompletions) { - case 0: - ted->cursor_error_time = time_get_seconds(); - break; - case 1: - autocomplete_complete(ted, start, buffer, completions[0]); - break; - case 2: - // open autocomplete selection menu - ted->autocomplete = true; - break; - default: assert(0); break; - } - free(completions[0]); - free(completions[1]); - free(start); + +static void autocomplete_next(Ted *ted) { + ++ted->autocomplete.cursor; +} + +static void autocomplete_prev(Ted *ted) { + --ted->autocomplete.cursor; +} + +void autocomplete_close(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + if (ac->open) { + ac->open = false; + ac->waiting_for_lsp = false; + autocomplete_clear_completions(ted); } } -static void autocomplete_frame(Ted *ted) { - - char *start; - TextBuffer *buffer; - if (autocomplete_get(ted, &start, &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; - - char *completions[AUTOCOMPLETE_NCOMPLETIONS]; - size_t ncompletions = tags_beginning_with(ted, start, completions, arr_count(completions)); - float menu_width = 400, menu_height = (float)ncompletions * char_height + 2 * padding; +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) { + ted->cursor_error_time = time_get_seconds(); + autocomplete_close(ted); +} + +static void autocomplete_find_completions(Ted *ted) { + 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->last_pos = pos; + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + LSPRequest request = { + .type = LSP_REQUEST_COMPLETION + }; + request.data.completion = (LSPRequestCompletion) { + .position = { + .document = lsp_document_id(lsp, buffer->filename), + .pos = buffer_pos_to_lsp(buffer, pos) + } + }; + lsp_send_request(lsp, &request); + if (!ac->completions) + ac->waiting_for_lsp = true; + } else { + // tag completion + autocomplete_clear_completions(ted); - if (ncompletions == 0) { - // no completions. close menu. - ted->autocomplete = false; - return; - } + 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); - ted->autocomplete_cursor = (i32)mod_i64(ted->autocomplete_cursor, (i64)ncompletions); + arr_set_len(ac->completions, ncompletions); - 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_MENU_BG]); - //gl_geometry_rect_border(menu_rect, 1, colors[COLOR_BORDER]); - ted->autocomplete_rect = menu_rect; + 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); } - - // vertical padding - start_y += padding; - menu_height -= 2 * padding; - - u16 cursor_entry = (u16)((ted->mouse_pos.y - start_y) / char_height); - if (cursor_entry < ncompletions) { - // highlight moused over entry - Rect r = rect(V2(x, start_y + cursor_entry * char_height), V2(menu_width, char_height)); - gl_geometry_rect(r, colors[COLOR_MENU_HL]); - ted->cursor = ted->cursor_hand; + free(completions); + } + + autocomplete_update_suggested(ted); +} + +static void autocomplete_process_lsp_response(Ted *ted, const LSPResponse *response) { + Autocomplete *ac = &ted->autocomplete; + bool was_waiting = ac->waiting_for_lsp; + ac->waiting_for_lsp = false; + if (!ac->open) { + // user hit escape before completions arrived. + return; + } + // @TODO: check if same buffer is open and if cursor has moved + const LSPRequest *request = &response->request; + if (request->type == LSP_REQUEST_COMPLETION) { + 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]; + // @TODO: deal with fancier textEdits + 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)); + 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); } - { // highlight cursor entry - Rect r = rect(V2(x, start_y + (float)ted->autocomplete_cursor * char_height), V2(menu_width, char_height)); - gl_geometry_rect(r, colors[COLOR_MENU_HL]); + } + autocomplete_update_suggested(ted); + switch (arr_len(ac->suggested)) { + case 0: + autocomplete_no_suggestions(ted); + return; + case 1: + // if we just finished loading suggestions, and there's only one suggestion, use it + if (was_waiting) + autocomplete_complete(ted, ac->completions[ac->suggested[0]]); + return; + } +} + +// open autocomplete, or just do the completion if there's only one suggestion +static void autocomplete_open(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + + 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); + + switch (arr_len(ac->completions)) { + case 0: + if (autocomplete_using_lsp(ted)) { + ac->open = true; + } else { + autocomplete_no_suggestions(ted); } - - 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(ted->autocomplete_rect, click)) { - u16 entry = (u16)((click.y - start_y) / char_height); - if (entry < ncompletions) { - // entry was clicked on! use this completion. - autocomplete_complete(ted, start, buffer, completions[entry]); - } + 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 ' '; + } +} + +static void autocomplete_frame(Ted *ted) { + Autocomplete *ac = &ted->autocomplete; + 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); + + Autocompletion completions[AUTOCOMPLETE_NCOMPLETIONS_VISIBLE] = {0}; + size_t ncompletions = 0; + arr_foreach_ptr(ac->suggested, u32, suggestion) { + Autocompletion *completion = &ac->completions[*suggestion]; + completions[ncompletions++] = *completion; + if (ncompletions == AUTOCOMPLETE_NCOMPLETIONS_VISIBLE) + break; + } + + float menu_width = 400, menu_height = (float)ncompletions * char_height + 2 * padding; + + if (ac->waiting_for_lsp) { + menu_height = 200.f; + } + + 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; + + 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_MENU_BG]); + //gl_geometry_rect_border(menu_rect, 1, colors[COLOR_BORDER]); + ac->rect = menu_rect; + } + + u16 cursor_entry = (u16)((ted->mouse_pos.y - start_y) / char_height); + if (cursor_entry < ncompletions) { + // highlight moused over entry + Rect r = rect(V2(x, start_y + cursor_entry * char_height), V2(menu_width, char_height)); + gl_geometry_rect(r, colors[COLOR_MENU_HL]); + ted->cursor = ted->cursor_hand; + } + + if (!ac->waiting_for_lsp) { + // highlight cursor entry + Rect r = rect(V2(x, start_y + (float)ac->cursor * char_height), V2(menu_width, char_height)); + gl_geometry_rect(r, colors[COLOR_MENU_HL]); + } + + 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)) { + u16 entry = (u16)((click.y - start_y) / char_height); + if (entry < ncompletions) { + // entry was clicked on! use this completion. + autocomplete_complete(ted, ac->completions[ac->suggested[entry]]); } } - - 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); + } + + 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); + + u8 border_thickness = settings->border_thickness; + + if (ac->waiting_for_lsp) { + state.x = x + padding; state.y = y; + text_utf8_with_state(font, &state, "Loading..."); + } else { for (size_t i = 0; i < ncompletions; ++i) { - state.x = x + padding; state.y = y; - text_utf8_with_state(font, &state, completions[i]); + + state.x = x; state.y = y; + 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(completions[i].kind); + rgba_u32_to_floats(colors[label_color], state.color); + + // draw icon + char icon_text[2] = {symbol_kind_icon(completions[i].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; + + + text_utf8_with_state(font, &state, completions[i].label); + + const char *detail = completions[i].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); + } + } y += char_height; } - - gl_geometry_draw(); - text_render(font); - - for (size_t i = 0; i < ncompletions; ++i) - free(completions[i]); - - free(start); - } else { - ted->autocomplete = false; } + + gl_geometry_draw(); + text_render(font); } @@ -111,10 +111,29 @@ typedef unsigned long long ullong; #define WarnUnusedResult #endif +#if __GNUC__ +#define ATTRIBUTE_PRINTF(fmt_idx, arg_idx) __attribute__ ((format(printf, fmt_idx, arg_idx))) +#else +#define ATTRIBUTE_PRINTF(fmt_idx, arg_idx) +#endif +#if _MSC_VER > 1400 +#define PRINTF_FORMAT_STRING _Printf_format_string_ +#else +#define PRINTF_FORMAT_STRING +#endif + #define Status bool WarnUnusedResult // false = error, true = success #define arr_count(a) (sizeof (a) / sizeof *(a)) + +// usage: if UNLIKELY (x > 2) ... +#if __GNUC__ +#define UNLIKELY(x) (__builtin_expect(x,0)) +#else +#define UNLIKELY(x) (x) +#endif + #ifdef __GNUC__ #define no_warn_start _Pragma("GCC diagnostic push") \ _Pragma("GCC diagnostic ignored \"-Wpedantic\"") \ @@ -67,6 +67,10 @@ bool buffer_is_untitled(TextBuffer *buffer) { return false; } +bool buffer_is_named_file(TextBuffer *buffer) { + return buffer->filename && !buffer_is_untitled(buffer); +} + // add this edit to the undo history static void buffer_append_edit(TextBuffer *buffer, BufferEdit const *edit) { // whenever an edit is made, clear the redo history @@ -245,6 +249,8 @@ static inline Font *buffer_font(TextBuffer *buffer) { // what programming language is this? Language buffer_language(TextBuffer *buffer) { + // @TODO: cache this? + // (we're calling buffer_lsp on every edit and that calls this) if (buffer->manual_language >= 1 && buffer->manual_language <= LANG_COUNT) return (Language)(buffer->manual_language - 1); Settings const *settings = buffer->ted->default_settings; // important we don't use buffer_settings here since that would cause a loop! @@ -280,6 +286,13 @@ Language buffer_language(TextBuffer *buffer) { return match; } +LSP *buffer_lsp(TextBuffer *buffer) { + if (!buffer_is_named_file(buffer)) + return NULL; + return ted_get_lsp(buffer->ted, buffer_language(buffer)); +} + + // score is higher if context is closer match. static long context_score(const char *path, Language lang, const SettingsContext *context) { long score = 0; @@ -545,6 +558,10 @@ void buffer_check_valid(TextBuffer *buffer) { } } #else +static void buffer_pos_check_valid(TextBuffer *buffer, BufferPos p) { + (void)buffer; (void)p; +} + void buffer_check_valid(TextBuffer *buffer) { (void)buffer; } @@ -729,6 +746,15 @@ static void buffer_line_free(Line *line) { // Free a buffer. Once a buffer is freed, you can call buffer_create on it again. // Does not free the pointer `buffer` (buffer might not have even been allocated with malloc) void buffer_free(TextBuffer *buffer) { + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + LSPRequest did_close = {.type = LSP_REQUEST_DID_CLOSE}; + did_close.data.close = (LSPRequestDidClose){ + .document = lsp_document_id(lsp, buffer->filename) + }; + lsp_send_request(lsp, &did_close); + } + Line *lines = buffer->lines; u32 nlines = buffer->nlines; for (u32 i = 0; i < nlines; ++i) { @@ -1370,6 +1396,36 @@ static Status buffer_insert_lines(TextBuffer *buffer, u32 where, u32 number) { return false; } +// LSP uses UTF-16 indices because Microsoft fucking loves UTF-16 and won't let it die +LSPPosition buffer_pos_to_lsp(TextBuffer *buffer, BufferPos pos) { + LSPPosition lsp_pos = { + .line = pos.line + }; + buffer_pos_validate(buffer, &pos); + const Line *line = &buffer->lines[pos.line]; + const char32_t *str = line->str; + for (uint32_t i = 0; i < pos.index; ++i) { + if (str[i] < 0x10000) + lsp_pos.character += 1; // this codepoint needs 1 UTF-16 word + else + lsp_pos.character += 2; // this codepoint needs 2 UTF-16 words + } + return lsp_pos; +} + +static void buffer_send_lsp_did_change_request(LSP *lsp, TextBuffer *buffer, BufferPos pos, + u32 nchars_deleted, String32 new_text) { + if (!buffer_is_named_file(buffer)) + return; // this isn't a named buffer so we can't send a didChange request. + LSPDocumentChangeEvent event = {0}; + if (new_text.len > 0) + event.text = str32_to_utf8_cstr(new_text); + event.range.start = buffer_pos_to_lsp(buffer, pos); + BufferPos pos_end = buffer_pos_advance(buffer, pos, nchars_deleted); + event.range.end = buffer_pos_to_lsp(buffer, pos_end); + lsp_document_changed(lsp, buffer->filename, event); +} + // inserts the given text, returning the position of the end of the text BufferPos buffer_insert_text_at_pos(TextBuffer *buffer, BufferPos pos, String32 str) { buffer_pos_validate(buffer, &pos); @@ -1405,6 +1461,10 @@ BufferPos buffer_insert_text_at_pos(TextBuffer *buffer, BufferPos pos, String32 str32_remove_all_instances_of_char(&str, '\r'); + LSP *lsp = buffer_lsp(buffer); + if (lsp) + buffer_send_lsp_did_change_request(lsp, buffer, pos, 0, str); + if (buffer->store_undo_events) { BufferEdit *last_edit = arr_lastp(buffer->undo_history); i64 where_in_last_edit = last_edit ? buffer_pos_diff(buffer, last_edit->pos, pos) : -1; @@ -1650,6 +1710,10 @@ void buffer_delete_chars_at_pos(TextBuffer *buffer, BufferPos pos, i64 nchars_) // Not doing this might also cause other bugs, best to keep it here just in case. nchars = (u32)buffer_get_text_at_pos(buffer, pos, NULL, nchars); + LSP *lsp = buffer_lsp(buffer); + if (lsp) + buffer_send_lsp_did_change_request(lsp, buffer, pos, nchars, (String32){0}); + if (buffer->store_undo_events) { // we need to make sure the undo history keeps track of the edit. // we will either combine it with the previous BufferEdit, or create a new @@ -2040,7 +2104,6 @@ void buffer_paste(TextBuffer *buffer) { } } - // if an error occurs, buffer is left untouched (except for the error field) and the function returns false. Status buffer_load_file(TextBuffer *buffer, char const *filename) { FILE *fp = fopen(filename, "rb"); @@ -2059,88 +2122,93 @@ Status buffer_load_file(TextBuffer *buffer, char const *filename) { buffer_seterr(buffer, "File too big (size: %zu).", file_size); success = false; } else { - u8 *file_contents = buffer_calloc(buffer, 1, file_size); - if (file_contents) { - lines_capacity = 4; - lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines - if (lines) { - nlines = 1; - size_t bytes_read = fread(file_contents, 1, file_size, fp); - if (bytes_read == file_size) { - char32_t c = 0; - for (u8 *p = file_contents, *end = p + file_size; p != end; ) { - if (*p == '\r' && p != end-1 && p[1] == '\n') { - // CRLF line endings - p += 2; - c = '\n'; - } else { - size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); - if (n == 0) { - // null character - c = 0; - ++p; - } else if (n >= (size_t)(-2)) { - // invalid UTF-8 - success = false; - buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); - break; - } else { - p += n; - } - } - if (c == '\n') { - if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) - ++nlines; - } else { - u32 line_idx = nlines - 1; - Line *line = &lines[line_idx]; - buffer_line_append_char(buffer, line, c); - } - } + u8 *file_contents = buffer_calloc(buffer, 1, file_size + 1); + lines_capacity = 4; + lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines + nlines = 1; + size_t bytes_read = fread(file_contents, 1, file_size, fp); + if (bytes_read == file_size) { + char32_t c = 0; + for (u8 *p = file_contents, *end = p + file_size; p != end; ) { + if (*p == '\r' && p != end-1 && p[1] == '\n') { + // CRLF line endings + p += 2; + c = '\n'; } else { - success = false; + size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); + if (n == 0) { + // null character + c = 0; + ++p; + } else if (n >= (size_t)(-2)) { + // invalid UTF-8 + success = false; + buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); + break; + } else { + p += n; + } } - if (!success) { - // something went wrong; we need to free all the memory we used - for (u32 i = 0; i < nlines; ++i) - buffer_line_free(&lines[i]); - free(lines); + if (c == '\n') { + if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) + ++nlines; + } else { + u32 line_idx = nlines - 1; + Line *line = &lines[line_idx]; + buffer_line_append_char(buffer, line, c); } } - free(file_contents); } - if (ferror(fp)) { + if (ferror(fp)) { buffer_seterr(buffer, "Error reading from file."); success = false; } + if (!success) { + // something went wrong; we need to free all the memory we used + for (u32 i = 0; i < nlines; ++i) + buffer_line_free(&lines[i]); + free(lines); + } + + if (success) { + char *filename_copy = buffer_strdup(buffer, filename); + if (!filename_copy) success = false; + if (success) { + // everything is good + buffer_clear(buffer); + buffer->lines = lines; + buffer->nlines = nlines; + buffer->frame_earliest_line_modified = 0; + buffer->frame_latest_line_modified = nlines - 1; + buffer->lines_capacity = lines_capacity; + buffer->filename = filename_copy; + buffer->last_write_time = time_last_modified(buffer->filename); + if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { + // can't write to this file; make the buffer view only. + buffer->view_only = true; + } + } + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + // send didOpen + LSPRequest request = {.type = LSP_REQUEST_DID_OPEN}; + LSPRequestDidOpen *open = &request.data.open; + open->file_contents = (char *)file_contents; + open->document = lsp_document_id(lsp, filename); + open->language = buffer_language(buffer); + lsp_send_request(lsp, &request); + file_contents = NULL; // don't free + } + } + + free(file_contents); } - if (fclose(fp) != 0) { - buffer_seterr(buffer, "Error closing file."); - success = false; - } + fclose(fp); } else { buffer_seterr(buffer, "Couldn't open file %s: %s.", filename, strerror(errno)); success = false; } - if (success) { - char *filename_copy = buffer_strdup(buffer, filename); - if (!filename_copy) success = false; - if (success) { - // everything is good - buffer_clear(buffer); - buffer->lines = lines; - buffer->nlines = nlines; - buffer->frame_earliest_line_modified = 0; - buffer->frame_latest_line_modified = nlines - 1; - buffer->lines_capacity = lines_capacity; - buffer->filename = filename_copy; - buffer->last_write_time = time_last_modified(buffer->filename); - if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { - // can't write to this file; make the buffer view only. - buffer->view_only = true; - } - } - } return success; } @@ -2284,11 +2352,11 @@ u32 buffer_last_rendered_line(TextBuffer *buffer) { // returns true if the buffer "used" this event bool buffer_handle_click(Ted *ted, TextBuffer *buffer, v2 click, u8 times) { BufferPos buffer_pos; - if (ted->autocomplete) { - if (rect_contains_point(ted->autocomplete_rect, click)) + if (ted->autocomplete.open) { + if (rect_contains_point(ted->autocomplete.rect, click)) return false; // don't look at clicks in the autocomplete menu else - ted->autocomplete = false; // close autocomplete menu if user clicks outside of it + autocomplete_close(ted); // close autocomplete menu if user clicks outside of it } if (buffer_pixels_to_pos(buffer, click, &buffer_pos)) { // user clicked on buffer @@ -13,7 +13,7 @@ static void build_stop(Ted *ted) { } arr_clear(ted->build_queue); if (ted->active_buffer == &ted->build_buffer) { - ted->active_buffer = NULL; + ted_switch_to_buffer(ted, NULL); ted_reset_active_buffer(ted); } } diff --git a/colors.c b/colors.c new file mode 100644 index 0000000..a684508 --- /dev/null +++ b/colors.c @@ -0,0 +1,69 @@ + +static ColorSetting color_setting_from_str(char const *str) { + // @OPTIMIZE: sort color_names, binary search + for (int i = 0; i < COLOR_COUNT; ++i) { + ColorName const *n = &color_names[i]; + if (streq(n->name, str)) + return n->setting; + } + return COLOR_UNKNOWN; +} + +static char const *color_setting_to_str(ColorSetting s) { + for (int i = 0; i < COLOR_COUNT; ++i) { + ColorName const *n = &color_names[i]; + if (n->setting == s) + return n->name; + } + return "???"; +} + +// converts #rrggbb/#rrggbbaa to a color. returns false if it's not in the right format. +static Status color_from_str(char const *str, u32 *color) { + uint r = 0, g = 0, b = 0, a = 0xff; + bool success = false; + switch (strlen(str)) { + case 4: + success = sscanf(str, "#%01x%01x%01x", &r, &g, &b) == 3; + // extend single hex digit to double hex digit + r |= r << 4; + g |= g << 4; + b |= b << 4; + break; + case 5: + success = sscanf(str, "#%01x%01x%01x%01x", &r, &g, &b, &a) == 4; + r |= r << 4; + g |= g << 4; + b |= b << 4; + a |= a << 4; + break; + case 7: + success = sscanf(str, "#%02x%02x%02x", &r, &g, &b) == 3; + break; + case 9: + success = sscanf(str, "#%02x%02x%02x%02x", &r, &g, &b, &a) == 4; + break; + } + if (!success || r > 0xff || g > 0xff || b > 0xff || a > 0xff) + return false; + if (color) + *color = (u32)r << 24 | (u32)g << 16 | (u32)b << 8 | (u32)a; + return true; +} + + +static ColorSetting color_for_symbol_kind(SymbolKind kind) { + switch (kind) { + case SYMBOL_CONSTANT: + return COLOR_CONSTANT; + case SYMBOL_TYPE: + case SYMBOL_FIELD: + case SYMBOL_VARIABLE: + case SYMBOL_OTHER: + case SYMBOL_FUNCTION: + return COLOR_TEXT; + case SYMBOL_KEYWORD: + return COLOR_KEYWORD; + } + return COLOR_TEXT; +} @@ -1,4 +1,4 @@ -ENUM_U16 { +typedef enum { COLOR_UNKNOWN, COLOR_TEXT, @@ -24,6 +24,8 @@ ENUM_U16 { COLOR_ACTIVE_TAB_HL, COLOR_SELECTED_TAB_HL, COLOR_FIND_HL, + + COLOR_AUTOCOMPLETE_BORDER, COLOR_YES, COLOR_NO, @@ -43,7 +45,7 @@ ENUM_U16 { COLOR_COUNT -} ENUM_U16_END(ColorSetting); +} ColorSetting; typedef struct { ColorSetting setting; @@ -82,6 +84,7 @@ static ColorName const color_names[] = { {COLOR_STRING, "string"}, {COLOR_CHARACTER, "character"}, {COLOR_CONSTANT, "constant"}, + {COLOR_AUTOCOMPLETE_BORDER, "autocomplete-border"}, {COLOR_YES, "yes"}, {COLOR_NO, "no"}, {COLOR_CANCEL, "cancel"}, @@ -91,56 +94,3 @@ static ColorName const color_names[] = { }; static_assert_if_possible(arr_count(color_names) == COLOR_COUNT) - -static ColorSetting color_setting_from_str(char const *str) { - // @OPTIMIZE: sort color_names, binary search - for (int i = 0; i < COLOR_COUNT; ++i) { - ColorName const *n = &color_names[i]; - if (streq(n->name, str)) - return n->setting; - } - return COLOR_UNKNOWN; -} - -static char const *color_setting_to_str(ColorSetting s) { - for (int i = 0; i < COLOR_COUNT; ++i) { - ColorName const *n = &color_names[i]; - if (n->setting == s) - return n->name; - } - return "???"; -} - -// converts #rrggbb/#rrggbbaa to a color. returns false if it's not in the right format. -static Status color_from_str(char const *str, u32 *color) { - uint r = 0, g = 0, b = 0, a = 0xff; - bool success = false; - switch (strlen(str)) { - case 4: - success = sscanf(str, "#%01x%01x%01x", &r, &g, &b) == 3; - // extend single hex digit to double hex digit - r |= r << 4; - g |= g << 4; - b |= b << 4; - break; - case 5: - success = sscanf(str, "#%01x%01x%01x%01x", &r, &g, &b, &a) == 4; - r |= r << 4; - g |= g << 4; - b |= b << 4; - a |= a << 4; - break; - case 7: - success = sscanf(str, "#%02x%02x%02x", &r, &g, &b) == 3; - break; - case 9: - success = sscanf(str, "#%02x%02x%02x%02x", &r, &g, &b, &a) == 4; - break; - } - if (!success || r > 0xff || g > 0xff || b > 0xff || a > 0xff) - return false; - if (color) - *color = (u32)r << 24 | (u32)g << 16 | (u32)b << 8 | (u32)a; - return true; -} - @@ -127,7 +127,7 @@ void command_execute(Ted *ted, Command c, i64 argument) { buffer = &ted->line_buffer; ted_switch_to_buffer(ted, buffer); buffer_select_all(buffer); - } else if (ted->autocomplete) { + } else if (ted->autocomplete.open) { autocomplete_select_cursor_completion(ted); } else if (buffer) { if (buffer->selection) @@ -263,14 +263,14 @@ void command_execute(Ted *ted, Command c, i64 argument) { } break; case CMD_AUTOCOMPLETE: - if (ted->autocomplete) - ++ted->autocomplete_cursor; + if (ted->autocomplete.open) + autocomplete_next(ted); else autocomplete_open(ted); break; case CMD_AUTOCOMPLETE_BACK: - if (ted->autocomplete) - --ted->autocomplete_cursor; + if (ted->autocomplete.open) + autocomplete_prev(ted); break; case CMD_UNDO: @@ -376,8 +376,8 @@ void command_execute(Ted *ted, Command c, i64 argument) { if (*ted->error_shown) { // dismiss error box *ted->error_shown = '\0'; - } else if (ted->autocomplete) { - ted->autocomplete = false; + } else if (ted->autocomplete.open) { + autocomplete_close(ted); } else if (ted->menu) { menu_escape(ted); } else { @@ -1,34 +1,19 @@ -#ifndef ARR_C_ -#define ARR_C_ -/* -This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to <http://unlicense.org/> +/* VARIOUS DATA STRUCTURES +- dynamic array +- string builder +- string hash table */ + // functions in this file suffixed with _ are not meant to be used outside here, unless you // know what you're doing // IMPORTANT NOTE: If you are using this with structures containing `long double`s, do // #define ARR_LONG_DOUBLE // before including this file +// ( otherwise the long doubles will not be aligned. +// this does mean that arrays waste 8 bytes of memory. +// which isnt important unless you're making a lot of arrays.) #include <stddef.h> typedef union { @@ -56,8 +41,12 @@ static inline ArrHeader *arr_hdr_(void *arr) { return (ArrHeader *)((char *)arr - offsetof(ArrHeader, data)); } -static inline u32 arr_len(void *arr) { - return arr ? arr_hdr_(arr)->len : 0; +static inline u32 arr_len(const void *arr) { + return arr ? arr_hdr_((void*)arr)->len : 0; +} + +static inline u32 arr_cap(void *arr) { + return arr ? arr_hdr_(arr)->cap : 0; } static inline unsigned arr_lenu(void *arr) { @@ -114,6 +103,7 @@ static void arr_reserve_(void **arr, size_t member_size, size_t n) { if (!*arr) { // create a new array with capacity n+1 + // why n+1? i dont know i wrote this a while ago ArrHeader *hdr = calloc(1, sizeof(ArrHeader) + (n+1) * member_size); if (hdr) { hdr->cap = (u32)n+1; @@ -274,4 +264,82 @@ static void arr_test(void) { } #endif -#endif // ARR_C_ +typedef struct { + // dynamic array, including a null byte. + char *str; +} StrBuilder; + +void str_builder_create(StrBuilder *builder) { + memset(builder, 0, sizeof *builder); + arr_add(builder->str, 0); +} + +StrBuilder str_builder_new(void) { + StrBuilder ret = {0}; + str_builder_create(&ret); + return ret; +} + +void str_builder_free(StrBuilder *builder) { + arr_free(builder->str); +} + +void str_builder_clear(StrBuilder *builder) { + str_builder_free(builder); + str_builder_create(builder); +} + +void str_builder_append(StrBuilder *builder, const char *s) { + assert(builder->str); + + size_t s_len = strlen(s); + size_t prev_size = arr_len(builder->str); + size_t prev_len = prev_size - 1; // null terminator + // note: this zeroes the newly created elements, so we have a new null terminator + arr_set_len(builder->str, prev_size + s_len); + memcpy(builder->str + prev_len, s, s_len); +} + +void str_builder_appendf(StrBuilder *builder, PRINTF_FORMAT_STRING const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3); +void str_builder_appendf(StrBuilder *builder, const char *fmt, ...) { + // idk if you can always just pass NULL to vsnprintf + va_list args; + char fakebuf[2] = {0}; + va_start(args, fmt); + int ret = vsnprintf(fakebuf, 1, fmt, args); + va_end(args); + + if (ret < 0) return; // bad format or something + u32 n = (u32)ret; + + size_t prev_size = arr_len(builder->str); + size_t prev_len = prev_size - 1; // null terminator + arr_set_len(builder->str, prev_size + n); + va_start(args, fmt); + vsnprintf(builder->str + prev_len, n + 1, fmt, args); + va_end(args); +} + +// append n null bytes. +void str_builder_append_null(StrBuilder *builder, size_t n) { + arr_set_len(builder->str, arr_len(builder->str) + n); +} + +u32 str_builder_len(StrBuilder *builder) { + assert(builder->str); + return arr_len(builder->str) - 1; +} + +char *str_builder_get_ptr(StrBuilder *builder, size_t index) { + assert(index <= str_builder_len(builder)); + return &builder->str[index]; +} + +void str_builder_shrink(StrBuilder *builder, size_t new_len) { + if (new_len > str_builder_len(builder)) { + assert(0); + return; + } + arr_set_len(builder->str, new_len + 1); +} + @@ -0,0 +1,713 @@ +// 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"; + } +} + +static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val); + +// 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'; +} + +static 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: { + JSONString string = value.val.string; + printf("\"%.*s\"", + (int)string.len, + json->text + string.pos); + } break; + case JSON_ARRAY: { + JSONArray array = value.val.array; + printf("["); + for (u32 i = 0; i < array.len; ++i) { + json_debug_print_value(json, json->values[array.elements + i]); + printf(", "); + } + printf("]"); + } break; + case JSON_OBJECT: { + JSONObject obj = value.val.object; + 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("}"); + } 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}; +} + +// returns (JSONString){0} (which is interpreted as an empty string) if `name` does +// not exist or is not a string. +JSONString json_object_get_string(const JSON *json, JSONObject object, const char *name) { + JSONValue value = json_object_get(json, object, name); + if (value.type == JSON_STRING) { + return value.val.string; + } else { + return (JSONString){0}; + } +} + +// returns (JSONObject){0} (which is interpreted as an empty object) if `name` does +// not exist or is not an object. +JSONObject json_object_get_object(const JSON *json, JSONObject object, const char *name) { + JSONValue value = json_object_get(json, object, name); + if (value.type == JSON_OBJECT) { + return value.val.object; + } else { + return (JSONObject){0}; + } +} + +// returns (JSONArray){0} (which is interpreted as an empty array) if `name` does +// not exist or is not an array. +JSONArray json_object_get_array(const JSON *json, JSONObject object, const char *name) { + JSONValue value = json_object_get(json, object, name); + if (value.type == JSON_ARRAY) { + return value.val.array; + } else { + return (JSONArray){0}; + } +} + +// returns NaN if `name` does not exist or is not a number. +double json_object_get_number(const JSON *json, JSONObject object, const char *name) { + JSONValue value = json_object_get(json, object, name); + if (value.type == JSON_NUMBER) { + return value.val.number; + } else { + return NAN; + } +} + +JSONValue json_array_get(const JSON *json, JSONArray array, u64 i) { + if (i < array.len) { + return json->values[array.elements + i]; + } + return (JSONValue){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 new file mode 100644 index 0000000..947feb9 --- /dev/null +++ b/lsp-parse.c @@ -0,0 +1,337 @@ +static WarnUnusedResult bool lsp_expect_type(LSP *lsp, JSONValue value, JSONValueType type, const char *what) { + if (value.type != type) { + lsp_set_error(lsp, "Expected %s for %s, got %s", + json_type_to_str(type), + what, + json_type_to_str(value.type)); + return false; + } + return true; +} + +static WarnUnusedResult bool lsp_expect_object(LSP *lsp, JSONValue value, const char *what) { + return lsp_expect_type(lsp, value, JSON_OBJECT, what); +} + +static WarnUnusedResult bool lsp_expect_array(LSP *lsp, JSONValue value, const char *what) { + return lsp_expect_type(lsp, value, JSON_ARRAY, what); +} + +static WarnUnusedResult bool lsp_expect_string(LSP *lsp, JSONValue value, const char *what) { + return lsp_expect_type(lsp, value, JSON_STRING, what); +} + +static WarnUnusedResult bool lsp_expect_number(LSP *lsp, JSONValue value, const char *what) { + 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); + json_string_get(json, string, response->string_data + offset, string.len + 1); + return (LSPString){ + .offset = offset + }; +} + +static int completion_qsort_cmp(void *context, const void *av, const void *bv) { + const LSPResponse *response = context; + const LSPCompletionItem *a = av, *b = bv; + const char *a_sort_text = lsp_response_string(response, a->sort_text); + const char *b_sort_text = lsp_response_string(response, b->sort_text); + int sort_text_cmp = strcmp(a_sort_text, b_sort_text); + if (sort_text_cmp != 0) + return sort_text_cmp; + // for some reason, rust-analyzer outputs identical sortTexts + // i have no clue what that means. + // the LSP "specification" is not very specific. + // we'll sort by label in this case. + // this is what VSCode seems to do. + // i hate microsofot. + const char *a_label = lsp_response_string(response, a->label); + const char *b_label = lsp_response_string(response, b->label); + return strcmp(a_label, b_label); +} + +static bool parse_position(LSP *lsp, const JSON *json, JSONValue pos_value, LSPPosition *pos) { + if (!lsp_expect_object(lsp, pos_value, "document position")) + return false; + JSONObject pos_object = pos_value.val.object; + JSONValue line = json_object_get(json, pos_object, "line"); + JSONValue character = json_object_get(json, pos_object, "character"); + if (!lsp_expect_number(lsp, line, "document line number") + || !lsp_expect_number(lsp, character, "document column number")) + return false; + pos->line = (u32)line.val.number; + pos->character = (u32)line.val.number; + return true; +} + +static bool parse_range(LSP *lsp, const JSON *json, JSONValue range_value, LSPRange *range) { + if (!lsp_expect_object(lsp, range_value, "document range")) + return false; + JSONObject range_object = range_value.val.object; + JSONValue start = json_object_get(json, range_object, "start"); + JSONValue end = json_object_get(json, range_object, "end"); + return parse_position(lsp, json, start, &range->start) + && parse_position(lsp, json, end, &range->end); +} + +static bool parse_completion(LSP *lsp, const JSON *json, LSPResponse *response) { + // deal with textDocument/completion response. + // result: CompletionItem[] | CompletionList | null + LSPResponseCompletion *completion = &response->data.completion; + + JSONValue result = json_get(json, "result"); + JSONValue items_value = {0}; + switch (result.type) { + case JSON_NULL: + // no completions + return true; + case JSON_ARRAY: + items_value = result; + break; + case JSON_OBJECT: + items_value = json_object_get(json, result.val.object, "items"); + break; + default: + lsp_set_error(lsp, "Weird result type for textDocument/completion response: %s.", json_type_to_str(result.type)); + break; + } + + if (!lsp_expect_array(lsp, items_value, "completion list")) + return false; + + JSONArray items = items_value.val.array; + + arr_set_len(completion->items, items.len); + + for (u32 i = 0; i < items.len; ++i) { + LSPCompletionItem *item = &completion->items[i]; + + JSONValue item_value = json_array_get(json, items, i); + if (!lsp_expect_object(lsp, item_value, "completion list")) + return false; + JSONObject item_object = item_value.val.object; + + JSONValue label_value = json_object_get(json, item_object, "label"); + if (!lsp_expect_string(lsp, label_value, "completion label")) + return false; + JSONString label = label_value.val.string; + item->label = lsp_response_add_json_string(response, json, label); + + // defaults + item->sort_text = item->label; + item->filter_text = item->label; + item->text_edit = (LSPTextEdit) { + .type = LSP_TEXT_EDIT_PLAIN, + .at_cursor = true, + .range = {0}, + .new_text = item->label + }; + + double kind = json_object_get_number(json, item_object, "kind"); + if (isnormal(kind) && kind >= LSP_COMPLETION_KIND_MIN && kind <= LSP_COMPLETION_KIND_MAX) { + item->kind = (LSPCompletionKind)kind; + } + + JSONString sort_text = json_object_get_string(json, item_object, "sortText"); + if (sort_text.pos) { + // LSP allows using a different string for sorting. + item->sort_text = lsp_response_add_json_string(response, json, sort_text); + } + + JSONString filter_text = json_object_get_string(json, item_object, "filterText"); + if (filter_text.pos) { + // LSP allows using a different string for filtering. + item->filter_text = lsp_response_add_json_string(response, json, filter_text); + } + + double edit_type = json_object_get_number(json, item_object, "insertTextFormat"); + if (!isnan(edit_type)) { + if (edit_type != LSP_TEXT_EDIT_PLAIN && edit_type != LSP_TEXT_EDIT_SNIPPET) { + // maybe in the future more edit types will be added. + // probably they'll have associated capabilities, but I think it's best to just ignore unrecognized types + debug_println("Bad InsertTextFormat: %g", edit_type); + edit_type = LSP_TEXT_EDIT_PLAIN; + } + item->text_edit.type = (LSPTextEditType)edit_type; + } + + + JSONString detail_text = json_object_get_string(json, item_object, "detail"); + if (detail_text.pos) { + item->detail = lsp_response_add_json_string(response, json, detail_text); + } + + // @TODO(eventually): additionalTextEdits + // (try to find a case where this comes up) + + // what should happen when this completion is selected? + JSONValue text_edit_value = json_object_get(json, item_object, "textEdit"); + if (text_edit_value.type == JSON_OBJECT) { + JSONObject text_edit = text_edit_value.val.object; + item->text_edit.at_cursor = false; + + JSONValue range = json_object_get(json, text_edit, "range"); + if (!parse_range(lsp, json, range, &item->text_edit.range)) + return false; + + JSONValue new_text_value = json_object_get(json, text_edit, "newText"); + if (!lsp_expect_string(lsp, new_text_value, "completion newText")) + return false; + item->text_edit.new_text = lsp_response_add_json_string(response, + json, new_text_value.val.string); + } else { + // not using textEdit. check insertText. + JSONValue insert_text_value = json_object_get(json, item_object, "insertText"); + if (insert_text_value.type == JSON_STRING) { + // string which will be inserted if this completion is selected + item->text_edit.new_text = lsp_response_add_json_string(response, + json, insert_text_value.val.string); + } + } + + } + + qsort_with_context(completion->items, items.len, sizeof *completion->items, + completion_qsort_cmp, response); + + return true; +} + +static bool parse_server2client_request(LSP *lsp, JSON *json, LSPRequest *request) { + JSONValue method_value = json_get(json, "method"); + if (!lsp_expect_string(lsp, method_value, "request method")) + return false; + + char method[64] = {0}; + json_string_get(json, method_value.val.string, method, sizeof method); + + if (streq(method, "window/showMessage")) { + request->type = LSP_REQUEST_SHOW_MESSAGE; + goto window_message; + } else if (streq(method, "window/logMessage")) { + request->type = LSP_REQUEST_LOG_MESSAGE; + window_message:; + JSONValue type = json_get(json, "params.type"); + JSONValue message = json_get(json, "params.message"); + if (!lsp_expect_number(lsp, type, "MessageType")) + return false; + if (!lsp_expect_string(lsp, message, "message string")) + return false; + + int mtype = (int)type.val.number; + if (mtype < 1 || mtype > 4) { + lsp_set_error(lsp, "Bad MessageType: %g", type.val.number); + return false; + } + + LSPRequestMessage *m = &request->data.message; + m->type = (LSPWindowMessageType)mtype; + m->message = json_string_get_alloc(json, message.val.string); + return true; + } else if (str_has_prefix(method, "$/")) { + // we can safely ignore this + } else { + lsp_set_error(lsp, "Unrecognized request method: %s", method); + } + return false; +} + + +static void process_message(LSP *lsp, JSON *json) { + + #if 0 + printf("\x1b[3m"); + json_debug_print(json); + printf("\x1b[0m\n"); + #endif + JSONValue id_value = json_get(json, "id"); + + // get the request associated with this (if any) + LSPRequest response_to = {0}; + if (id_value.type == JSON_NUMBER) { + u64 id = (u64)id_value.val.number; + arr_foreach_ptr(lsp->requests_sent, LSPRequest, req) { + if (req->id == id) { + response_to = *req; + arr_remove(lsp->requests_sent, (u32)(req - lsp->requests_sent)); + break; + } + } + } + + JSONValue error = json_get(json, "error.message"); + if (error.type == JSON_STRING) { + char err[256] = {0}; + json_string_get(json, error.val.string, err, sizeof err);; + + if (streq(err, "waiting for cargo metadata or cargo check")) { + // fine. be that way. i'll resend the goddamn request. + // i'll keep bombarding you with requests. + // maybe next time you should abide by the standard and only send an initialize response when youre actually ready to handle my requests. fuck you. + if (response_to.type) { + lsp_send_request(lsp, &response_to); + // don't free + memset(&response_to, 0, sizeof response_to); + } + } else { + lsp_set_error(lsp, "%s", err); + } + goto ret; + } + + JSONValue result = json_get(json, "result"); + if (result.type != JSON_UNDEFINED) { + if (response_to.type == LSP_REQUEST_INITIALIZE) { + // it's the response to our initialize request! + // let's send back an "initialized" request (notification) because apparently + // that's something we need to do. + LSPRequest initialized = { + .type = LSP_REQUEST_INITIALIZED, + .data = {0}, + }; + write_request(lsp, &initialized); + // we can now send requests which have nothing to do with initialization + lsp->initialized = true; + } else { + LSPResponse response = {0}; + bool success = false; + response.request = response_to; + switch (response_to.type) { + case LSP_REQUEST_COMPLETION: + success = parse_completion(lsp, json, &response); + break; + default: + // it's some response we don't care about + break; + } + if (success) { + SDL_LockMutex(lsp->messages_mutex); + LSPMessage *message = arr_addp(lsp->messages); + message->type = LSP_RESPONSE; + message->u.response = response; + SDL_UnlockMutex(lsp->messages_mutex); + response_to.type = 0; // don't free + } else { + lsp_response_free(&response); + } + } + } else if (json_has(json, "method")) { + LSPRequest request = {0}; + if (parse_server2client_request(lsp, json, &request)) { + SDL_LockMutex(lsp->messages_mutex); + LSPMessage *message = arr_addp(lsp->messages); + message->type = LSP_REQUEST; + message->u.request = request; + SDL_UnlockMutex(lsp->messages_mutex); + } + } else { + lsp_set_error(lsp, "Bad message from server (no result, no method)."); + } + ret: + lsp_request_free(&response_to); + json_free(json); + +} diff --git a/lsp-write.c b/lsp-write.c new file mode 100644 index 0000000..28b19d3 --- /dev/null +++ b/lsp-write.c @@ -0,0 +1,342 @@ + +static const char *lsp_language_id(Language lang) { + switch (lang) { + case LANG_CONFIG: + case LANG_TED_CFG: + case LANG_NONE: + return "text"; + case LANG_C: + return "c"; + case LANG_CPP: + return "cpp"; + case LANG_JAVA: + return "java"; + case LANG_JAVASCRIPT: + return "javascript"; + case LANG_MARKDOWN: + return "markdown"; + case LANG_GO: + return "go"; + case LANG_RUST: + return "rust"; + case LANG_PYTHON: + return "python"; + case LANG_HTML: + return "html"; + case LANG_TEX: + return "latex"; + case LANG_COUNT: break; + } + assert(0); + return "text"; +} + + +typedef struct { + LSP *lsp; + StrBuilder builder; + bool is_first; +} JSONWriter; + +static JSONWriter json_writer_new(LSP *lsp) { + return (JSONWriter){ + .lsp = lsp, + .builder = str_builder_new(), + .is_first = true + }; +} + +static void write_obj_start(JSONWriter *o) { + str_builder_append(&o->builder, "{"); + o->is_first = true; +} + +static void write_obj_end(JSONWriter *o) { + str_builder_append(&o->builder, "}"); + o->is_first = false; +} + +static void write_arr_start(JSONWriter *o) { + str_builder_append(&o->builder, "["); + o->is_first = true; +} + +static void write_arr_end(JSONWriter *o) { + str_builder_append(&o->builder, "]"); + o->is_first = false; +} + +static void write_arr_elem(JSONWriter *o) { + if (o->is_first) { + o->is_first = false; + } else { + str_builder_append(&o->builder, ","); + } +} + +static void write_escaped(JSONWriter *o, const char *string) { + StrBuilder *b = &o->builder; + size_t output_index = str_builder_len(b); + size_t capacity = 2 * strlen(string) + 1; + // append a bunch of null bytes which will hold the escaped string + str_builder_append_null(b, capacity); + char *out = str_builder_get_ptr(b, output_index); + // do the escaping + size_t length = json_escape_to(out, capacity, string); + // shrink down to just the escaped text + str_builder_shrink(&o->builder, output_index + length); +} + +static void write_string(JSONWriter *o, const char *string) { + str_builder_append(&o->builder, "\""); + write_escaped(o, string); + str_builder_append(&o->builder, "\""); +} + +static void write_key(JSONWriter *o, const char *key) { + // NOTE: no keys in the LSP spec need escaping. + str_builder_appendf(&o->builder, "%s\"%s\":", o->is_first ? "" : ",", key); + o->is_first = false; +} + +static void write_key_obj_start(JSONWriter *o, const char *key) { + write_key(o, key); + write_obj_start(o); +} + +static void write_key_arr_start(JSONWriter *o, const char *key) { + write_key(o, key); + write_arr_start(o); +} + +static void write_number(JSONWriter *o, double number) { + str_builder_appendf(&o->builder, "%g", number); +} + +static void write_key_number(JSONWriter *o, const char *key, double number) { + write_key(o, key); + write_number(o, number); +} + +static void write_null(JSONWriter *o) { + str_builder_append(&o->builder, "null"); +} + +static void write_key_null(JSONWriter *o, const char *key) { + write_key(o, key); + write_null(o); +} + +static void write_key_string(JSONWriter *o, const char *key, const char *s) { + write_key(o, key); + write_string(o, s); +} + +static void write_file_uri(JSONWriter *o, DocumentID document) { + const char *path = o->lsp->document_paths[document]; + str_builder_append(&o->builder, "\"file:///"); + write_escaped(o, path); + str_builder_append(&o->builder, "\""); +} + +static void write_key_file_uri(JSONWriter *o, const char *key, DocumentID document) { + write_key(o, key); + write_file_uri(o, document); +} + +static void write_position(JSONWriter *o, LSPPosition position) { + write_obj_start(o); + write_key_number(o, "line", (double)position.line); + write_key_number(o, "character", (double)position.character); + write_obj_end(o); +} + +static void write_key_position(JSONWriter *o, const char *key, LSPPosition position) { + write_key(o, key); + write_position(o, position); +} + +static void write_range(JSONWriter *o, LSPRange range) { + write_obj_start(o); + write_key_position(o, "start", range.start); + write_key_position(o, "end", range.end); + write_obj_end(o); +} + +static void write_key_range(JSONWriter *o, const char *key, LSPRange range) { + write_key(o, key); + write_range(o, range); +} + +static const char *lsp_request_method(LSPRequest *request) { + switch (request->type) { + case LSP_REQUEST_NONE: break; + case LSP_REQUEST_SHOW_MESSAGE: + return "window/showMessage"; + case LSP_REQUEST_LOG_MESSAGE: + return "window/logMessage"; + case LSP_REQUEST_INITIALIZE: + return "initialize"; + case LSP_REQUEST_INITIALIZED: + return "initialized"; + case LSP_REQUEST_DID_OPEN: + return "textDocument/didOpen"; + case LSP_REQUEST_DID_CLOSE: + return "textDocument/didClose"; + case LSP_REQUEST_DID_CHANGE: + return "textDocument/didChange"; + case LSP_REQUEST_COMPLETION: + return "textDocument/completion"; + case LSP_REQUEST_SHUTDOWN: + return "shutdown"; + case LSP_REQUEST_EXIT: + return "exit"; + } + assert(0); + return "$/ignore"; +} + +static bool request_type_is_notification(LSPRequestType type) { + switch (type) { + case LSP_REQUEST_NONE: break; + case LSP_REQUEST_INITIALIZED: + case LSP_REQUEST_EXIT: + case LSP_REQUEST_DID_OPEN: + case LSP_REQUEST_DID_CLOSE: + case LSP_REQUEST_DID_CHANGE: + return true; + case LSP_REQUEST_INITIALIZE: + case LSP_REQUEST_SHUTDOWN: + case LSP_REQUEST_SHOW_MESSAGE: + case LSP_REQUEST_LOG_MESSAGE: + case LSP_REQUEST_COMPLETION: + return false; + } + assert(0); + return false; +} + +static void write_request(LSP *lsp, LSPRequest *request) { + JSONWriter writer = json_writer_new(lsp); + JSONWriter *o = &writer; + + u32 max_header_size = 64; + // this is where our header will go + str_builder_append_null(&o->builder, max_header_size); + + write_obj_start(o); + write_key_string(o, "jsonrpc", "2.0"); + + bool is_notification = request_type_is_notification(request->type); + if (!is_notification) { + u32 id = lsp->request_id++; + request->id = id; + write_key_number(o, "id", id); + } + write_key_string(o, "method", lsp_request_method(request)); + + switch (request->type) { + case LSP_REQUEST_NONE: + // these are server-to-client-only requests + case LSP_REQUEST_SHOW_MESSAGE: + case LSP_REQUEST_LOG_MESSAGE: + assert(0); + break; + case LSP_REQUEST_INITIALIZED: + case LSP_REQUEST_SHUTDOWN: + case LSP_REQUEST_EXIT: + // no params + break; + case LSP_REQUEST_INITIALIZE: { + write_key_obj_start(o, "params"); + write_key_number(o, "processId", process_get_id()); + write_key_obj_start(o, "capabilities"); + write_obj_end(o); + write_key_null(o, "rootUri"); + write_key_null(o, "workspaceFolders"); + write_obj_end(o); + } break; + case LSP_REQUEST_DID_OPEN: { + const LSPRequestDidOpen *open = &request->data.open; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_file_uri(o, "uri", open->document); + write_key_string(o, "languageId", lsp_language_id(open->language)); + write_key_number(o, "version", 1); + write_key_string(o, "text", open->file_contents); + write_obj_end(o); + write_obj_end(o); + } break; + case LSP_REQUEST_DID_CLOSE: { + const LSPRequestDidClose *close = &request->data.close; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_file_uri(o, "uri", close->document); + write_obj_end(o); + write_obj_end(o); + } break; + case LSP_REQUEST_DID_CHANGE: { + LSPRequestDidChange *change = &request->data.change; + static unsigned long long version_number = 1; // @TODO @TEMPORARY + ++version_number; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_number(o, "version", (double)version_number); + write_key_file_uri(o, "uri", change->document); + write_obj_end(o); + write_key_arr_start(o, "contentChanges"); + arr_foreach_ptr(change->changes, LSPDocumentChangeEvent, event) { + write_arr_elem(o); + write_obj_start(o); + write_key_range(o, "range", event->range); + write_key_string(o, "text", event->text ? event->text : ""); + write_obj_end(o); + } + write_arr_end(o); + write_obj_end(o); + } break; + case LSP_REQUEST_COMPLETION: { + const LSPRequestCompletion *completion = &request->data.completion; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_file_uri(o, "uri", completion->position.document); + write_obj_end(o); + write_key_position(o, "position", completion->position.pos); + write_obj_end(o); + } break; + } + + write_obj_end(o); + + StrBuilder builder = o->builder; + + // this is kind of hacky but it lets us send the whole request with one write call. + // probably not *actually* needed. i thought it would help fix an error but it didn't. + size_t content_length = str_builder_len(&builder) - max_header_size; + char content_length_str[32]; + sprintf(content_length_str, "%zu", content_length); + size_t header_size = strlen("Content-Length: \r\n\r\n") + strlen(content_length_str); + char *header = &builder.str[max_header_size - header_size]; + strcpy(header, "Content-Length: "); + strcat(header, content_length_str); + // we specifically DON'T want a null byte + memcpy(header + strlen(header), "\r\n\r\n", 4); + + char *content = header; + #if 0 + printf("\x1b[1m%s\x1b[0m\n",content); + #endif + + // @TODO: does write always write the full amount? probably not. this should be fixed. + process_write(&lsp->process, content, strlen(content)); + + str_builder_free(&builder); + + if (is_notification) { + lsp_request_free(request); + } else { + SDL_LockMutex(lsp->requests_mutex); + arr_add(lsp->requests_sent, *request); + SDL_UnlockMutex(lsp->requests_mutex); + } +} @@ -0,0 +1,413 @@ +static void lsp_request_free(LSPRequest *r); +static void lsp_response_free(LSPResponse *r); + +#define lsp_set_error(lsp, ...) do {\ + SDL_LockMutex(lsp->error_mutex);\ + strbuf_printf(lsp->error, __VA_ARGS__);\ + SDL_UnlockMutex(lsp->error_mutex);\ + } while (0) +#include "lsp-write.c" +#include "lsp-parse.c" + +bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) { + bool has_err = false; + SDL_LockMutex(lsp->error_mutex); + has_err = *lsp->error != '\0'; + if (error_size) + str_cpy(error, error_size, lsp->error); + if (clear) + *lsp->error = '\0'; + SDL_UnlockMutex(lsp->error_mutex); + return has_err; +} + + +static void lsp_document_change_event_free(LSPDocumentChangeEvent *event) { + free(event->text); +} + +static void lsp_request_free(LSPRequest *r) { + switch (r->type) { + case LSP_REQUEST_NONE: + case LSP_REQUEST_INITIALIZE: + case LSP_REQUEST_INITIALIZED: + case LSP_REQUEST_SHUTDOWN: + case LSP_REQUEST_EXIT: + case LSP_REQUEST_COMPLETION: + case LSP_REQUEST_DID_CLOSE: + break; + case LSP_REQUEST_DID_OPEN: { + LSPRequestDidOpen *open = &r->data.open; + free(open->file_contents); + } break; + case LSP_REQUEST_SHOW_MESSAGE: + case LSP_REQUEST_LOG_MESSAGE: + free(r->data.message.message); + break; + case LSP_REQUEST_DID_CHANGE: { + LSPRequestDidChange *c = &r->data.change; + arr_foreach_ptr(c->changes, LSPDocumentChangeEvent, event) + lsp_document_change_event_free(event); + arr_free(c->changes); + } break; + } +} + +static void lsp_response_free(LSPResponse *r) { + arr_free(r->string_data); + switch (r->request.type) { + case LSP_REQUEST_COMPLETION: + arr_free(r->data.completion.items); + break; + default: + break; + } + lsp_request_free(&r->request); +} + +void lsp_message_free(LSPMessage *message) { + switch (message->type) { + case LSP_REQUEST: + lsp_request_free(&message->u.request); + break; + case LSP_RESPONSE: + lsp_response_free(&message->u.response); + break; + } + memset(message, 0, sizeof *message); +} + + +// figure out if data begins with a complete LSP response. +static bool has_response(const char *data, size_t data_len, u64 *p_offset, u64 *p_size) { + const char *content_length = strstr(data, "Content-Length"); + if (!content_length) return false; + const char *p = content_length + strlen("Content-Length"); + if (!p[0] || !p[1] || !p[2]) return false; + p += 2; + size_t size = (size_t)atoll(p); + *p_size = size; + const char *header_end = strstr(content_length, "\r\n\r\n"); + if (!header_end) return false; + header_end += 4; + u64 offset = (u64)(header_end - data); + *p_offset = offset; + return offset + size <= data_len; +} + +void lsp_send_request(LSP *lsp, const LSPRequest *request) { + SDL_LockMutex(lsp->requests_mutex); + arr_add(lsp->requests_client2server, *request); + SDL_UnlockMutex(lsp->requests_mutex); +} + +const char *lsp_response_string(const LSPResponse *response, LSPString string) { + assert(string.offset < arr_len(response->string_data)); + return &response->string_data[string.offset]; +} + +// receive responses/requests/notifications from LSP, up to max_size bytes. +static void lsp_receive(LSP *lsp, size_t max_size) { + + { + // read stderr. if all goes well, we shouldn't get anything over stderr. + char stderr_buf[1024] = {0}; + for (size_t i = 0; i < (max_size + sizeof stderr_buf) / sizeof stderr_buf; ++i) { + ssize_t nstderr = process_read_stderr(&lsp->process, stderr_buf, sizeof stderr_buf - 1); + if (nstderr > 0) { + // uh oh + stderr_buf[nstderr] = '\0'; + fprintf(stderr, "\x1b[1m\x1b[93m%s\x1b[0m", stderr_buf); + } else { + break; + } + } + } + + size_t received_so_far = arr_len(lsp->received_data); + arr_reserve(lsp->received_data, received_so_far + max_size + 1); + long long bytes_read = process_read(&lsp->process, lsp->received_data + received_so_far, max_size); + if (bytes_read <= 0) { + // no data + return; + } + received_so_far += (size_t)bytes_read; + // kind of a hack. this is needed because arr_set_len zeroes the data. + arr_hdr_(lsp->received_data)->len = (u32)received_so_far; + lsp->received_data[received_so_far] = '\0';// null terminate + #if 0 + printf("\x1b[3m%s\x1b[0m\n",lsp->received_data); + #endif + + u64 response_offset=0, response_size=0; + while (has_response(lsp->received_data, received_so_far, &response_offset, &response_size)) { + if (response_offset + response_size > arr_len(lsp->received_data)) { + // we haven't received this whole response yet. + break; + } + + char *copy = strn_dup(lsp->received_data + response_offset, response_size); + JSON json = {0}; + if (json_parse(&json, copy)) { + assert(json.text == copy); + json.is_text_copied = true; + process_message(lsp, &json); + } else { + lsp_set_error(lsp, "couldn't parse response JSON: %s", json.error); + json_free(&json); + } + size_t leftover_data_len = arr_len(lsp->received_data) - (response_offset + response_size); + + //printf("arr_cap = %u response_offset = %u, response_size = %zu, leftover len = %u\n", + // arr_hdr_(lsp->received_data)->cap, + // response_offset, response_size, leftover_data_len); + memmove(lsp->received_data, lsp->received_data + response_offset + response_size, + leftover_data_len); + arr_set_len(lsp->received_data, leftover_data_len); + arr_reserve(lsp->received_data, leftover_data_len + 1); + lsp->received_data[leftover_data_len] = '\0'; + } +} + +// send requests. +static bool lsp_send(LSP *lsp) { + if (!lsp->initialized) { + // don't send anything before the server is initialized. + return false; + } + + LSPRequest *requests = NULL; + SDL_LockMutex(lsp->requests_mutex); + size_t n_requests = arr_len(lsp->requests_client2server); + requests = calloc(n_requests, sizeof *requests); + memcpy(requests, lsp->requests_client2server, n_requests * sizeof *requests); + arr_clear(lsp->requests_client2server); + SDL_UnlockMutex(lsp->requests_mutex); + + bool quit = false; + for (size_t i = 0; i < n_requests; ++i) { + LSPRequest *r = &requests[i]; + if (!quit) { + // this could slow down lsp_free if there's a gigantic request. + // whatever. + write_request(lsp, r); + } + + if (SDL_SemTryWait(lsp->quit_sem) == 0) { + quit = true; + // important that we don't break here so all the requests get freed. + } + } + + free(requests); + return quit; +} + + +// Do any necessary communication with the LSP. +// This writes requests and reads (and parses) responses. +static int lsp_communication_thread(void *data) { + LSP *lsp = data; + while (1) { + bool quit = lsp_send(lsp); + if (quit) break; + + lsp_receive(lsp, (size_t)10<<20); + if (SDL_SemWaitTimeout(lsp->quit_sem, 5) == 0) + break; + } + + if (lsp->initialized) { + LSPRequest shutdown = { + .type = LSP_REQUEST_SHUTDOWN, + .data = {0} + }; + LSPRequest exit = { + .type = LSP_REQUEST_EXIT, + .data = {0} + }; + write_request(lsp, &shutdown); + // i give you ONE MILLISECOND to send your fucking shutdown response + time_sleep_ms(1); + write_request(lsp, &exit); + // i give you ONE MILLISECOND to terminate + // I WILL KILL YOU IF IT TAKES ANY LONGER + time_sleep_ms(1); + + #if 0 + char buf[1024]={0}; + long long n = process_read(&lsp->process, buf, sizeof buf); + if (n>0) { + buf[n]=0; + printf("%s\n",buf); + } + n = process_read_stderr(&lsp->process, buf, sizeof buf); + if (n>0) { + buf[n]=0; + printf("\x1b[1m%s\x1b[0m\n",buf); + } + #endif + } + return 0; +} + +u32 lsp_document_id(LSP *lsp, const char *path) { + u32 *value = str_hash_table_get(&lsp->document_ids, path); + if (!value) { + u32 id = arr_len(lsp->document_paths); + value = str_hash_table_insert(&lsp->document_ids, path); + *value = id; + arr_add(lsp->document_paths, str_dup(path)); + } + return *value; +} + +bool lsp_create(LSP *lsp, const char *analyzer_command) { + ProcessSettings settings = { + .stdin_blocking = true, + .stdout_blocking = false, + .stderr_blocking = false, + .separate_stderr = true, + }; + process_run_ex(&lsp->process, analyzer_command, &settings); + LSPRequest initialize = { + .type = LSP_REQUEST_INITIALIZE + }; + // immediately send the request rather than queueing it. + // this is a small request, so it shouldn't be a problem. + write_request(lsp, &initialize); + + str_hash_table_create(&lsp->document_ids, sizeof(u32)); + lsp->quit_sem = SDL_CreateSemaphore(0); + lsp->messages_mutex = SDL_CreateMutex(); + lsp->requests_mutex = SDL_CreateMutex(); + lsp->communication_thread = SDL_CreateThread(lsp_communication_thread, "LSP communicate", lsp); + return true; +} + +bool lsp_next_message(LSP *lsp, LSPMessage *message) { + bool any = false; + SDL_LockMutex(lsp->messages_mutex); + if (arr_len(lsp->messages)) { + *message = lsp->messages[0]; + arr_remove(lsp->messages, 0); + any = true; + } + SDL_UnlockMutex(lsp->messages_mutex); + return any; +} + +void lsp_free(LSP *lsp) { + SDL_SemPost(lsp->quit_sem); + SDL_WaitThread(lsp->communication_thread, NULL); + SDL_DestroyMutex(lsp->messages_mutex); + SDL_DestroyMutex(lsp->requests_mutex); + SDL_DestroySemaphore(lsp->quit_sem); + process_kill(&lsp->process); + arr_free(lsp->received_data); + str_hash_table_clear(&lsp->document_ids); + for (size_t i = 0; i < arr_len(lsp->document_paths); ++i) + free(lsp->document_paths[i]); + arr_clear(lsp->document_paths); + arr_foreach_ptr(lsp->messages, LSPMessage, message) { + lsp_message_free(message); + } + arr_free(lsp->messages); +} + +void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change) { + // @TODO(optimization, eventually): batch changes (using the contentChanges array) + LSPRequest request = {.type = LSP_REQUEST_DID_CHANGE}; + LSPRequestDidChange *c = &request.data.change; + c->document = lsp_document_id(lsp, document); + arr_add(c->changes, change); + lsp_send_request(lsp, &request); +} + +SymbolKind lsp_symbol_kind_to_ted(LSPSymbolKind kind) { + switch (kind) { + case LSP_SYMBOL_OTHER: + case LSP_SYMBOL_FILE: + case LSP_SYMBOL_MODULE: + case LSB_SYMBOL_NAMESPACE: + case LSP_SYMBOL_PACKAGE: + return SYMBOL_OTHER; + + case LSP_SYMBOL_CLASS: + case LSP_SYMBOL_TYPEPARAMETER: + case LSP_SYMBOL_ENUM: + case LSP_SYMBOL_INTERFACE: + case LSP_SYMBOL_STRUCT: + case LSP_SYMBOL_EVENT: // i have no clue what this is. let's say it's a type. + return SYMBOL_TYPE; + + case LSP_SYMBOL_PROPERTY: + case LSP_SYMBOL_FIELD: + case LSP_SYMBOL_KEY: + return SYMBOL_FIELD; + + case LSP_SYMBOL_CONSTRUCTOR: + case LSP_SYMBOL_FUNCTION: + case LSP_SYMBOL_OPERATOR: + case LSP_SYMBOL_METHOD: + return SYMBOL_FUNCTION; + + case LSP_SYMBOL_VARIABLE: + return SYMBOL_VARIABLE; + + case LSP_SYMBOL_CONSTANT: + case LSP_SYMBOL_STRING: + case LSP_SYMBOL_NUMBER: + case LSP_SYMBOL_BOOLEAN: + case LSP_SYMBOL_ARRAY: + case LSP_SYMBOL_OBJECT: + case LSP_SYMBOL_ENUMMEMBER: + case LSP_SYMBOL_NULL: + return SYMBOL_CONSTANT; + } + + return SYMBOL_OTHER; +} + +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; + } +} @@ -0,0 +1,282 @@ +// @TODO: +// - use document IDs instead of strings (also lets us use real document version numbers) +// - document this and lsp.c. +// - deal with "Save as" (generate didOpen) +// - maximum queue size for requests/responses just in case? +// - delete old sent requests +// (if the server never sends a response) +// - TESTING: make rust-analyzer-slow (waits 10s before sending response) + +typedef u32 DocumentID; + +typedef enum { + LSP_REQUEST, + LSP_RESPONSE +} LSPMessageType; + +typedef struct { + u32 offset; +} LSPString; + +typedef struct { + u32 line; + // NOTE: this is the UTF-16 character index! + u32 character; +} LSPPosition; + +typedef struct { + LSPPosition start; + LSPPosition end; +} LSPRange; + +typedef enum { + LSP_REQUEST_NONE, + + // client-to-server + LSP_REQUEST_INITIALIZE, + LSP_REQUEST_INITIALIZED, + LSP_REQUEST_DID_OPEN, + LSP_REQUEST_DID_CLOSE, + LSP_REQUEST_DID_CHANGE, + LSP_REQUEST_COMPLETION, + LSP_REQUEST_SHUTDOWN, + LSP_REQUEST_EXIT, + + // server-to-client + LSP_REQUEST_SHOW_MESSAGE, + LSP_REQUEST_LOG_MESSAGE, +} LSPRequestType; + +typedef struct { + Language language; + DocumentID document; + // freed by lsp_request_free + char *file_contents; +} LSPRequestDidOpen; + +typedef struct { + DocumentID document; +} LSPRequestDidClose; + +// see TextDocumentContentChangeEvent in the LSP spec +typedef struct { + LSPRange range; + // new text. will be freed. you can use NULL for the empty string. + char *text; +} LSPDocumentChangeEvent; + +typedef struct { + DocumentID document; + LSPDocumentChangeEvent *changes; // dynamic array +} LSPRequestDidChange; + +typedef enum { + ERROR = 1, + WARNING = 2, + INFO = 3, + LOG = 4 +} LSPWindowMessageType; + +typedef struct { + LSPWindowMessageType type; + // freed by lsp_request_free + char *message; +} LSPRequestMessage; + +typedef struct { + DocumentID document; + LSPPosition pos; +} LSPDocumentPosition; + +typedef struct { + LSPDocumentPosition position; +} LSPRequestCompletion; + +typedef struct { + // id is set by lsp.c; you shouldn't set it. + u32 id; + LSPRequestType type; + union { + LSPRequestDidOpen open; + LSPRequestDidClose close; + LSPRequestDidChange change; + LSPRequestCompletion completion; + // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE + LSPRequestMessage message; + } data; +} LSPRequest; + +typedef enum { + // LSP doesn't actually define this but this will be used for unrecognized values + // (in case they add more symbol kinds in the future) + LSP_SYMBOL_OTHER = 0, + + #define LSP_SYMBOL_KIND_MIN 1 + LSP_SYMBOL_FILE = 1, + LSP_SYMBOL_MODULE = 2, + LSB_SYMBOL_NAMESPACE = 3, + LSP_SYMBOL_PACKAGE = 4, + LSP_SYMBOL_CLASS = 5, + LSP_SYMBOL_METHOD = 6, + LSP_SYMBOL_PROPERTY = 7, + LSP_SYMBOL_FIELD = 8, + LSP_SYMBOL_CONSTRUCTOR = 9, + LSP_SYMBOL_ENUM = 10, + LSP_SYMBOL_INTERFACE = 11, + LSP_SYMBOL_FUNCTION = 12, + LSP_SYMBOL_VARIABLE = 13, + LSP_SYMBOL_CONSTANT = 14, + LSP_SYMBOL_STRING = 15, + LSP_SYMBOL_NUMBER = 16, + LSP_SYMBOL_BOOLEAN = 17, + LSP_SYMBOL_ARRAY = 18, + LSP_SYMBOL_OBJECT = 19, + LSP_SYMBOL_KEY = 20, + LSP_SYMBOL_NULL = 21, + LSP_SYMBOL_ENUMMEMBER = 22, + LSP_SYMBOL_STRUCT = 23, + LSP_SYMBOL_EVENT = 24, + LSP_SYMBOL_OPERATOR = 25, + LSP_SYMBOL_TYPEPARAMETER = 26, + #define LSP_SYMBOL_KIND_MAX 26 +} LSPSymbolKind; + +typedef enum { + #define LSP_COMPLETION_KIND_MIN 1 + LSP_COMPLETION_TEXT = 1, + LSP_COMPLETION_METHOD = 2, + LSP_COMPLETION_FUNCTION = 3, + LSP_COMPLETION_CONSTRUCTOR = 4, + LSP_COMPLETION_FIELD = 5, + LSP_COMPLETION_VARIABLE = 6, + LSP_COMPLETION_CLASS = 7, + LSP_COMPLETION_INTERFACE = 8, + LSP_COMPLETION_MODULE = 9, + LSP_COMPLETION_PROPERTY = 10, + LSP_COMPLETION_UNIT = 11, + LSP_COMPLETION_VALUE = 12, + LSP_COMPLETION_ENUM = 13, + LSP_COMPLETION_KEYWORD = 14, + LSP_COMPLETION_SNIPPET = 15, + LSP_COMPLETION_COLOR = 16, + LSP_COMPLETION_FILE = 17, + LSP_COMPLETION_REFERENCE = 18, + LSP_COMPLETION_FOLDER = 19, + LSP_COMPLETION_ENUMMEMBER = 20, + LSP_COMPLETION_CONSTANT = 21, + LSP_COMPLETION_STRUCT = 22, + LSP_COMPLETION_EVENT = 23, + LSP_COMPLETION_OPERATOR = 24, + LSP_COMPLETION_TYPEPARAMETER = 25, + #define LSP_COMPLETION_KIND_MAX 25 +} LSPCompletionKind; + + +// see InsertTextFormat in the LSP spec. +typedef enum { + // plain text + LSP_TEXT_EDIT_PLAIN = 1, + // snippet e.g. "some_method($1, $2)$0" + LSP_TEXT_EDIT_SNIPPET = 2 +} LSPTextEditType; + +typedef struct { + LSPTextEditType type; + + // if set to true, `range` should be ignored + // -- this is a completion which uses insertText. + // how to handle this: + // "VS Code when code complete is requested in this example + // `con<cursor position>` and a completion item with an `insertText` of + // `console` is provided it will only insert `sole`" + bool at_cursor; + + LSPRange range; + LSPString new_text; +} LSPTextEdit; + +typedef struct { + // display text for this completion + LSPString label; + // text used to filter completions + LSPString filter_text; + // more detail for this item, e.g. the signature of a function + LSPString detail; + // the edit to be applied when this completion is selected. + LSPTextEdit text_edit; + // note: the items are sorted here in this file, + // so you probably don't need to access this. + LSPString sort_text; + // type of completion + LSPCompletionKind kind; +} LSPCompletionItem; + +typedef struct { + // dynamic array + LSPCompletionItem *items; +} LSPResponseCompletion; + +typedef LSPRequestType LSPResponseType; +typedef struct { + LSPRequest request; // the request which this is a response to + // LSP responses tend to have a lot of strings. + // to avoid doing a ton of allocations+frees, + // they're all stored here. + char *string_data; + union { + LSPResponseCompletion completion; + } data; +} LSPResponse; + +typedef struct { + LSPMessageType type; + union { + LSPRequest request; + LSPResponse response; + } u; +} LSPMessage; + +typedef struct LSP { + Process process; + u32 request_id; + StrHashTable document_ids; // values are u32. they are indices into document_filenames. + // this is a dynamic array which just keeps growing. + // but the user isn't gonna open millions of files so it's fine. + char **document_paths; + LSPMessage *messages; + SDL_mutex *messages_mutex; + LSPRequest *requests_client2server; + LSPRequest *requests_server2client; + // we keep track of client-to-server requests + // so that we can process responses. + // also fucking rust-analyzer gives "waiting for cargo metadata or cargo check" + // WHY NOT JUST WAIT UNTIL YOUVE DONE THAT BEFORE SENDING THE INITIALIZE RESPONSE. YOU HAVE NOT FINISHED INITIALIZATION. YOU ARE LYING. + // YOU GIVE A -32801 ERROR CODE WHICH IS "ContentModified" -- WHAT THE FUCK? THATS JUST COMPLETLY WRONG + // so we need to re-send requests in that case. + LSPRequest *requests_sent; + SDL_mutex *requests_mutex; + bool initialized; // has the response to the initialize request been sent? + SDL_Thread *communication_thread; + SDL_sem *quit_sem; + char *received_data; // dynamic array + SDL_mutex *error_mutex; + char error[256]; +} LSP; + +// @TODO: function declarations + +// returns true if there's an error. +// returns false and sets error to "" if there's no error. +// if clear = true, the error will be cleared. +// you can set error = NULL, error_size = 0, clear = true to just clear the error +bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear); +void lsp_message_free(LSPMessage *message); +u32 lsp_document_id(LSP *lsp, const char *path); +void lsp_send_request(LSP *lsp, const LSPRequest *request); +const char *lsp_response_string(const LSPResponse *response, LSPString string); +bool lsp_create(LSP *lsp, const char *analyzer_command); +bool lsp_next_message(LSP *lsp, LSPMessage *message); +void lsp_document_changed(LSP *lsp, const char *document, LSPDocumentChangeEvent change); +void lsp_free(LSP *lsp); +SymbolKind lsp_symbol_kind_to_ted(LSPSymbolKind kind); +SymbolKind lsp_completion_kind_to_ted(LSPCompletionKind kind); @@ -1,6 +1,24 @@ -/* +/* +@TODO: +- kind (icon/color) + - improve color_for_symbol_kind +- send textDocument.completion.completionItemKind capability +- only show "Loading..." if it's taking some time (prevent flash) +- LSP setting +- scroll through completions +- figure out under which circumstances backspace should close completions + - close completions when a non-word character is typed +- rename buffer->filename to buffer->path + - make buffer->path NULL for untitled buffers & fix resulting mess +- rust-analyzer bug reports: + - bad json can give "Unexpected error: client exited without proper shutdown sequence" + - rust-analyzer should wait until cargo metadata/check is done before sending initialize response FUTURE FEATURES: +- robust find (results shouldn't move around when you type things) +- multiple files with command line arguments - configurable max buffer size + max view-only buffer size +- :set-build-command, don't let ../Cargo.toml override ./Makefile +- add numlock as a key modifier - better undo chaining (dechain on backspace?) - allow multiple fonts (fonts directory?) - regenerate tags for completion too if there are no results @@ -48,7 +66,9 @@ no_warn_end #endif #include "unicode.h" +#include "ds.c" #include "util.c" + #if _WIN32 #include "filesystem-win.c" #elif __unix__ @@ -56,7 +76,6 @@ no_warn_end #else #error "Unrecognized operating system." #endif -#include "arr.c" #include "math.c" #if _WIN32 @@ -92,8 +111,10 @@ static void die(char const *fmt, ...) { #include "ted.h" #include "gl.c" #include "text.c" +#include "lsp.h" #include "string32.c" +#include "colors.c" #include "syntax.c" bool tag_goto(Ted *ted, char const *tag); #include "buffer.c" @@ -108,6 +129,8 @@ bool tag_goto(Ted *ted, char const *tag); #include "command.c" #include "config.c" #include "session.c" +#include "json.c" +#include "lsp.c" #if PROFILE #define PROFILE_TIME(var) double var = time_get_seconds(); @@ -286,6 +309,74 @@ int main(int argc, char **argv) { PROFILE_TIME(init_start) PROFILE_TIME(basic_init_start) + + if (0) { + // @TODO TEMPORARY + LSP lsp={0}; +// chdir("/p/test-lsp"); + if (!lsp_create(&lsp, "rust-analyzer")) { + printf("lsp_create: %s\n",lsp.error); + exit(1); + } + usleep(1000000);//if we don't do this we get "waiting for cargo metadata or cargo check" + LSPRequest test_req = {.type = LSP_REQUEST_COMPLETION}; + test_req.data.completion = (LSPRequestCompletion){ + .position = { + .document = lsp_document_id(&lsp, "/p/test-lsp/src/main.rs"), + .pos = { + .line = 2, + .character = 2, + }, + } + }; + lsp_send_request(&lsp, &test_req); + while (1) { + LSPMessage message = {0}; + while (lsp_next_message(&lsp, &message)) { + if (message.type == LSP_RESPONSE) { + const LSPResponse *response = &message.u.response; + switch (response->request.type) { + case LSP_REQUEST_COMPLETION: { + const LSPResponseCompletion *completion = &response->data.completion; + arr_foreach_ptr(completion->items, LSPCompletionItem, item) { + printf("(%d)%s => ", + item->text_edit.type, + lsp_response_string(response, item->label)); + printf("%s\n", + lsp_response_string(response, item->text_edit.new_text)); + } + } break; + default: + break; + } + } else if (message.type == LSP_REQUEST) { + const LSPRequest *request = &message.u.request; + switch (request->type) { + case LSP_REQUEST_SHOW_MESSAGE: { + const LSPRequestMessage *m = &request->data.message; + // @TODO actually show + printf("Show (%d): %s\n", m->type, m->message); + } break; + case LSP_REQUEST_LOG_MESSAGE: { + const LSPRequestMessage *m = &request->data.message; + // @TODO actually log + printf("Log (%d): %s\n", m->type, m->message); + } break; + default: break; + } + } + lsp_message_free(&message); + } + char error[256]; + if (lsp_get_error(&lsp, error, sizeof error, true)) { + printf("lsp error: %s\n", error); + } + usleep(10000); + } + lsp_free(&lsp); + exit(0); + } + #if __unix__ { struct sigaction act = {0}; @@ -341,6 +432,14 @@ int main(int argc, char **argv) { } ted->last_save_time = -1e50; + + // @TODO TEMPORARY + ted->test_lsp = calloc(1, sizeof(LSP)); + if (!lsp_create(ted->test_lsp, "rust-analyzer")) { + printf("lsp_create: %s\n",ted->test_lsp->error); + exit(1); + } + // make sure signal handler has access to ted. error_signal_handler_ted = ted; @@ -789,6 +888,38 @@ int main(int argc, char **argv) { menu_update(ted); } + { + LSP *lsp = ted_get_active_lsp(ted); + if (lsp) { + LSPMessage message = {0}; + while (lsp_next_message(lsp, &message)) { + switch (message.type) { + case LSP_REQUEST: { + LSPRequest *r = &message.u.request; + switch (r->type) { + case LSP_REQUEST_SHOW_MESSAGE: { + LSPRequestMessage *m = &r->data.message; + // @TODO: multiple messages + ted_seterr(ted, "%s", m->message); + } break; + case LSP_REQUEST_LOG_MESSAGE: { + LSPRequestMessage *m = &r->data.message; + // @TODO: actual logging + printf("%s\n", m->message); + } break; + default: break; + } + } break; + case LSP_RESPONSE: { + LSPResponse *r = &message.u.response; + autocomplete_process_lsp_response(ted, r); + } break; + } + lsp_message_free(&message); + } + } + } + ted_update_window_dimensions(ted); float window_width = ted->window_width, window_height = ted->window_height; @@ -886,11 +1017,11 @@ int main(int argc, char **argv) { if (ted->nodes_used[0]) { float y1 = padding; node_frame(ted, node, rect4(x1, y1, x2, y)); - if (ted->autocomplete) { + if (ted->autocomplete.open) { autocomplete_frame(ted); } } else { - ted->autocomplete = false; + autocomplete_close(ted); text_utf8_anchored(font, "Press Ctrl+O to open a file or Ctrl+N to create a new one.", window_width * 0.5f, window_height * 0.5f, ted_color(ted, COLOR_TEXT_SECONDARY), ANCHOR_MIDDLE); text_render(font); @@ -50,7 +50,7 @@ void menu_open(Ted *ted, Menu menu) { if (ted->menu) menu_close_with_next(ted, menu); if (ted->find) find_close(ted); - ted->autocomplete = false; + autocomplete_close(ted); ted->menu = menu; TextBuffer *prev_buf = ted->prev_active_buffer = ted->active_buffer; if (prev_buf) diff --git a/process-posix.c b/process-posix.c index f35017f..c8ea376 100644 --- a/process-posix.c +++ b/process-posix.c @@ -6,55 +6,130 @@ struct Process { pid_t pid; - int pipe; + int stdout_pipe; + // only applicable if separate_stderr was specified. + int stderr_pipe; + int stdin_pipe; char error[64]; }; -bool process_run(Process *proc, char const *command) { +int process_get_id(void) { + return getpid(); +} + +static void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); +} + +bool process_run_ex(Process *proc, const char *command, const ProcessSettings *settings) { memset(proc, 0, sizeof *proc); + int stdin_pipe[2] = {0}, stdout_pipe[2] = {0}, stderr_pipe[2] = {0}; + if (pipe(stdin_pipe) != 0) { + strbuf_printf(proc->error, "%s", strerror(errno)); + return false; + } + if (pipe(stdout_pipe) != 0) { + strbuf_printf(proc->error, "%s", strerror(errno)); + close(stdin_pipe[0]); + close(stdin_pipe[1]); + return false; + } + if (settings->separate_stderr) { + if (pipe(stderr_pipe) != 0) { + strbuf_printf(proc->error, "%s", strerror(errno)); + close(stdin_pipe[0]); + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + return false; + } + } + bool success = false; - int pipefd[2]; - if (pipe(pipefd) == 0) { - pid_t pid = fork(); - if (pid == 0) { - // child process - // put child in its own group. it will be in this group with all of its descendents, - // so by killing everything in the group, we kill all the descendents of this process. - // if we didn't do this, we would just be killing the sh process in process_kill. - setpgid(0, 0); - // send stdout and stderr to pipe - dup2(pipefd[1], STDOUT_FILENO); - dup2(pipefd[1], STDERR_FILENO); - close(pipefd[0]); - close(pipefd[1]); - char *program = "/bin/sh"; - char *argv[] = {program, "-c", (char *)command, NULL}; - if (execv(program, argv) == -1) { - dprintf(STDERR_FILENO, "%s: %s\n", program, strerror(errno)); - exit(127); - } - } else if (pid > 0) { - // parent process - close(pipefd[1]); - fcntl(pipefd[0], F_SETFL, fcntl(pipefd[0], F_GETFL) | O_NONBLOCK); // set pipe to non-blocking - proc->pid = pid; - proc->pipe = pipefd[0]; - success = true; + pid_t pid = fork(); + if (pid == 0) { + // child process + // put child in its own group. it will be in this group with all of its descendents, + // so by killing everything in the group, we kill all the descendents of this process. + // if we didn't do this, we would just be killing the sh process in process_kill. + setpgid(0, 0); + // pipe stuff + dup2(stdout_pipe[1], STDOUT_FILENO); + if (stderr_pipe[1]) + dup2(stderr_pipe[1], STDERR_FILENO); + else + dup2(stdout_pipe[1], STDERR_FILENO); + dup2(stdin_pipe[0], STDIN_FILENO); + // don't need these file descriptors anymore + close(stdin_pipe[0]); + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + if (stderr_pipe[0]) { + close(stderr_pipe[0]); + close(stderr_pipe[1]); } - } else { - strbuf_printf(proc->error, "%s", strerror(errno)); + + char *program = "/bin/sh"; + char *argv[] = {program, "-c", (char *)command, NULL}; + if (execv(program, argv) == -1) { + dprintf(STDERR_FILENO, "%s: %s\n", program, strerror(errno)); + exit(127); + } + } else if (pid > 0) { + // parent process + + // we're reading from (the child's) stdout/stderr and writing to stdin, + // so we don't need the write end of the stdout pipe or the + // read end of the stdin pipe. + close(stdout_pipe[1]); + if (stderr_pipe[1]) + close(stderr_pipe[1]); + close(stdin_pipe[0]); + // set pipes to non-blocking + if (!settings->stdout_blocking) + set_nonblocking(stdout_pipe[0]); + if (stderr_pipe[0] && !settings->stderr_blocking) + set_nonblocking(stderr_pipe[0]); + if (!settings->stdin_blocking) + set_nonblocking(stdin_pipe[1]); + proc->pid = pid; + proc->stdout_pipe = stdout_pipe[0]; + if (stderr_pipe[0]) + proc->stderr_pipe = stderr_pipe[0]; + proc->stdin_pipe = stdin_pipe[1]; + success = true; } return success; } +bool process_run(Process *proc, char const *command) { + const ProcessSettings settings = {0}; + return process_run_ex(proc, command, &settings); +} + + char const *process_geterr(Process *p) { return *p->error ? p->error : NULL; } -long long process_read(Process *proc, char *data, size_t size) { - assert(proc->pipe); - ssize_t bytes_read = read(proc->pipe, data, size); +long long process_write(Process *proc, const char *data, size_t size) { + assert(proc->stdin_pipe); // check that process hasn't been killed + ssize_t bytes_written = write(proc->stdin_pipe, data, size); + if (bytes_written >= 0) { + return (long long)bytes_written; + } else if (errno == EAGAIN) { + return 0; + } else { + strbuf_printf(proc->error, "%s", strerror(errno)); + return -2; + } +} + +static long long process_read_fd(Process *proc, int fd, char *data, size_t size) { + assert(fd); + ssize_t bytes_read = read(fd, data, size); if (bytes_read >= 0) { return (long long)bytes_read; } else if (errno == EAGAIN) { @@ -63,16 +138,31 @@ long long process_read(Process *proc, char *data, size_t size) { strbuf_printf(proc->error, "%s", strerror(errno)); return -2; } +} + +long long process_read(Process *proc, char *data, size_t size) { + return process_read_fd(proc, proc->stdout_pipe, data, size); +} + +long long process_read_stderr(Process *proc, char *data, size_t size) { + return process_read_fd(proc, proc->stderr_pipe, data, size); +} +static void process_close_pipes(Process *proc) { + close(proc->stdin_pipe); + close(proc->stdout_pipe); + close(proc->stderr_pipe); + proc->stdin_pipe = 0; + proc->stdout_pipe = 0; + proc->stderr_pipe = 0; } void process_kill(Process *proc) { kill(-proc->pid, SIGKILL); // kill everything in process group // get rid of zombie process waitpid(proc->pid, NULL, 0); - close(proc->pipe); proc->pid = 0; - proc->pipe = 0; + process_close_pipes(proc); } int process_check_status(Process *proc, char *message, size_t message_size) { @@ -83,7 +173,7 @@ int process_check_status(Process *proc, char *message, size_t message_size) { return 0; } else if (ret > 0) { if (WIFEXITED(wait_status)) { - close(proc->pipe); proc->pipe = 0; + process_close_pipes(proc); int code = WEXITSTATUS(wait_status); if (code == 0) { str_printf(message, message_size, "exited successfully"); @@ -93,14 +183,14 @@ int process_check_status(Process *proc, char *message, size_t message_size) { return -1; } } else if (WIFSIGNALED(wait_status)) { - close(proc->pipe); + process_close_pipes(proc); str_printf(message, message_size, "terminated by signal %d", WTERMSIG(wait_status)); return -1; } return 0; } else { // this process is gone or something? - close(proc->pipe); proc->pipe = 0; + process_close_pipes(proc); str_printf(message, message_size, "process ended unexpectedly"); return -1; } diff --git a/process-win.c b/process-win.c index a5f05c0..8ea7b91 100644 --- a/process-win.c +++ b/process-win.c @@ -1,3 +1,5 @@ +#error "@TODO : implement process_write, separate_stderr" + #include "process.h" struct Process { @@ -4,17 +4,39 @@ typedef struct Process Process; +// zero everything except what you're using +typedef struct { + bool stdin_blocking; + bool stdout_blocking; + bool separate_stderr; + bool stderr_blocking; // not applicable if separate_stderr is false. +} ProcessSettings; + +// get process ID of this process +int process_get_id(void); // execute the given command (like if it was passed to system()). // returns false on failure +bool process_run_ex(Process *proc, const char *command, const ProcessSettings *props); +// like process_run_ex, but with the default settings bool process_run(Process *process, char const *command); // returns the error last error produced, or NULL if there was no error. char const *process_geterr(Process *process); +// write to stdin +// returns: +// -2 on error +// or a non-negative number indicating the number of bytes written. +long long process_write(Process *process, const char *data, size_t size); +// read from stdout+stderr // returns: // -2 on error // -1 if no data is available right now // 0 on end of file // or a positive number indicating the number of bytes read to data (at most size) long long process_read(Process *process, char *data, size_t size); +// like process_read, but reads stderr. +// this function ALWAYS RETURNS -2 if separate_stderr is not specified in the ProcessSettings. +// if separate_stderr is false, then both stdout and stderr will be sent via process_read. +long long process_read_stderr(Process *process, char *data, size_t size); // Checks if the process has exited. Returns: // -1 if the process returned a non-zero exit code, or got a signal. // 1 if the process exited successfully @@ -66,6 +66,17 @@ Settings *ted_active_settings(Ted *ted) { return settings; } +LSP *ted_get_lsp(Ted *ted, Language lang) { + // @TODO + return ted->test_lsp; +} + +LSP *ted_get_active_lsp(Ted *ted) { + if (!ted->active_buffer) + return NULL; + return buffer_lsp(ted->active_buffer); +} + u32 ted_color(Ted *ted, ColorSetting color) { return ted_active_settings(ted)->colors[color]; } @@ -134,7 +145,7 @@ static void ted_load_fonts(Ted *ted) { void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer) { TextBuffer *search_buffer = find_search_buffer(ted); ted->active_buffer = buffer; - ted->autocomplete = false; + autocomplete_close(ted); if (buffer != search_buffer) { if (ted->find) find_update(ted, true); // make sure find results are for this file @@ -173,7 +184,7 @@ static void ted_reset_active_buffer(Ted *ted) { ted_switch_to_buffer(ted, &ted->buffers[node->tabs[node->active_tab]]); } else { // there's nothing to set it to - ted->active_buffer = NULL; + ted_switch_to_buffer(ted, NULL); } } @@ -233,6 +233,9 @@ yes = #afa no = #faa cancel = #ffa +# autocomplete +autocomplete-border = #777 + # Syntax highlighting keyword = #0c0 preprocessor = #77f @@ -352,7 +352,39 @@ typedef struct { u32 build_output_line; // which line in the build output corresponds to this error } BuildError; +// LSPSymbolKinds are translated to these. this is a much coarser categorization +typedef enum { + SYMBOL_OTHER, + SYMBOL_FUNCTION, + SYMBOL_FIELD, + SYMBOL_TYPE, + SYMBOL_VARIABLE, + SYMBOL_CONSTANT, + SYMBOL_KEYWORD +} SymbolKind; + +typedef struct { + char *label; + char *filter; + char *text; + char *detail; // this can be NULL! + SymbolKind kind; +} Autocompletion; + +typedef struct { + bool open; // is the autocomplete window open? + bool waiting_for_lsp; + + Autocompletion *completions; // dynamic array of all completions + u32 *suggested; // dynamic array of completions to be suggested (indices into completions) + BufferPos last_pos; // position of cursor last time completions were generated. if this changes, we need to recompute completions. + i32 cursor; // which completion is currently selected (index into suggested) + Rect rect; // rectangle where the autocomplete menu is (needed to avoid interpreting autocomplete clicks as other clicks) +} Autocomplete; + typedef struct Ted { + struct LSP *test_lsp; // @TODO: something better + SDL_Window *window; Font *font_bold; Font *font; @@ -401,10 +433,7 @@ typedef struct Ted { Command warn_unsaved; // if non-zero, the user is trying to execute this command, but there are unsaved changes bool build_shown; // are we showing the build output? bool building; // is the build process running? - bool autocomplete; // is the autocomplete window open? - - i32 autocomplete_cursor; // which completion is currently selected - Rect autocomplete_rect; // rectangle where the autocomplete menu is (needed to avoid interpreting autocomplete clicks as other clicks) + Autocomplete autocomplete; FILE *log; @@ -466,11 +495,13 @@ typedef struct Ted { char error_shown[512]; // error display in box on screen } Ted; +void autocomplete_close(Ted *ted); void command_execute(Ted *ted, Command c, i64 argument); void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer); // the settings of the active buffer, or the default settings if there is no active buffer Settings *ted_active_settings(Ted *ted); void ted_load_configs(Ted *ted, bool reloading); +struct LSP *ted_get_lsp(Ted *ted, Language lang); static TextBuffer *find_search_buffer(Ted *ted); // first, we read all config files, then we parse them. // this is because we want less specific settings (e.g. settings applied @@ -19,6 +19,7 @@ fn main() -> Result<()> { line.pop(); lines.push(line); } +let x = lines. for line in lines { println!("{}", line); } @@ -8,6 +8,9 @@ static bool unicode_is_start_of_code_point(u8 byte) { // continuation bytes are of the form 10xxxxxx return (byte & 0xC0) != 0x80; } +static bool unicode_is_continuation_byte(u8 byte) { + return (byte & 0xC0) == 0x80; +} // A lot like mbrtoc32. Doesn't depend on the locale though, for one thing. // *c will be filled with the next UTF-8 code point in `str`. `bytes` refers to the maximum @@ -142,9 +142,14 @@ static void str_cat(char *dst, size_t dst_sz, char const *src) { } // safer version of strncpy. dst_sz includes a null terminator. -static void str_cpy(char *dst, size_t dst_sz, char const *src) { - size_t srclen = strlen(src); - size_t n = srclen; // number of bytes to copy +static void strn_cpy(char *dst, size_t dst_sz, char const *src, size_t src_len) { + size_t n = src_len; // number of bytes to copy + for (size_t i = 0; i < n; ++i) { + if (src[i] == '\0') { + n = i; + break; + } + } if (dst_sz == 0) { assert(0); @@ -157,6 +162,11 @@ static void str_cpy(char *dst, size_t dst_sz, char const *src) { dst[n] = 0; } +// safer version of strcpy. dst_sz includes a null terminator. +static void str_cpy(char *dst, size_t dst_sz, char const *src) { + strn_cpy(dst, dst_sz, src, SIZE_MAX); +} + #define strbuf_cpy(dst, src) str_cpy(dst, sizeof dst, src) #define strbuf_cat(dst, src) str_cat(dst, sizeof dst, src) @@ -239,18 +249,28 @@ static int str_qsort_case_insensitive_cmp(const void *av, const void *bv) { return strcmp_case_insensitive(*a, *b); } -static void *qsort_ctx_arg; -static int (*qsort_ctx_cmp)(void *, const void *, const void *); -static int qsort_with_context_cmp(const void *a, const void *b) { - return qsort_ctx_cmp(qsort_ctx_arg, a, b); +// imo windows has the argument order right here +#if _WIN32 +#define qsort_with_context qsort_s +#else +typedef struct { + int (*compar)(void *, const void *, const void *); + void *context; +} QSortWithContext; +static int qsort_with_context_cmp(const void *a, const void *b, void *context) { + QSortWithContext *c = context; + return c->compar(c->context, a, b); } - -static void qsort_with_context(void *base, size_t nmemb, size_t size, int (*compar)(void *, const void *, const void *), void *arg) { - // just use global variables. hopefully we don't try to run this in something multithreaded! - qsort_ctx_arg = arg; - qsort_ctx_cmp = compar; - qsort(base, nmemb, size, qsort_with_context_cmp); +static void qsort_with_context(void *base, size_t nmemb, size_t size, + int (*compar)(void *, const void *, const void *), + void *arg) { + QSortWithContext ctx = { + .compar = compar, + .context = arg + }; + qsort_r(base, nmemb, size, qsort_with_context_cmp, &ctx); } +#endif // the actual file name part of the path; get rid of the containing directory. // NOTE: the returned string is part of path, so you don't need to free it or anything. @@ -357,3 +377,127 @@ static bool copy_file(char const *src, char const *dst) { } return success; } + + +static uint64_t str_hash(char const *str, size_t len) { + uint64_t hash = 0; + char const *p = str, *end = str + len; + for (; p < end; ++p) { + hash = ((hash * 1664737020647550361 + 123843) << 8) + 2918635993572506131*(uint64_t)*p; + } + return hash; +} + +typedef struct { + char *str; + size_t len; + uint64_t data[]; +} StrHashTableSlot; + +typedef StrHashTableSlot *StrHashTableSlotPtr; + +typedef struct { + StrHashTableSlot **slots; + size_t data_size; + size_t nentries; /* # of filled slots */ +} StrHashTable; + +static inline void str_hash_table_create(StrHashTable *t, size_t data_size) { + t->slots = NULL; + t->data_size = data_size; + t->nentries = 0; +} + +static StrHashTableSlot **str_hash_table_slot_get(StrHashTableSlot **slots, char const *s, size_t s_len, size_t i) { + StrHashTableSlot **slot; + size_t slots_cap = arr_len(slots); + while (1) { + assert(i < slots_cap); + slot = &slots[i]; + if (!*slot) break; + if (s && (*slot)->str && + s_len == (*slot)->len && memcmp(s, (*slot)->str, s_len) == 0) + break; + i = (i+1) % slots_cap; + } + return slot; +} + +static void str_hash_table_grow(StrHashTable *t) { + size_t slots_cap = arr_len(t->slots); + if (slots_cap <= 2 * t->nentries) { + StrHashTableSlot **new_slots = NULL; + size_t new_slots_cap = slots_cap * 2 + 10; + arr_set_len(new_slots, new_slots_cap); + memset(new_slots, 0, new_slots_cap * sizeof *new_slots); + arr_foreach_ptr(t->slots, StrHashTableSlotPtr, slotp) { + StrHashTableSlot *slot = *slotp; + if (slot) { + uint64_t new_hash = str_hash(slot->str, slot->len); + StrHashTableSlot **new_slot = str_hash_table_slot_get(new_slots, slot->str, slot->len, new_hash % new_slots_cap); + *new_slot = slot; + } + } + arr_clear(t->slots); + t->slots = new_slots; + } +} + +static inline size_t str_hash_table_slot_size(StrHashTable *t) { + return sizeof(StrHashTableSlot) + ((t->data_size + sizeof(uint64_t) - 1) / sizeof(uint64_t)) * sizeof(uint64_t); +} + +static StrHashTableSlot *str_hash_table_insert_(StrHashTable *t, char const *str, size_t len) { + size_t slots_cap; + uint64_t hash; + StrHashTableSlot **slot; + str_hash_table_grow(t); + slots_cap = arr_len(t->slots); + hash = str_hash(str, len); + slot = str_hash_table_slot_get(t->slots, str, len, hash % slots_cap); + if (!*slot) { + *slot = calloc(1, str_hash_table_slot_size(t)); + char *s = (*slot)->str = calloc(1, len + 1); + memcpy(s, str, len); + (*slot)->len = len; + ++t->nentries; + } + return *slot; +} + +// does NOT check for a null byte. +static inline void *str_hash_table_insert_with_len(StrHashTable *t, char const *str, size_t len) { + return str_hash_table_insert_(t, str, len)->data; +} + +static inline void *str_hash_table_insert(StrHashTable *t, char const *str) { + return str_hash_table_insert_(t, str, strlen(str))->data; +} + +static void str_hash_table_clear(StrHashTable *t) { + arr_foreach_ptr(t->slots, StrHashTableSlotPtr, slotp) { + if (*slotp) { + free((*slotp)->str); + } + free(*slotp); + } + arr_clear(t->slots); + t->nentries = 0; +} + +static StrHashTableSlot *str_hash_table_get_(StrHashTable *t, char const *str, size_t len) { + size_t nslots = arr_len(t->slots), slot_index; + if (!nslots) return NULL; + slot_index = str_hash(str, len) % arr_len(t->slots); + return *str_hash_table_slot_get(t->slots, str, len, slot_index); +} + +static inline void *str_hash_table_get_with_len(StrHashTable *t, char const *str, size_t len) { + StrHashTableSlot *slot = str_hash_table_get_(t, str, len); + if (!slot) return NULL; + return slot->data; +} + +static inline void *str_hash_table_get(StrHashTable *t, char const *str) { + return str_hash_table_get_with_len(t, str, strlen(str)); +} |