summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--autocomplete.c467
-rw-r--r--base.h19
-rw-r--r--buffer.c214
-rw-r--r--build.c2
-rw-r--r--colors.c69
-rw-r--r--colors.h60
-rw-r--r--command.c14
-rw-r--r--ds.c (renamed from arr.c)120
-rw-r--r--json.c713
-rw-r--r--lsp-parse.c337
-rw-r--r--lsp-write.c342
-rw-r--r--lsp.c413
-rw-r--r--lsp.h282
-rw-r--r--main.c139
-rw-r--r--menu.c2
-rw-r--r--process-posix.c168
-rw-r--r--process-win.c2
-rw-r--r--process.h22
-rw-r--r--ted.c15
-rw-r--r--ted.cfg3
-rw-r--r--ted.h39
-rw-r--r--test.rs1
-rw-r--r--unicode.h3
-rw-r--r--util.c170
25 files changed, 3254 insertions, 366 deletions
diff --git a/Makefile b/Makefile
index 92123c8..eb73aa5 100644
--- a/Makefile
+++ b/Makefile
@@ -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);
}
diff --git a/base.h b/base.h
index ee427fc..5ba03bc 100644
--- a/base.h
+++ b/base.h
@@ -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\"") \
diff --git a/buffer.c b/buffer.c
index bf8047c..19a55f2 100644
--- a/buffer.c
+++ b/buffer.c
@@ -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
diff --git a/build.c b/build.c
index 6ec8daf..3ac0001 100644
--- a/build.c
+++ b/build.c
@@ -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;
+}
diff --git a/colors.h b/colors.h
index f45ed01..0c8b1ea 100644
--- a/colors.h
+++ b/colors.h
@@ -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;
-}
-
diff --git a/command.c b/command.c
index ce447ed..5ed30b7 100644
--- a/command.c
+++ b/command.c
@@ -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 {
diff --git a/arr.c b/ds.c
index d092e68..eb235d6 100644
--- a/arr.c
+++ b/ds.c
@@ -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);
+}
+
diff --git a/json.c b/json.c
new file mode 100644
index 0000000..e07b0ec
--- /dev/null
+++ b/json.c
@@ -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);
+ }
+}
diff --git a/lsp.c b/lsp.c
new file mode 100644
index 0000000..9d02d34
--- /dev/null
+++ b/lsp.c
@@ -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;
+ }
+}
diff --git a/lsp.h b/lsp.h
new file mode 100644
index 0000000..0aa8628
--- /dev/null
+++ b/lsp.h
@@ -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);
diff --git a/main.c b/main.c
index 1967db8..ce40db1 100644
--- a/main.c
+++ b/main.c
@@ -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);
diff --git a/menu.c b/menu.c
index a41da08..2e04d49 100644
--- a/menu.c
+++ b/menu.c
@@ -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 {
diff --git a/process.h b/process.h
index 4db864e..2d5a10c 100644
--- a/process.h
+++ b/process.h
@@ -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
diff --git a/ted.c b/ted.c
index 5d82198..c9ac007 100644
--- a/ted.c
+++ b/ted.c
@@ -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);
}
}
diff --git a/ted.cfg b/ted.cfg
index 4b815f6..ff4f7e4 100644
--- a/ted.cfg
+++ b/ted.cfg
@@ -233,6 +233,9 @@ yes = #afa
no = #faa
cancel = #ffa
+# autocomplete
+autocomplete-border = #777
+
# Syntax highlighting
keyword = #0c0
preprocessor = #77f
diff --git a/ted.h b/ted.h
index e204b17..7d23088 100644
--- a/ted.h
+++ b/ted.h
@@ -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
diff --git a/test.rs b/test.rs
index 6c852d9..40c2438 100644
--- a/test.rs
+++ b/test.rs
@@ -19,6 +19,7 @@ fn main() -> Result<()> {
line.pop();
lines.push(line);
}
+let x = lines.
for line in lines {
println!("{}", line);
}
diff --git a/unicode.h b/unicode.h
index fb9810a..fdd5669 100644
--- a/unicode.h
+++ b/unicode.h
@@ -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
diff --git a/util.c b/util.c
index 07cd8ea..e973674 100644
--- a/util.c
+++ b/util.c
@@ -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));
+}