diff options
Diffstat (limited to 'lsp-write.c')
-rw-r--r-- | lsp-write.c | 423 |
1 files changed, 423 insertions, 0 deletions
diff --git a/lsp-write.c b/lsp-write.c new file mode 100644 index 0000000..87cb238 --- /dev/null +++ b/lsp-write.c @@ -0,0 +1,423 @@ + + +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_arr_elem_obj_start(JSONWriter *o) { + write_arr_elem(o); + write_obj_start(o); +} + +static void write_arr_elem_arr_start(JSONWriter *o) { + write_arr_elem(o); + 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_arr_elem_number(JSONWriter *o, double number) { + write_arr_elem(o); + 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_bool(JSONWriter *o, bool b) { + str_builder_append(&o->builder, b ? "true" : "false"); +} + +static void write_key_bool(JSONWriter *o, const char *key, bool b) { + write_key(o, key); + write_bool(o, b); +} + +static void write_arr_elem_null(JSONWriter *o) { + write_arr_elem(o); + 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_arr_elem_string(JSONWriter *o, const char *s) { + write_arr_elem(o); + write_string(o, s); +} + +static void write_file_uri(JSONWriter *o, LSPDocumentID document) { + if (document >= arr_len(o->lsp->document_data)) { + assert(0); + str_builder_append(&o->builder, "\"\""); + return; + } + const char *path = o->lsp->document_data[document].path; + 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, LSPDocumentID 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; +} + +// NOTE: don't call lsp_request_free after calling this function. +// I will do it for you. +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_key_obj_start(o, "textDocument"); + write_key_obj_start(o, "completion"); + // completion capabilities + write_key_obj_start(o, "completionItem"); + write_key_bool(o, "snippetSupport", false); + write_key_bool(o, "commitCharactersSupport", false); + write_key_arr_start(o, "documentationFormat"); + // we dont really support markdown + write_arr_elem_string(o, "plaintext"); + write_arr_end(o); + write_key_bool(o, "deprecatedSupport", true); + write_key_bool(o, "preselectSupport", false); + write_key_obj_start(o, "tagSupport"); + write_key_arr_start(o, "valueSet"); + // currently the only tag in the spec + write_arr_elem_number(o, 1); + write_arr_end(o); + write_obj_end(o); + write_key_bool(o, "insertReplaceSupport", false); + write_obj_end(o); + // "completion item kinds" supported by ted + // (these are the little icons displayed for function/variable/etc.) + write_key_obj_start(o, "completionItemKind"); + write_key_arr_start(o, "valueSet"); + for (int i = LSP_COMPLETION_KIND_MIN; + i <= LSP_COMPLETION_KIND_MAX; ++i) { + write_arr_elem_number(o, i); + } + write_arr_end(o); + write_obj_end(o); + write_obj_end(o); + write_obj_end(o); + write_obj_end(o); + write_key_null(o, "rootUri"); + write_key_null(o, "workspaceFolders"); + write_key_obj_start(o, "clientInfo"); + write_key_string(o, "name", "ted"); + write_obj_end(o); + 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", 0); + 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; + if (change->document >= arr_len(lsp->document_data)) { + assert(0); + break; + } + LSPDocumentData *document = &lsp->document_data[change->document]; + ++document->version_number; + write_key_obj_start(o, "params"); + write_key_obj_start(o, "textDocument"); + write_key_number(o, "version", (double)document->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); + } +} |