From 8deb7afed44d7be52cb450c3177ecc8e63fd46eb Mon Sep 17 00:00:00 2001 From: pommicket Date: Mon, 19 Dec 2022 11:00:45 -0500 Subject: much better system for writing requests --- lsp-write-request.c | 303 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lsp.c | 182 +------------------------------ lsp.h | 5 +- main.c | 6 +- 4 files changed, 314 insertions(+), 182 deletions(-) create mode 100644 lsp-write-request.c diff --git a/lsp-write-request.c b/lsp-write-request.c new file mode 100644 index 0000000..121392d --- /dev/null +++ b/lsp-write-request.c @@ -0,0 +1,303 @@ + +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 { + StrBuilder builder; + bool is_first; +} JSONWriter; + +static JSONWriter json_writer_new(void) { + return (JSONWriter){ + .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) { + str_builder_append(&o->builder, ","); + o->is_first = false; + } +} + +static void write_string(JSONWriter *o, const char* string) { + // @TODO: escape in-place + char *escaped = json_escape(string); + str_builder_appendf(&o->builder, "\"%s\"", escaped); + free(escaped); +} + +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, const char *path) { + char *escaped_path = json_escape(path); + str_builder_appendf(&o->builder, "\"file:///%s\"", escaped_path); + free(escaped_path); +} + +static void write_key_file_uri(JSONWriter *o, const char *key, const char *path) { + write_key(o, key); + write_file_uri(o, path); +} + +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_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"; +} + +// technically there are "requests" and "notifications" +// notifications are different in that they don't have IDs and don't return responses. +// this function handles both. +// NOTE: do not call lsp_request_free on request. freeing the request will be handled. +// returns the ID of the request +static void write_request(LSP *lsp, LSPRequest *request) { + JSONWriter writer = json_writer_new(); + 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 == LSP_REQUEST_INITIALIZED + || request->type == LSP_REQUEST_EXIT + || request->type == LSP_REQUEST_DID_OPEN; + 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->path); + 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_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_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); + write_obj_end(o); + } + write_arr_end(o); + write_obj_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.path); + 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 1 + 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 94b0eca..b7aa991 100644 --- a/lsp.c +++ b/lsp.c @@ -1,3 +1,6 @@ +static void lsp_request_free(LSPRequest *r); +#include "lsp-write-request.c" + bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) { bool has_err = false; SDL_LockMutex(lsp->error_mutex); @@ -16,40 +19,8 @@ bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) { SDL_UnlockMutex(lsp->error_mutex);\ } while (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"; -} - -static void lsp_position_free(LSPDocumentPosition *position) { +static void lsp_document_position_free(LSPDocumentPosition *position) { free(position->path); } @@ -67,7 +38,7 @@ static void lsp_request_free(LSPRequest *r) { break; case LSP_REQUEST_COMPLETION: { LSPRequestCompletion *completion = &r->data.completion; - lsp_position_free(&completion->position); + lsp_document_position_free(&completion->position); } break; case LSP_REQUEST_DID_OPEN: { LSPRequestDidOpen *open = &r->data.open; @@ -138,149 +109,6 @@ static WarnUnusedResult bool lsp_expect_number(LSP *lsp, JSONValue value, const return lsp_expect_type(lsp, value, JSON_NUMBER, what); } -static void write_string(StrBuilder *builder, const char* string) { - abort(); -} - -static void write_range(StrBuilder *builder, LSPRange range) { -} - -// technically there are "requests" and "notifications" -// notifications are different in that they don't have IDs and don't return responses. -// this function handles both. -// NOTE: do not call lsp_request_free on request. freeing the request will be handled. -// returns the ID of the request -static void write_request(LSP *lsp, LSPRequest *request) { - - StrBuilder builder = str_builder_new(); - - u32 max_header_size = 64; - // this is where our header will go - str_builder_append_null(&builder, max_header_size); - - str_builder_append(&builder, "{\"jsonrpc\":\"2.0\","); - - bool is_notification = request->type == LSP_REQUEST_INITIALIZED - || request->type == LSP_REQUEST_EXIT - || request->type == LSP_REQUEST_DID_OPEN; - if (!is_notification) { - u32 id = lsp->request_id++; - request->id = id; - str_builder_appendf(&builder, "\"id\":%lu,", (unsigned long)id); - } - - 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_INITIALIZE: { - str_builder_appendf(&builder, - "\"method\":\"initialize\",\"params\":{" - "\"processId\":%d," - "\"capabilities\":{}," - "\"rootUri\":null," - "\"workspaceFolders\":null" - "}", process_get_id()); - } break; - case LSP_REQUEST_INITIALIZED: - str_builder_append(&builder, "\"method\":\"initialized\""); - break; - case LSP_REQUEST_DID_OPEN: { - const LSPRequestDidOpen *open = &request->data.open; - char *escaped_path = json_escape(open->path); - // @TODO: escape directly into builder - str_builder_appendf(&builder, - "\"method\":\"textDocument/didOpen\",\"params\":{" - "\"textDocument\":{" - "\"uri\":\"file://%s\"," - "\"languageId\":\"%s\"," - "\"version\":1," - "\"text\":", - escaped_path, - lsp_language_id(open->language)); - write_string(&builder, open->file_contents); - str_builder_appendf(&builder, "}}"); - free(escaped_path); - } break; - case LSP_REQUEST_DID_CHANGE: { - LSPRequestDidChange *change = &request->data.change; - static unsigned long long version_number = 0; // @TODO @TEMPORARY - ++version_number; - str_builder_appendf(&builder, - "\"method\":\"textDocument/didChange\",\"params\":{" - "\"textDocument\":{\"version\":%llu,\"uri\":\"file://%s\"}," - "\"contentChanges\":[", - version_number, change->document); - arr_foreach_ptr(change->changes, LSPDocumentChangeEvent, event) { - if (event != change->changes) str_builder_append(&builder, ","); - str_builder_appendf(&builder, "{\"range\":"); - write_range(&builder, event->range); - str_builder_appendf(&builder, "\"text\":"); - write_string(&builder, event->text); - str_builder_append(&builder, "}"); - } - str_builder_appendf(&builder, - "]}"); - } break; - case LSP_REQUEST_COMPLETION: { - const LSPRequestCompletion *completion = &request->data.completion; - char *escaped_path = json_escape(completion->position.path); - str_builder_appendf(&builder,"\"method\":\"textDocument/completion\",\"params\":{" - "\"textDocument\":{\"uri\":\"file://%s\"}," - "\"position\":{" - "\"line\":%lu," - "\"character\":%lu" - "}" - "}", - escaped_path, - (ulong)completion->position.line, - (ulong)completion->position.character); - free(escaped_path); - } break; - case LSP_REQUEST_SHUTDOWN: - str_builder_append(&builder, "\"method\":\"shutdown\""); - break; - case LSP_REQUEST_EXIT: - str_builder_append(&builder, "\"method\":\"exit\""); - break; - } - - str_builder_append(&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 1 - 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); - } -} - // 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"); diff --git a/lsp.h b/lsp.h index 6a7ca24..170d883 100644 --- a/lsp.h +++ b/lsp.h @@ -18,6 +18,7 @@ typedef struct { typedef struct { u32 line; + // NOTE: this is the UTF-16 character index! u32 character; } LSPPosition; @@ -79,9 +80,7 @@ typedef struct { typedef struct { // freed by lsp_request_free char *path; - u32 line; - // the **UTF-16** "character" offset within the line - u32 character; + LSPPosition pos; } LSPDocumentPosition; typedef struct { diff --git a/main.c b/main.c index f8e3c98..2c0a18d 100644 --- a/main.c +++ b/main.c @@ -311,8 +311,10 @@ int main(int argc, char **argv) { test_req.data.completion = (LSPRequestCompletion){ .position = { .path = str_dup("/p/test-lsp/src/main.rs"), - .line = 2, - .character = 2, + .pos = { + .line = 2, + .character = 2, + }, } }; lsp_send_request(&lsp, &test_req); -- cgit v1.2.3