summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lsp-write-request.c303
-rw-r--r--lsp.c182
-rw-r--r--lsp.h5
-rw-r--r--main.c6
4 files changed, 314 insertions, 182 deletions
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);