diff options
Diffstat (limited to 'lsp.c')
-rw-r--r-- | lsp.c | 339 |
1 files changed, 339 insertions, 0 deletions
@@ -0,0 +1,339 @@ +// @TODO: maximum queue size for requests/responses just in case? + +typedef enum { + LSP_NONE, + LSP_INITIALIZE, + LSP_INITIALIZED, + LSP_OPEN, + LSP_COMPLETION, +} LSPRequestType; + +typedef struct { + // buffer language + Language language; + // these will be free'd. + char *filename; + char *file_contents; +} LSPRequestOpen; + +typedef struct { + LSPRequestType type; + union { + LSPRequestOpen open; + } data; +} LSPRequest; + +typedef struct { + Process process; + u64 request_id; + JSON *responses; + SDL_mutex *responses_mutex; + LSPRequest *requests; + 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 + char error[256]; +} LSP; + + +static void write_request_content(LSP *lsp, const char *content) { + char header[128]; + size_t content_size = strlen(content); + strbuf_printf(header, "Content-Length: %zu\r\n\r\n", content_size); + #if 0 + printf("\x1b[1m%s%s\x1b[0m\n", header, content); + #endif + process_write(&lsp->process, header, strlen(header)); + process_write(&lsp->process, content, content_size); +} + +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 write_request(LSP *lsp, const LSPRequest *request) { + unsigned long long id = lsp->request_id++; + + + switch (request->type) { + case LSP_NONE: + assert(0); + break; + case LSP_INITIALIZE: { + char content[1024]; + strbuf_printf(content, + "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"initialize\",\"params\":{" + "\"processId\":%d," + "\"capabilities\":{}" + "}}", id, process_get_id()); + write_request_content(lsp, content); + } break; + case LSP_INITIALIZED: { + char content[1024]; + strbuf_printf(content, + "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"initialized\",\"params\":{" + "}}", id); + write_request_content(lsp, content); + } break; + case LSP_OPEN: { + const LSPRequestOpen *open = &request->data.open; + char *escaped_filename = json_escape(open->filename); + char *did_open = a_sprintf( + "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"textDocument/open\",\"params\":{" + "textDocument:{" + "uri:\"file://%s\"," + "languageId:\"%s\"," + "version:1," + "text:\"", + id, escaped_filename, lsp_language_id(open->language)); + free(escaped_filename); + + size_t did_open_sz = strlen(did_open) + 2 * strlen(open->file_contents) + 16; + did_open = realloc(did_open, did_open_sz); + + size_t n = json_escape_to(did_open + strlen(did_open), + did_open_sz - 10 - strlen(did_open), + open->file_contents); + char *p = did_open + n; + sprintf(p, "\"}}}"); + + free(did_open); + + write_request_content(lsp, did_open); + } break; + case LSP_COMPLETION: { + char content[1024]; + // no params needed + strbuf_printf(content, + "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"initialize\",\"params\":{" + "}}", id); + write_request_content(lsp, content); + } break; + } +} + +// 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, *request); + SDL_UnlockMutex(lsp->requests_mutex); +} + +// receive responses 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)) { + char *copy = strn_dup(lsp->received_data + response_offset, response_size); + JSON json = {0}; + json_parse(&json, copy); + assert(json.text == copy); + json.is_text_copied = true; + + JSONValue result = json_get(&json, "result"); + if (result.type == JSON_UNDEFINED) { + // uh oh + JSONValue error = json_get(&json, "error.message"); + if (error.type == JSON_STRING) { + json_string_get(&json, &error.val.string, lsp->error, sizeof lsp->error); + } else { + strbuf_printf(lsp->error, "Server error (no message)"); + } + } else { + SDL_LockMutex(lsp->responses_mutex); + arr_add(lsp->responses, json); + SDL_UnlockMutex(lsp->responses_mutex); + } + + size_t leftover_data_len = arr_len(lsp->received_data) - (response_offset + response_size); + 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'; + } +} + +static void free_request(LSPRequest *r) { + switch (r->type) { + case LSP_NONE: + assert(0); + break; + case LSP_INITIALIZE: + case LSP_INITIALIZED: + case LSP_COMPLETION: + break; + case LSP_OPEN: { + LSPRequestOpen *open = &r->data.open; + free(open->filename); + free(open->file_contents); + } break; + } +} + +// 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); + requests = calloc(n_requests, sizeof *requests); + memcpy(requests, lsp->requests, n_requests * sizeof *requests); + 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); + } + free_request(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; + } + return 0; +} + +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_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); + + lsp->quit_sem = SDL_CreateSemaphore(0); + lsp->responses_mutex = SDL_CreateMutex(); + lsp->communication_thread = SDL_CreateThread(lsp_communication_thread, "LSP communicate", lsp); + return true; +} + +bool lsp_next_response(LSP *lsp, JSON *json) { + bool any = false; + SDL_LockMutex(lsp->responses_mutex); + if (arr_len(lsp->responses)) { + *json = lsp->responses[0]; + arr_remove(lsp->responses, 0); + any = true; + } + SDL_UnlockMutex(lsp->responses_mutex); + return any; +} + +void lsp_free(LSP *lsp) { + SDL_SemPost(lsp->quit_sem); + SDL_WaitThread(lsp->communication_thread, NULL); + SDL_DestroySemaphore(lsp->quit_sem); + process_kill(&lsp->process); + arr_free(lsp->received_data); +} |