From 2667c71e71d77ecade1142c133ed7181ce38c664 Mon Sep 17 00:00:00 2001 From: pommicket Date: Thu, 22 Dec 2022 18:28:15 -0500 Subject: reorganize lsp code --- lsp-parse.c | 329 +++++++++++++++++++++++++++++++++++++++++++++++++ lsp-write-request.c | 342 --------------------------------------------------- lsp-write.c | 342 +++++++++++++++++++++++++++++++++++++++++++++++++++ lsp.c | 345 ++-------------------------------------------------- main.c | 1 + 5 files changed, 681 insertions(+), 678 deletions(-) create mode 100644 lsp-parse.c delete mode 100644 lsp-write-request.c create mode 100644 lsp-write.c diff --git a/lsp-parse.c b/lsp-parse.c new file mode 100644 index 0000000..6167cc8 --- /dev/null +++ b/lsp-parse.c @@ -0,0 +1,329 @@ +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 + }; + + JSONValue sort_text_value = json_object_get(json, item_object, "sortText"); + if (sort_text_value.type == JSON_STRING) { + // LSP allows using a different string for sorting. + item->sort_text = lsp_response_add_json_string(response, + json, sort_text_value.val.string); + } + + JSONValue filter_text_value = json_object_get(json, item_object, "filterText"); + if (filter_text_value.type == JSON_STRING) { + // LSP allows using a different string for filtering. + item->filter_text = lsp_response_add_json_string(response, + json, filter_text_value.val.string); + } + + JSONValue text_type_value = json_object_get(json, item_object, "insertTextFormat"); + if (text_type_value.type == JSON_NUMBER) { + double type = text_type_value.val.number; + if (type != LSP_TEXT_EDIT_PLAIN && type != LSP_TEXT_EDIT_SNIPPET) { + lsp_set_error(lsp, "Bad InsertTextFormat: %g", type); + return false; + } + item->text_edit.type = (LSPTextEditType)type; + } + + // @TODO: detail + + // @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-request.c b/lsp-write-request.c deleted file mode 100644 index 28b19d3..0000000 --- a/lsp-write-request.c +++ /dev/null @@ -1,342 +0,0 @@ - -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-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 index f6d2947..281ec17 100644 --- a/lsp.c +++ b/lsp.c @@ -1,5 +1,13 @@ static void lsp_request_free(LSPRequest *r); -#include "lsp-write-request.c" +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; @@ -13,12 +21,6 @@ bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) { return has_err; } -#define lsp_set_error(lsp, ...) do {\ - SDL_LockMutex(lsp->error_mutex);\ - strbuf_printf(lsp->error, __VA_ARGS__);\ - SDL_UnlockMutex(lsp->error_mutex);\ - } while (0) - static void lsp_document_change_event_free(LSPDocumentChangeEvent *event) { free(event->text); @@ -75,32 +77,6 @@ void lsp_message_free(LSPMessage *message) { memset(message, 0, sizeof *message); } -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); -} // 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) { @@ -125,314 +101,11 @@ void lsp_send_request(LSP *lsp, const LSPRequest *request) { SDL_UnlockMutex(lsp->requests_mutex); } -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; -} - const char *lsp_response_string(const LSPResponse *response, LSPString string) { assert(string.offset < arr_len(response->string_data)); return &response->string_data[string.offset]; } -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 - }; - - JSONValue sort_text_value = json_object_get(json, item_object, "sortText"); - if (sort_text_value.type == JSON_STRING) { - // LSP allows using a different string for sorting. - item->sort_text = lsp_response_add_json_string(response, - json, sort_text_value.val.string); - } - - JSONValue filter_text_value = json_object_get(json, item_object, "filterText"); - if (filter_text_value.type == JSON_STRING) { - // LSP allows using a different string for filtering. - item->filter_text = lsp_response_add_json_string(response, - json, filter_text_value.val.string); - } - - JSONValue text_type_value = json_object_get(json, item_object, "insertTextFormat"); - if (text_type_value.type == JSON_NUMBER) { - double type = text_type_value.val.number; - if (type != LSP_TEXT_EDIT_PLAIN && type != LSP_TEXT_EDIT_SNIPPET) { - lsp_set_error(lsp, "Bad InsertTextFormat: %g", type); - return false; - } - item->text_edit.type = (LSPTextEditType)type; - } - - // @TODO: detail - - // @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 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); - -} - // receive responses/requests/notifications from LSP, up to max_size bytes. static void lsp_receive(LSP *lsp, size_t max_size) { diff --git a/main.c b/main.c index 9b113c2..5bd0a47 100644 --- a/main.c +++ b/main.c @@ -4,6 +4,7 @@ - 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: -- cgit v1.2.3