summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--arr.c30
-rw-r--r--base.h19
-rw-r--r--json.c667
-rw-r--r--lsp.c801
-rw-r--r--main.c68
-rw-r--r--process-posix.c168
-rw-r--r--process-win.c2
-rw-r--r--process.h22
-rw-r--r--test.rs1
-rw-r--r--util.c114
11 files changed, 1819 insertions, 77 deletions
diff --git a/Makefile b/Makefile
index 92123c8..eb73aa5 100644
--- a/Makefile
+++ b/Makefile
@@ -2,9 +2,9 @@ ALL_CFLAGS=$(CFLAGS) -Wall -Wextra -Wshadow -Wconversion -Wpedantic -pedantic -s
-Wno-unused-function -Wno-fixed-enum-extension -Wimplicit-fallthrough -Wno-format-truncation -Wno-unknown-warning-option \
-Ipcre2
LIBS=-lSDL2 -lGL -lm libpcre2-32.a
-DEBUG_CFLAGS=$(ALL_CFLAGS) -DDEBUG -O0 -g
+DEBUG_CFLAGS=$(ALL_CFLAGS) -Wno-unused-parameter -DDEBUG -O0 -g
RELEASE_CFLAGS=$(ALL_CFLAGS) -O3
-PROFILE_CFLAGS=$(ALL_CFLAGS) -O3 -DPROFILE=1
+PROFILE_CFLAGS=$(ALL_CFLAGS) -O3 -g -DPROFILE=1
# if you change the directories below, ted won't work.
# we don't yet have support for using different data directories
GLOBAL_DATA_DIR=/usr/share/ted
diff --git a/arr.c b/arr.c
index d092e68..2ca394c 100644
--- a/arr.c
+++ b/arr.c
@@ -1,27 +1,5 @@
#ifndef ARR_C_
#define ARR_C_
-/*
-This is free and unencumbered software released into the public domain.
-Anyone is free to copy, modify, publish, use, compile, sell, or
-distribute this software, either in source code form or as a compiled
-binary, for any purpose, commercial or non-commercial, and by any
-means.
-In jurisdictions that recognize copyright laws, the author or authors
-of this software dedicate any and all copyright interest in the
-software to the public domain. We make this dedication for the benefit
-of the public at large and to the detriment of our heirs and
-successors. We intend this dedication to be an overt act of
-relinquishment in perpetuity of all present and future rights to this
-software under copyright law.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-For more information, please refer to <http://unlicense.org/>
-*/
// functions in this file suffixed with _ are not meant to be used outside here, unless you
// know what you're doing
@@ -29,6 +7,9 @@ For more information, please refer to <http://unlicense.org/>
// IMPORTANT NOTE: If you are using this with structures containing `long double`s, do
// #define ARR_LONG_DOUBLE
// before including this file
+// ( otherwise the long doubles will not be aligned.
+// this does mean that arrays waste 8 bytes of memory.
+// which isnt important unless you're making a lot of arrays.)
#include <stddef.h>
typedef union {
@@ -60,6 +41,10 @@ static inline u32 arr_len(void *arr) {
return arr ? arr_hdr_(arr)->len : 0;
}
+static inline u32 arr_cap(void *arr) {
+ return arr ? arr_hdr_(arr)->cap : 0;
+}
+
static inline unsigned arr_lenu(void *arr) {
return (unsigned)arr_len(arr);
}
@@ -114,6 +99,7 @@ static void arr_reserve_(void **arr, size_t member_size, size_t n) {
if (!*arr) {
// create a new array with capacity n+1
+ // why n+1? i dont know i wrote this a while ago
ArrHeader *hdr = calloc(1, sizeof(ArrHeader) + (n+1) * member_size);
if (hdr) {
hdr->cap = (u32)n+1;
diff --git a/base.h b/base.h
index ee427fc..5ba03bc 100644
--- a/base.h
+++ b/base.h
@@ -111,10 +111,29 @@ typedef unsigned long long ullong;
#define WarnUnusedResult
#endif
+#if __GNUC__
+#define ATTRIBUTE_PRINTF(fmt_idx, arg_idx) __attribute__ ((format(printf, fmt_idx, arg_idx)))
+#else
+#define ATTRIBUTE_PRINTF(fmt_idx, arg_idx)
+#endif
+#if _MSC_VER > 1400
+#define PRINTF_FORMAT_STRING _Printf_format_string_
+#else
+#define PRINTF_FORMAT_STRING
+#endif
+
#define Status bool WarnUnusedResult // false = error, true = success
#define arr_count(a) (sizeof (a) / sizeof *(a))
+
+// usage: if UNLIKELY (x > 2) ...
+#if __GNUC__
+#define UNLIKELY(x) (__builtin_expect(x,0))
+#else
+#define UNLIKELY(x) (x)
+#endif
+
#ifdef __GNUC__
#define no_warn_start _Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wpedantic\"") \
diff --git a/json.c b/json.c
new file mode 100644
index 0000000..09ef6e9
--- /dev/null
+++ b/json.c
@@ -0,0 +1,667 @@
+// JSON parser for LSP
+// provides FAST(ish) parsing but SLOW lookup
+// this is especially fast for small objects
+// this actually supports "extended json", where objects can have arbitrary values as keys.
+
+typedef struct {
+ u32 pos;
+ u32 len;
+} JSONString;
+
+typedef struct JSONValue JSONValue;
+
+typedef struct {
+ u32 len;
+ // this is an index into the values array
+ // values[items..items+len] store the names
+ // values[items+len..items+2*len] store the values
+ u32 items;
+} JSONObject;
+
+typedef struct {
+ u32 len;
+ // this is an index into the values array
+ // values[elements..elements+len] are the elements
+ u32 elements;
+} JSONArray;
+
+typedef enum {
+ // note: json doesn't actually include undefined.
+ // this is only for returning things from json_get etc.
+ JSON_UNDEFINED,
+ JSON_NULL,
+ JSON_FALSE,
+ JSON_TRUE,
+ JSON_NUMBER,
+ JSON_STRING,
+ JSON_OBJECT,
+ JSON_ARRAY
+} JSONValueType;
+
+struct JSONValue {
+ JSONValueType type;
+ union {
+ double number;
+ JSONString string;
+ JSONArray array;
+ JSONObject object;
+ } val;
+};
+
+
+typedef struct {
+ char error[64];
+ bool is_text_copied; // if this is true, then json_free will call free on text
+ const char *text;
+ // root = values[0]
+ JSONValue *values;
+} JSON;
+
+#define SKIP_WHITESPACE while (json_is_space(text[index])) ++index;
+
+const char *json_type_to_str(JSONValueType type) {
+ switch (type) {
+ case JSON_UNDEFINED:
+ return "undefined";
+ case JSON_NULL:
+ return "null";
+ case JSON_STRING:
+ return "string";
+ case JSON_NUMBER:
+ return "number";
+ case JSON_FALSE:
+ return "false";
+ case JSON_TRUE:
+ return "true";
+ case JSON_ARRAY:
+ return "array";
+ case JSON_OBJECT:
+ return "object";
+ }
+}
+
+static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val);
+
+// defining this instead of using isspace seems to be faster
+// probably because isspace depends on the locale.
+static inline bool json_is_space(char c) {
+ return c == ' ' || c == '\n' || c == '\r' || c == '\t';
+}
+
+static void json_debug_print_value(const JSON *json, const JSONValue *value) {
+ switch (value->type) {
+ case JSON_UNDEFINED: printf("undefined"); break;
+ case JSON_NULL: printf("null"); break;
+ case JSON_FALSE: printf("false"); break;
+ case JSON_TRUE: printf("true"); break;
+ case JSON_NUMBER: printf("%g", value->val.number); break;
+ case JSON_STRING: {
+ const JSONString *string = &value->val.string;
+ printf("\"%.*s\"",
+ (int)string->len,
+ json->text + string->pos);
+ } break;
+ case JSON_ARRAY: {
+ const JSONArray *array = &value->val.array;
+ printf("[");
+ for (u32 i = 0; i < array->len; ++i) {
+ json_debug_print_value(json, &json->values[array->elements + i]);
+ printf(", ");
+ }
+ printf("]");
+ } break;
+ case JSON_OBJECT: {
+ const JSONObject *obj = &value->val.object;
+ printf("{");
+ for (u32 i = 0; i < obj->len; ++i) {
+ json_debug_print_value(json, &json->values[obj->items + i]);
+ printf(": ");
+ json_debug_print_value(json, &json->values[obj->items + obj->len + i]);
+ printf(", ");
+ }
+ printf("}");
+ } break;
+ }
+}
+
+// count number of comma-separated values until
+// closing ] or }
+static u32 json_count(JSON *json, u32 index) {
+ int bracket_depth = 0;
+ int brace_depth = 0;
+ u32 count = 1;
+ const char *text = json->text;
+ SKIP_WHITESPACE;
+ // special case: empty object/array
+ if (text[index] == '}' || text[index] == ']')
+ return 0;
+
+// int mark[5000] = {0};
+ for (; ; ++index) {
+ switch (text[index]) {
+ case '\0':
+ return 0; // bad no closing bracket
+ case '[':
+ ++bracket_depth;
+ break;
+ case ']':
+ --bracket_depth;
+ if (bracket_depth < 0)
+ return count;
+ break;
+ case '{':
+ ++brace_depth;
+// mark[index] = 1;
+ break;
+ case '}':
+ --brace_depth;
+// mark[index] = 1;
+ if (brace_depth < 0){
+ // useful visualization for debugging
+// for (int i = 0; text[i]; ++i ){
+// switch (mark[i]){
+// case 1: printf("\x1b[91m"); break;
+// case 2: printf("\x1b[92m"); break;
+// }
+// printf("%c",text[i]);
+// if (mark[i]) printf("\x1b[0m");
+// }
+// printf("\n");
+
+ return count;
+ }
+ break;
+ case ',':
+ if (bracket_depth == 0 && brace_depth == 0)
+ ++count;
+ break;
+ case '"': {
+ ++index; // skip opening "
+ int escaped = 0;
+ for (; ; ++index) {
+// mark[index] = 2;
+ switch (text[index]) {
+ case '\0': return 0; // bad no closing quote
+ case '\\': escaped = !escaped; break;
+ case '"':
+ if (!escaped)
+ goto done;
+ escaped = false;
+ break;
+ default:
+ escaped = false;
+ break;
+ }
+ }
+ done:;
+ } break;
+ }
+ }
+}
+
+static bool json_parse_object(JSON *json, u32 *p_index, JSONObject *object) {
+ u32 index = *p_index;
+ const char *text = json->text;
+ ++index; // go past {
+ u32 count = json_count(json, index);
+ object->len = count;
+ object->items = arr_len(json->values);
+ arr_set_len(json->values, arr_len(json->values) + 2 * count);
+
+ for (u32 i = 0; i < count; ++i) {
+ if (i > 0) {
+ if (text[index] != ',') {
+ strbuf_printf(json->error, "stuff after value in object");
+ return false;
+ }
+ ++index;
+ }
+ SKIP_WHITESPACE;
+ JSONValue name = {0}, value = {0};
+ if (!json_parse_value(json, &index, &name))
+ return false;
+ SKIP_WHITESPACE;
+ if (text[index] != ':') {
+ strbuf_printf(json->error, "stuff after name in object");
+ return false;
+ }
+ ++index; // skip :
+ SKIP_WHITESPACE;
+ if (!json_parse_value(json, &index, &value))
+ return false;
+ SKIP_WHITESPACE;
+ json->values[object->items + i] = name;
+ json->values[object->items + count + i] = value;
+ }
+
+ if (text[index] != '}') {
+ strbuf_printf(json->error, "mismatched brackets or quotes.");
+ return false;
+ }
+ ++index; // skip }
+ *p_index = index;
+ return true;
+}
+
+static bool json_parse_array(JSON *json, u32 *p_index, JSONArray *array) {
+ u32 index = *p_index;
+ const char *text = json->text;
+ ++index; // go past [
+ u32 count = json_count(json, index);
+ array->len = count;
+ array->elements = arr_len(json->values);
+
+ arr_set_len(json->values, arr_len(json->values) + count);
+
+ SKIP_WHITESPACE;
+
+ for (u32 i = 0; i < count; ++i) {
+ if (i > 0) {
+ if (text[index] != ',') {
+ strbuf_printf(json->error, "stuff after element in array");
+ return false;
+ }
+ ++index;
+ }
+ SKIP_WHITESPACE;
+ JSONValue element = {0};
+ if (!json_parse_value(json, &index, &element))
+ return false;
+ SKIP_WHITESPACE;
+ json->values[array->elements + i] = element;
+ }
+
+ if (text[index] != ']') {
+ strbuf_printf(json->error, "mismatched brackets or quotes.");
+ return false;
+ }
+ ++index; // skip ]
+ *p_index = index;
+ return true;
+}
+
+static bool json_parse_string(JSON *json, u32 *p_index, JSONString *string) {
+ u32 index = *p_index;
+ ++index; // skip opening "
+ string->pos = index;
+ const char *text = json->text;
+ bool escaped = false;
+ for (; ; ++index) {
+ switch (text[index]) {
+ case '"':
+ if (!escaped)
+ goto done;
+ escaped = false;
+ break;
+ case '\\':
+ escaped = !escaped;
+ break;
+ case '\0':
+ strbuf_printf(json->error, "string literal goes to end of JSON");
+ return false;
+ default:
+ escaped = false;
+ break;
+ }
+ }
+ done:
+ string->len = index - string->pos;
+ ++index; // skip closing "
+ *p_index = index;
+ return true;
+}
+
+static bool json_parse_number(JSON *json, u32 *p_index, double *number) {
+ char *endp = NULL;
+ const char *text = json->text;
+ u32 index = *p_index;
+ *number = strtod(text + index, &endp);
+ if (endp == text + index) {
+ strbuf_printf(json->error, "bad number");
+ return false;
+ }
+ index = (u32)(endp - text);
+ *p_index = index;
+ return true;
+}
+
+static bool json_parse_value(JSON *json, u32 *p_index, JSONValue *val) {
+ const char *text = json->text;
+ u32 index = *p_index;
+ SKIP_WHITESPACE;
+ switch (text[index]) {
+ case '{':
+ val->type = JSON_OBJECT;
+ if (!json_parse_object(json, &index, &val->val.object))
+ return false;
+ break;
+ case '[':
+ val->type = JSON_ARRAY;
+ if (!json_parse_array(json, &index, &val->val.array))
+ return false;
+ break;
+ case '"':
+ val->type = JSON_STRING;
+ if (!json_parse_string(json, &index, &val->val.string))
+ return false;
+ break;
+ case ANY_DIGIT:
+ case '-':
+ case '+':
+ val->type = JSON_NUMBER;
+ if (!json_parse_number(json, &index, &val->val.number))
+ return false;
+ break;
+ case 'f':
+ val->type = JSON_FALSE;
+ if (!str_has_prefix(&text[index], "false"))
+ return false;
+ index += 5;
+ break;
+ case 't':
+ val->type = JSON_TRUE;
+ if (!str_has_prefix(&text[index], "true"))
+ return false;
+ index += 4;
+ break;
+ case 'n':
+ val->type = JSON_NULL;
+ if (!str_has_prefix(&text[index], "null"))
+ return false;
+ index += 4;
+ break;
+ default:
+ strbuf_printf(json->error, "bad value");
+ return false;
+ }
+ *p_index = index;
+ return true;
+}
+
+void json_free(JSON *json) {
+ arr_free(json->values);
+ // important we don't zero json here because we want to preserve json->error.
+ if (json->is_text_copied) {
+ free((void*)json->text);
+ }
+ json->text = NULL;
+}
+
+// NOTE: text must live as long as json!!!
+bool json_parse(JSON *json, const char *text) {
+ memset(json, 0, sizeof *json);
+ json->text = text;
+ arr_reserve(json->values, strlen(text) / 8);
+ arr_addp(json->values); // add root
+ JSONValue val = {0};
+ u32 index = 0;
+ if (!json_parse_value(json, &index, &val)) {
+ json_free(json);
+ return false;
+ }
+ SKIP_WHITESPACE;
+ if (text[index]) {
+ json_free(json);
+ strbuf_printf(json->error, "extra text after end of root object");
+ return false;
+ }
+ json->values[0] = val;
+ return true;
+}
+
+// like json_parse, but a copy of text is made, so you can free/overwrite it immediately.
+bool json_parse_copy(JSON *json, const char *text) {
+ bool success = json_parse(json, str_dup(text));
+ if (success) {
+ json->is_text_copied = true;
+ return true;
+ } else {
+ free((void*)json->text);
+ json->text = NULL;
+ return false;
+ }
+}
+
+static bool json_streq(const JSON *json, const JSONString *string, const char *name) {
+ const char *p = &json->text[string->pos];
+ const char *end = p + string->len;
+ for (; p < end; ++p, ++name) {
+ if (*name != *p)
+ return false;
+ }
+ return *name == '\0';
+}
+
+// returns undefined if the property `name` does not exist.
+JSONValue json_object_get(const JSON *json, const JSONObject *object, const char *name) {
+ const JSONValue *items = &json->values[object->items];
+ for (u32 i = 0; i < object->len; ++i) {
+ const JSONValue *this_name = items++;
+ if (this_name->type == JSON_STRING && json_streq(json, &this_name->val.string, name)) {
+ return json->values[object->items + object->len + i];
+ }
+ }
+ return (JSONValue){0};
+}
+
+JSONValue json_array_get(const JSON *json, const JSONArray *array, u64 i) {
+ if (i < array->len) {
+ return json->values[array->elements + i];
+ }
+ return (JSONValue){0};
+}
+
+// e.g. if json is { "a" : { "b": 3 }}, then json_get(json, "a.b") = 3.
+// returns undefined if there is no such property
+JSONValue json_get(const JSON *json, const char *path) {
+ char segment[128];
+ const char *p = path;
+ if (!json->values) {
+ return (JSONValue){0};
+ }
+ JSONValue curr_value = json->values[0];
+ while (*p) {
+ size_t segment_len = strcspn(p, ".");
+ strn_cpy(segment, sizeof segment, p, segment_len);
+ if (curr_value.type != JSON_OBJECT) {
+ return (JSONValue){0};
+ }
+ curr_value = json_object_get(json, &curr_value.val.object, segment);
+ p += segment_len;
+ if (*p == '.') ++p;
+ }
+ return curr_value;
+}
+
+// equivalent to json_get(json, path).type != JSON_UNDEFINED, but more readable
+bool json_has(const JSON *json, const char *path) {
+ JSONValue value = json_get(json, path);
+ return value.type != JSON_UNDEFINED;
+}
+
+// turn a json string into a null terminated string.
+// this won't be nice if the json string includes \u0000 but that's rare.
+// if buf_sz > string->len, the string will fit.
+void json_string_get(const JSON *json, JSONString string, char *buf, size_t buf_sz) {
+ const char *text = json->text;
+ if (buf_sz == 0) {
+ assert(0);
+ return;
+ }
+ char *buf_end = buf + buf_sz - 1;
+ for (u32 i = string.pos, end = string.pos + string.len; i < end && buf < buf_end; ++i) {
+ if (text[i] != '\\') {
+ *buf++ = text[i];
+ } else {
+ ++i;
+ if (i >= end) break;
+ // escape sequence
+ switch (text[i]) {
+ case 'n': *buf++ = '\n'; break;
+ case 'r': *buf++ = '\r'; break;
+ case 'b': *buf++ = '\b'; break;
+ case 't': *buf++ = '\t'; break;
+ case 'f': *buf++ = '\f'; break;
+ case '\\': *buf++ = '\\'; break;
+ case '/': *buf++ = '/'; break;
+ case '"': *buf++ = '"'; break;
+ case 'u': {
+ if ((buf_end - buf) < 4 || i + 5 > end)
+ goto brk;
+ ++i;
+
+ char hex[5] = {0};
+ hex[0] = text[i++];
+ hex[1] = text[i++];
+ hex[2] = text[i++];
+ hex[3] = text[i++];
+ unsigned code_point=0;
+ sscanf(hex, "%04x", &code_point);
+ // technically this won't deal with people writing out UTF-16 surrogate halves
+ // using \u. i dont care.
+ size_t n = unicode_utf32_to_utf8(buf, code_point);
+ if (n <= 4) buf += n;
+ } break;
+ }
+ }
+ }
+ brk:
+ *buf = '\0';
+}
+
+// returns a malloc'd null-terminated string.
+static char *json_string_get_alloc(const JSON *json, JSONString string) {
+ u32 n = string.len + 1;
+ if (n == 0) --n; // extreme edge case
+ char *buf = calloc(1, n);
+ json_string_get(json, string, buf, n);
+ return buf;
+}
+
+
+#if __unix__
+static void json_test_time_large(const char *filename) {
+ struct timespec start={0},end={0};
+ FILE *fp = fopen(filename,"rb");
+ if (!fp) {
+ perror(filename);
+ return;
+ }
+
+ fseek(fp,0,SEEK_END);
+ size_t sz = (size_t)ftell(fp);
+ char *buf = calloc(1,sz+1);
+ rewind(fp);
+ fread(buf, 1, sz, fp);
+ fclose(fp);
+ for (int trial = 0; trial < 5; ++trial) {
+ clock_gettime(CLOCK_MONOTONIC, &start);
+ JSON json={0};
+ bool success = json_parse(&json, buf);
+ if (!success) {
+ printf("FAIL: %s\n",json.error);
+ return;
+ }
+
+ json_free(&json);
+ clock_gettime(CLOCK_MONOTONIC, &end);
+
+
+
+ printf("time: %.1fms\n",
+ ((double)end.tv_sec*1e3+(double)end.tv_nsec*1e-6)
+ -((double)start.tv_sec*1e3+(double)start.tv_nsec*1e-6));
+ }
+
+}
+static void json_test_time_small(void) {
+ struct timespec start={0},end={0};
+ int trials = 50000000;
+ clock_gettime(CLOCK_MONOTONIC, &start);
+ for (int trial = 0; trial < trials; ++trial) {
+ JSON json={0};
+ bool success = json_parse(&json, "{\"hello\":\"there\"}");
+ if (!success) {
+ printf("FAIL: %s\n",json.error);
+ return;
+ }
+
+ json_free(&json);
+ }
+ clock_gettime(CLOCK_MONOTONIC, &end);
+ printf("time per trial: %.1fns\n",
+ (((double)end.tv_sec*1e9+(double)end.tv_nsec)
+ -((double)start.tv_sec*1e9+(double)start.tv_nsec))
+ / trials);
+
+}
+#endif
+
+void json_debug_print(const JSON *json) {
+ printf("%u values (capacity %u, text length %zu)\n",
+ arr_len(json->values), arr_cap(json->values), strlen(json->text));
+ json_debug_print_value(json, &json->values[0]);
+}
+
+// e.g. converts "Hello\nworld" to "Hello\\nworld"
+// if out_sz is at least 2 * strlen(str) + 1, the string will fit.
+void json_escape_to(char *out, size_t out_sz, const char *in) {
+ char *end = out + out_sz;
+ assert(out_sz);
+
+ --end; // leave room for null terminator
+
+ for (; *in; ++in) {
+ if (out + 1 > end) {
+ break;
+ }
+ char esc = '\0';
+ switch (*in) {
+ case '\0': goto brk;
+ case '\n':
+ esc = 'n';
+ goto escape;
+ case '\\':
+ esc = '\\';
+ goto escape;
+ case '"':
+ esc = '"';
+ goto escape;
+ case '\t':
+ esc = 't';
+ goto escape;
+ case '\r':
+ esc = 'r';
+ goto escape;
+ case '\f':
+ esc = 'f';
+ goto escape;
+ case '\b':
+ esc = 'b';
+ goto escape;
+ escape:
+ if (out + 2 > end)
+ goto brk;
+ *out++ = '\\';
+ *out++ = esc;
+ break;
+ default:
+ *out = *in;
+ ++out;
+ break;
+ }
+ }
+ brk:
+ *out = '\0';
+}
+
+// e.g. converts "Hello\nworld" to "Hello\\nworld"
+// the resulting string should be free'd.
+char *json_escape(const char *str) {
+ size_t out_sz = 2 * strlen(str) + 1;
+ char *out = calloc(1, out_sz);
+ json_escape_to(out, out_sz, str);
+ return out;
+}
+
+#undef SKIP_WHITESPACE
diff --git a/lsp.c b/lsp.c
new file mode 100644
index 0000000..61ba014
--- /dev/null
+++ b/lsp.c
@@ -0,0 +1,801 @@
+// @TODO:
+// - make json_object/array_get take value not pointer
+// - documentation
+// - make sure offsets are utf-16!
+// - maximum queue size for requests/responses just in case?
+
+typedef enum {
+ LSP_REQUEST,
+ LSP_RESPONSE
+} LSPMessageType;
+
+typedef enum {
+ LSP_NONE,
+
+ // client-to-server
+ LSP_INITIALIZE,
+ LSP_INITIALIZED,
+ LSP_OPEN,
+ LSP_COMPLETION,
+ LSP_SHUTDOWN,
+ LSP_EXIT,
+
+ // server-to-client
+ LSP_SHOW_MESSAGE,
+ LSP_LOG_MESSAGE
+} LSPRequestType;
+
+typedef struct {
+ // buffer language
+ Language language;
+ // freed by lsp_request_free
+ char *filename;
+ // freed by lsp_request_free
+ char *file_contents;
+} LSPRequestOpen;
+
+typedef enum {
+ ERROR = 1,
+ WARNING = 2,
+ INFO = 3,
+ LOG = 4
+} LSPWindowMessageType;
+
+typedef struct {
+ LSPWindowMessageType type;
+ // freed by lsp_request_free
+ char *message;
+} LSPRequestMessage;
+
+typedef struct {
+ // freed by lsp_request_free
+ char *path;
+ u32 line;
+ u32 character;
+} LSPDocumentPosition;
+
+typedef struct {
+ LSPDocumentPosition position;
+} LSPRequestCompletion;
+
+typedef struct {
+ LSPRequestType type;
+ union {
+ LSPRequestOpen open;
+ LSPRequestCompletion completion;
+ // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE
+ LSPRequestMessage message;
+ } data;
+} LSPRequest;
+
+// info we want to keep track of about a request,
+// so we can deal with the response appropriately.
+typedef struct {
+ u64 id;
+ LSPRequestType type;
+} LSPRequestTrackedInfo;
+
+typedef struct {
+ u32 offset;
+} LSPResponseString;
+
+typedef struct {
+ LSPResponseString label;
+ // note: the items are sorted here in this file,
+ // so you probably don't need to access this.
+ LSPResponseString sort_text;
+} LSPCompletionItem;
+
+typedef struct {
+ // dynamic array
+ LSPCompletionItem *items;
+} LSPResponseCompletion;
+
+typedef LSPRequestType LSPResponseType;
+typedef struct {
+ LSPResponseType type;
+ // LSP responses tend to have a lot of strings.
+ // to avoid doing a ton of allocations+frees,
+ // they're all stored here.
+ char *string_data;
+ union {
+ LSPResponseCompletion completion;
+ } data;
+} LSPResponse;
+
+typedef struct {
+ LSPMessageType type;
+ union {
+ LSPRequest request;
+ LSPResponse response;
+ } u;
+} LSPMessage;
+
+typedef struct {
+ Process process;
+ u64 request_id;
+ LSPMessage *messages;
+ SDL_mutex *messages_mutex;
+ LSPRequest *requests_client2server;
+ LSPRequest *requests_server2client;
+ // only applicable for client-to-server requests
+ LSPRequestTrackedInfo *requests_tracked_info;
+ 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
+ SDL_mutex *error_mutex;
+ char error[256];
+} LSP;
+
+// returns true if there's an error.
+// returns false and sets error to "" if there's no error.
+// if clear = true, the error will be cleared.
+// you may set error = NULL, error_size = 0, clear = true to just clear the error
+bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) {
+ bool has_err = false;
+ SDL_LockMutex(lsp->error_mutex);
+ has_err = *lsp->error != '\0';
+ if (error_size)
+ str_cpy(error, error_size, lsp->error);
+ if (clear)
+ *lsp->error = '\0';
+ SDL_UnlockMutex(lsp->error_mutex);
+ 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 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) {
+ free(position->path);
+}
+
+static void lsp_request_free(LSPRequest *r) {
+ switch (r->type) {
+ case LSP_NONE:
+ assert(0);
+ break;
+ case LSP_INITIALIZE:
+ case LSP_INITIALIZED:
+ case LSP_SHUTDOWN:
+ case LSP_EXIT:
+ break;
+ case LSP_COMPLETION: {
+ LSPRequestCompletion *completion = &r->data.completion;
+ lsp_position_free(&completion->position);
+ } break;
+ case LSP_OPEN: {
+ LSPRequestOpen *open = &r->data.open;
+ free(open->filename);
+ free(open->file_contents);
+ } break;
+ case LSP_SHOW_MESSAGE:
+ case LSP_LOG_MESSAGE:
+ free(r->data.message.message);
+ break;
+ }
+}
+
+static void lsp_response_free(LSPResponse *r) {
+ arr_free(r->string_data);
+ switch (r->type) {
+ case LSP_COMPLETION:
+ arr_free(r->data.completion.items);
+ break;
+ default:
+ break;
+ }
+}
+
+void lsp_message_free(LSPMessage *message) {
+ switch (message->type) {
+ case LSP_REQUEST:
+ lsp_request_free(&message->u.request);
+ break;
+ case LSP_RESPONSE:
+ lsp_response_free(&message->u.response);
+ break;
+ }
+ 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);
+}
+
+// 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.
+// returns the ID of the request
+static void write_request(LSP *lsp, const 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_INITIALIZED
+ || request->type == LSP_EXIT;
+ if (!is_notification) {
+ unsigned long long id = lsp->request_id++;
+ str_builder_appendf(&builder, "\"id\":%llu,", id);
+ LSPRequestTrackedInfo info = {
+ .id = id,
+ .type = request->type
+ };
+ SDL_LockMutex(lsp->requests_mutex);
+ arr_add(lsp->requests_tracked_info, info);
+ SDL_UnlockMutex(lsp->requests_mutex);
+ }
+
+ switch (request->type) {
+ case LSP_NONE:
+ // these are server-to-client-only requests
+ case LSP_SHOW_MESSAGE:
+ case LSP_LOG_MESSAGE:
+ assert(0);
+ break;
+ case LSP_INITIALIZE: {
+ str_builder_appendf(&builder,
+ "\"method\":\"initialize\",\"params\":{"
+ "\"processId\":%d,"
+ "\"capabilities\":{}"
+ "}", process_get_id());
+ } break;
+ case LSP_INITIALIZED:
+ str_builder_append(&builder, "\"method\":\"initialized\"");
+ break;
+ case LSP_OPEN: {
+ const LSPRequestOpen *open = &request->data.open;
+ char *escaped_filename = json_escape(open->filename);
+ char *escaped_text = json_escape(open->file_contents);
+
+ str_builder_appendf(&builder,
+ "\"method\":\"textDocument/open\",\"params\":{"
+ "textDocument:{"
+ "uri:\"file://%s\","
+ "languageId:\"%s\","
+ "version:1,"
+ "text:\"%s\"}}",
+ escaped_filename,
+ lsp_language_id(open->language),
+ escaped_text);
+ free(escaped_text);
+ free(escaped_filename);
+ } break;
+ case LSP_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_SHUTDOWN:
+ str_builder_append(&builder, "\"method\":\"shutdown\"");
+ break;
+ case LSP_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);
+}
+
+// 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_client2server, *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_SHOW_MESSAGE;
+ goto window_message;
+ } else if (streq(method, "window/logMessage")) {
+ request->type = LSP_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, LSPResponseString string) {
+ assert(string.offset < arr_len(response->string_data));
+ return &response->string_data[string.offset];
+}
+
+static LSPResponseString 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 (LSPResponseString){
+ .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.
+ 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_completion(LSP *lsp, const JSON *json, LSPResponse *response) {
+ // deal with textDocument/completion response.
+ // result: CompletionItem[] | CompletionList | null
+ response->type = LSP_COMPLETION;
+ 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;
+
+ JSONString sort_text = 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.
+ sort_text = sort_text_value.val.string;
+ }
+
+ item->label = lsp_response_add_json_string(response, json, label);
+ item->sort_text = lsp_response_add_json_string(response, json, sort_text);
+ }
+
+ 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 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);;
+ lsp_set_error(lsp, "%s", err);
+ goto ret;
+ }
+
+ JSONValue result = json_get(json, "result");
+ if (result.type != JSON_UNDEFINED) {
+ JSONValue id = json_get(json, "id");
+
+ if (id.type != JSON_NUMBER) {
+ // what
+ lsp_set_error(lsp, "Response with no ID.");
+ goto ret;
+ }
+ if (id.val.number == 0) {
+ // 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_INITIALIZED,
+ .data = {0},
+ };
+ write_request(lsp, &initialized);
+ // we can now send requests which have nothing to do with initialization
+ lsp->initialized = true;
+ } else {
+ u64 id_no = (u64)id.val.number;
+ LSPRequestTrackedInfo tracked_info = {0};
+ SDL_LockMutex(lsp->requests_mutex);
+ arr_foreach_ptr(lsp->requests_tracked_info, LSPRequestTrackedInfo, info) {
+ if (info->id == id_no) {
+ // hey its the thing
+ tracked_info = *info;
+ arr_remove(lsp->requests_tracked_info, (u32)(info - lsp->requests_tracked_info));
+ break;
+ }
+ }
+ SDL_UnlockMutex(lsp->requests_mutex);
+
+ LSPResponse response = {0};
+ bool success = false;
+ switch (tracked_info.type) {
+ case LSP_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);
+ } 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:
+ json_free(json);
+
+}
+
+// receive responses/requests/notifications 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 1
+ 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};
+ if (json_parse(&json, copy)) {
+ assert(json.text == copy);
+ json.is_text_copied = true;
+ process_message(lsp, &json);
+ } else {
+ lsp_set_error(lsp, "couldn't parse response JSON: %s", json.error);
+ json_free(&json);
+ }
+ 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';
+ }
+}
+
+// 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_client2server);
+ requests = calloc(n_requests, sizeof *requests);
+ memcpy(requests, lsp->requests_client2server, n_requests * sizeof *requests);
+ arr_clear(lsp->requests_client2server);
+ 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);
+ }
+ lsp_request_free(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;
+ }
+
+ if (lsp->initialized) {
+ LSPRequest shutdown = {
+ .type = LSP_SHUTDOWN,
+ .data = {0}
+ };
+ LSPRequest exit = {
+ .type = LSP_EXIT,
+ .data = {0}
+ };
+ write_request(lsp, &shutdown);
+ // i give you ONE MILLISECOND to send your fucking shutdown response
+ time_sleep_ms(1);
+ write_request(lsp, &exit);
+ // i give you ONE MILLISECOND to terminate
+ // I WILL KILL YOU IF IT TAKES ANY LONGER
+ time_sleep_ms(1);
+
+ #if 1
+ char buf[1024]={0};
+ long long n = process_read(&lsp->process, buf, sizeof buf);
+ if (n>0) {
+ buf[n]=0;
+ printf("%s\n",buf);
+ }
+ n = process_read_stderr(&lsp->process, buf, sizeof buf);
+ if (n>0) {
+ buf[n]=0;
+ printf("\x1b[1m%s\x1b[0m\n",buf);
+ }
+ #endif
+ }
+ 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->messages_mutex = SDL_CreateMutex();
+ lsp->requests_mutex = SDL_CreateMutex();
+ lsp->communication_thread = SDL_CreateThread(lsp_communication_thread, "LSP communicate", lsp);
+ return true;
+}
+
+bool lsp_next_message(LSP *lsp, LSPMessage *message) {
+ bool any = false;
+ SDL_LockMutex(lsp->messages_mutex);
+ if (arr_len(lsp->messages)) {
+ *message = lsp->messages[0];
+ arr_remove(lsp->messages, 0);
+ any = true;
+ }
+ SDL_UnlockMutex(lsp->messages_mutex);
+ return any;
+}
+
+void lsp_free(LSP *lsp) {
+ SDL_SemPost(lsp->quit_sem);
+ SDL_WaitThread(lsp->communication_thread, NULL);
+ SDL_DestroyMutex(lsp->messages_mutex);
+ SDL_DestroySemaphore(lsp->quit_sem);
+ process_kill(&lsp->process);
+ arr_free(lsp->received_data);
+ arr_foreach_ptr(lsp->messages, LSPMessage, message) {
+ lsp_message_free(message);
+ }
+ arr_free(lsp->messages);
+}
diff --git a/main.c b/main.c
index 027112d..385e73e 100644
--- a/main.c
+++ b/main.c
@@ -47,7 +47,9 @@ no_warn_end
#endif
#include "unicode.h"
+#include "arr.c"
#include "util.c"
+
#if _WIN32
#include "filesystem-win.c"
#elif __unix__
@@ -55,7 +57,6 @@ no_warn_end
#else
#error "Unrecognized operating system."
#endif
-#include "arr.c"
#include "math.c"
#if _WIN32
@@ -107,6 +108,8 @@ bool tag_goto(Ted *ted, char const *tag);
#include "command.c"
#include "config.c"
#include "session.c"
+#include "json.c"
+#include "lsp.c"
#if PROFILE
#define PROFILE_TIME(var) double var = time_get_seconds();
@@ -285,6 +288,69 @@ int main(int argc, char **argv) {
PROFILE_TIME(init_start)
PROFILE_TIME(basic_init_start)
+ // @TODO TEMPORARY
+ {
+ LSP lsp={0};
+ chdir("/p/test-lsp");
+ if (!lsp_create(&lsp, "rust-analyzer")) {
+ printf("lsp_create: %s\n",lsp.error);
+ exit(1);
+ }
+ usleep(1000000);//if we don't do this we get "waiting for cargo metadata or cargo check"
+ LSPRequest test_req = {.type = LSP_COMPLETION};
+ test_req.data.completion = (LSPRequestCompletion){
+ .position = {
+ .path = str_dup("/p/test-lsp/src/main.rs"),
+ .line = 2,
+ .character = 2,
+ }
+ };
+ lsp_send_request(&lsp, &test_req);
+ while (1) {
+ LSPMessage message = {0};
+ while (lsp_next_message(&lsp, &message)) {
+ if (message.type == LSP_RESPONSE) {
+ const LSPResponse *response = &message.u.response;
+ switch (response->type) {
+ case LSP_COMPLETION: {
+ const LSPResponseCompletion *completion = &response->data.completion;
+ arr_foreach_ptr(completion->items, LSPCompletionItem, item) {
+ printf("%s:%s\n",
+ lsp_response_string(response, item->sort_text),
+ lsp_response_string(response, item->label));
+ }
+ } break;
+ default:
+ break;
+ }
+ } else if (message.type == LSP_REQUEST) {
+ const LSPRequest *request = &message.u.request;
+ switch (request->type) {
+ case LSP_SHOW_MESSAGE: {
+ const LSPRequestMessage *m = &request->data.message;
+ // @TODO actually show
+ printf("Show (%d): %s\n", m->type, m->message);
+ } break;
+ case LSP_LOG_MESSAGE: {
+ const LSPRequestMessage *m = &request->data.message;
+ // @TODO actually log
+ printf("Log (%d): %s\n", m->type, m->message);
+ } break;
+ default: break;
+ }
+ }
+ lsp_message_free(&message);
+ }
+ char error[256];
+ if (lsp_get_error(&lsp, error, sizeof error, true)) {
+ printf("lsp error: %s\n", error);
+ }
+ usleep(10000);
+ }
+ lsp_free(&lsp);
+ exit(0);
+ }
+
#if __unix__
{
struct sigaction act = {0};
diff --git a/process-posix.c b/process-posix.c
index f35017f..c8ea376 100644
--- a/process-posix.c
+++ b/process-posix.c
@@ -6,55 +6,130 @@
struct Process {
pid_t pid;
- int pipe;
+ int stdout_pipe;
+ // only applicable if separate_stderr was specified.
+ int stderr_pipe;
+ int stdin_pipe;
char error[64];
};
-bool process_run(Process *proc, char const *command) {
+int process_get_id(void) {
+ return getpid();
+}
+
+static void set_nonblocking(int fd) {
+ fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
+}
+
+bool process_run_ex(Process *proc, const char *command, const ProcessSettings *settings) {
memset(proc, 0, sizeof *proc);
+ int stdin_pipe[2] = {0}, stdout_pipe[2] = {0}, stderr_pipe[2] = {0};
+ if (pipe(stdin_pipe) != 0) {
+ strbuf_printf(proc->error, "%s", strerror(errno));
+ return false;
+ }
+ if (pipe(stdout_pipe) != 0) {
+ strbuf_printf(proc->error, "%s", strerror(errno));
+ close(stdin_pipe[0]);
+ close(stdin_pipe[1]);
+ return false;
+ }
+ if (settings->separate_stderr) {
+ if (pipe(stderr_pipe) != 0) {
+ strbuf_printf(proc->error, "%s", strerror(errno));
+ close(stdin_pipe[0]);
+ close(stdin_pipe[1]);
+ close(stdout_pipe[0]);
+ close(stdout_pipe[1]);
+ return false;
+ }
+ }
+
bool success = false;
- int pipefd[2];
- if (pipe(pipefd) == 0) {
- pid_t pid = fork();
- if (pid == 0) {
- // child process
- // put child in its own group. it will be in this group with all of its descendents,
- // so by killing everything in the group, we kill all the descendents of this process.
- // if we didn't do this, we would just be killing the sh process in process_kill.
- setpgid(0, 0);
- // send stdout and stderr to pipe
- dup2(pipefd[1], STDOUT_FILENO);
- dup2(pipefd[1], STDERR_FILENO);
- close(pipefd[0]);
- close(pipefd[1]);
- char *program = "/bin/sh";
- char *argv[] = {program, "-c", (char *)command, NULL};
- if (execv(program, argv) == -1) {
- dprintf(STDERR_FILENO, "%s: %s\n", program, strerror(errno));
- exit(127);
- }
- } else if (pid > 0) {
- // parent process
- close(pipefd[1]);
- fcntl(pipefd[0], F_SETFL, fcntl(pipefd[0], F_GETFL) | O_NONBLOCK); // set pipe to non-blocking
- proc->pid = pid;
- proc->pipe = pipefd[0];
- success = true;
+ pid_t pid = fork();
+ if (pid == 0) {
+ // child process
+ // put child in its own group. it will be in this group with all of its descendents,
+ // so by killing everything in the group, we kill all the descendents of this process.
+ // if we didn't do this, we would just be killing the sh process in process_kill.
+ setpgid(0, 0);
+ // pipe stuff
+ dup2(stdout_pipe[1], STDOUT_FILENO);
+ if (stderr_pipe[1])
+ dup2(stderr_pipe[1], STDERR_FILENO);
+ else
+ dup2(stdout_pipe[1], STDERR_FILENO);
+ dup2(stdin_pipe[0], STDIN_FILENO);
+ // don't need these file descriptors anymore
+ close(stdin_pipe[0]);
+ close(stdin_pipe[1]);
+ close(stdout_pipe[0]);
+ close(stdout_pipe[1]);
+ if (stderr_pipe[0]) {
+ close(stderr_pipe[0]);
+ close(stderr_pipe[1]);
}
- } else {
- strbuf_printf(proc->error, "%s", strerror(errno));
+
+ char *program = "/bin/sh";
+ char *argv[] = {program, "-c", (char *)command, NULL};
+ if (execv(program, argv) == -1) {
+ dprintf(STDERR_FILENO, "%s: %s\n", program, strerror(errno));
+ exit(127);
+ }
+ } else if (pid > 0) {
+ // parent process
+
+ // we're reading from (the child's) stdout/stderr and writing to stdin,
+ // so we don't need the write end of the stdout pipe or the
+ // read end of the stdin pipe.
+ close(stdout_pipe[1]);
+ if (stderr_pipe[1])
+ close(stderr_pipe[1]);
+ close(stdin_pipe[0]);
+ // set pipes to non-blocking
+ if (!settings->stdout_blocking)
+ set_nonblocking(stdout_pipe[0]);
+ if (stderr_pipe[0] && !settings->stderr_blocking)
+ set_nonblocking(stderr_pipe[0]);
+ if (!settings->stdin_blocking)
+ set_nonblocking(stdin_pipe[1]);
+ proc->pid = pid;
+ proc->stdout_pipe = stdout_pipe[0];
+ if (stderr_pipe[0])
+ proc->stderr_pipe = stderr_pipe[0];
+ proc->stdin_pipe = stdin_pipe[1];
+ success = true;
}
return success;
}
+bool process_run(Process *proc, char const *command) {
+ const ProcessSettings settings = {0};
+ return process_run_ex(proc, command, &settings);
+}
+
+
char const *process_geterr(Process *p) {
return *p->error ? p->error : NULL;
}
-long long process_read(Process *proc, char *data, size_t size) {
- assert(proc->pipe);
- ssize_t bytes_read = read(proc->pipe, data, size);
+long long process_write(Process *proc, const char *data, size_t size) {
+ assert(proc->stdin_pipe); // check that process hasn't been killed
+ ssize_t bytes_written = write(proc->stdin_pipe, data, size);
+ if (bytes_written >= 0) {
+ return (long long)bytes_written;
+ } else if (errno == EAGAIN) {
+ return 0;
+ } else {
+ strbuf_printf(proc->error, "%s", strerror(errno));
+ return -2;
+ }
+}
+
+static long long process_read_fd(Process *proc, int fd, char *data, size_t size) {
+ assert(fd);
+ ssize_t bytes_read = read(fd, data, size);
if (bytes_read >= 0) {
return (long long)bytes_read;
} else if (errno == EAGAIN) {
@@ -63,16 +138,31 @@ long long process_read(Process *proc, char *data, size_t size) {
strbuf_printf(proc->error, "%s", strerror(errno));
return -2;
}
+}
+
+long long process_read(Process *proc, char *data, size_t size) {
+ return process_read_fd(proc, proc->stdout_pipe, data, size);
+}
+
+long long process_read_stderr(Process *proc, char *data, size_t size) {
+ return process_read_fd(proc, proc->stderr_pipe, data, size);
+}
+static void process_close_pipes(Process *proc) {
+ close(proc->stdin_pipe);
+ close(proc->stdout_pipe);
+ close(proc->stderr_pipe);
+ proc->stdin_pipe = 0;
+ proc->stdout_pipe = 0;
+ proc->stderr_pipe = 0;
}
void process_kill(Process *proc) {
kill(-proc->pid, SIGKILL); // kill everything in process group
// get rid of zombie process
waitpid(proc->pid, NULL, 0);
- close(proc->pipe);
proc->pid = 0;
- proc->pipe = 0;
+ process_close_pipes(proc);
}
int process_check_status(Process *proc, char *message, size_t message_size) {
@@ -83,7 +173,7 @@ int process_check_status(Process *proc, char *message, size_t message_size) {
return 0;
} else if (ret > 0) {
if (WIFEXITED(wait_status)) {
- close(proc->pipe); proc->pipe = 0;
+ process_close_pipes(proc);
int code = WEXITSTATUS(wait_status);
if (code == 0) {
str_printf(message, message_size, "exited successfully");
@@ -93,14 +183,14 @@ int process_check_status(Process *proc, char *message, size_t message_size) {
return -1;
}
} else if (WIFSIGNALED(wait_status)) {
- close(proc->pipe);
+ process_close_pipes(proc);
str_printf(message, message_size, "terminated by signal %d", WTERMSIG(wait_status));
return -1;
}
return 0;
} else {
// this process is gone or something?
- close(proc->pipe); proc->pipe = 0;
+ process_close_pipes(proc);
str_printf(message, message_size, "process ended unexpectedly");
return -1;
}
diff --git a/process-win.c b/process-win.c
index a5f05c0..8ea7b91 100644
--- a/process-win.c
+++ b/process-win.c
@@ -1,3 +1,5 @@
+#error "@TODO : implement process_write, separate_stderr"
+
#include "process.h"
struct Process {
diff --git a/process.h b/process.h
index 4db864e..2d5a10c 100644
--- a/process.h
+++ b/process.h
@@ -4,17 +4,39 @@
typedef struct Process Process;
+// zero everything except what you're using
+typedef struct {
+ bool stdin_blocking;
+ bool stdout_blocking;
+ bool separate_stderr;
+ bool stderr_blocking; // not applicable if separate_stderr is false.
+} ProcessSettings;
+
+// get process ID of this process
+int process_get_id(void);
// execute the given command (like if it was passed to system()).
// returns false on failure
+bool process_run_ex(Process *proc, const char *command, const ProcessSettings *props);
+// like process_run_ex, but with the default settings
bool process_run(Process *process, char const *command);
// returns the error last error produced, or NULL if there was no error.
char const *process_geterr(Process *process);
+// write to stdin
+// returns:
+// -2 on error
+// or a non-negative number indicating the number of bytes written.
+long long process_write(Process *process, const char *data, size_t size);
+// read from stdout+stderr
// returns:
// -2 on error
// -1 if no data is available right now
// 0 on end of file
// or a positive number indicating the number of bytes read to data (at most size)
long long process_read(Process *process, char *data, size_t size);
+// like process_read, but reads stderr.
+// this function ALWAYS RETURNS -2 if separate_stderr is not specified in the ProcessSettings.
+// if separate_stderr is false, then both stdout and stderr will be sent via process_read.
+long long process_read_stderr(Process *process, char *data, size_t size);
// Checks if the process has exited. Returns:
// -1 if the process returned a non-zero exit code, or got a signal.
// 1 if the process exited successfully
diff --git a/test.rs b/test.rs
index 6c852d9..40c2438 100644
--- a/test.rs
+++ b/test.rs
@@ -19,6 +19,7 @@ fn main() -> Result<()> {
line.pop();
lines.push(line);
}
+let x = lines.
for line in lines {
println!("{}", line);
}
diff --git a/util.c b/util.c
index cee9cc3..76229af 100644
--- a/util.c
+++ b/util.c
@@ -134,9 +134,14 @@ static void str_cat(char *dst, size_t dst_sz, char const *src) {
}
// safer version of strncpy. dst_sz includes a null terminator.
-static void str_cpy(char *dst, size_t dst_sz, char const *src) {
- size_t srclen = strlen(src);
- size_t n = srclen; // number of bytes to copy
+static void strn_cpy(char *dst, size_t dst_sz, char const *src, size_t src_len) {
+ size_t n = src_len; // number of bytes to copy
+ for (size_t i = 0; i < n; ++i) {
+ if (src[i] == '\0') {
+ n = i;
+ break;
+ }
+ }
if (dst_sz == 0) {
assert(0);
@@ -149,6 +154,11 @@ static void str_cpy(char *dst, size_t dst_sz, char const *src) {
dst[n] = 0;
}
+// safer version of strcpy. dst_sz includes a null terminator.
+static void str_cpy(char *dst, size_t dst_sz, char const *src) {
+ strn_cpy(dst, dst_sz, src, SIZE_MAX);
+}
+
#define strbuf_cpy(dst, src) str_cpy(dst, sizeof dst, src)
#define strbuf_cat(dst, src) str_cat(dst, sizeof dst, src)
@@ -231,18 +241,28 @@ static int str_qsort_case_insensitive_cmp(const void *av, const void *bv) {
return strcmp_case_insensitive(*a, *b);
}
-static void *qsort_ctx_arg;
-static int (*qsort_ctx_cmp)(void *, const void *, const void *);
-static int qsort_with_context_cmp(const void *a, const void *b) {
- return qsort_ctx_cmp(qsort_ctx_arg, a, b);
+// imo windows has the argument order right here
+#if _WIN32
+#define qsort_with_context qsort_s
+#else
+typedef struct {
+ int (*compar)(void *, const void *, const void *);
+ void *context;
+} QSortWithContext;
+static int qsort_with_context_cmp(const void *a, const void *b, void *context) {
+ QSortWithContext *c = context;
+ return c->compar(context, a, b);
}
-
-static void qsort_with_context(void *base, size_t nmemb, size_t size, int (*compar)(void *, const void *, const void *), void *arg) {
- // just use global variables. hopefully we don't try to run this in something multithreaded!
- qsort_ctx_arg = arg;
- qsort_ctx_cmp = compar;
- qsort(base, nmemb, size, qsort_with_context_cmp);
+static void qsort_with_context(void *base, size_t nmemb, size_t size,
+ int (*compar)(void *, const void *, const void *),
+ void *arg) {
+ QSortWithContext ctx = {
+ .compar = compar,
+ .context = arg
+ };
+ qsort_r(base, nmemb, size, qsort_with_context_cmp, &ctx);
}
+#endif
// the actual file name part of the path; get rid of the containing directory.
// NOTE: the returned string is part of path, so you don't need to free it or anything.
@@ -349,3 +369,71 @@ static bool copy_file(char const *src, char const *dst) {
}
return success;
}
+
+typedef struct {
+ // dynamic array, including a null byte.
+ char *str;
+} StrBuilder;
+
+void str_builder_create(StrBuilder *builder) {
+ memset(builder, 0, sizeof *builder);
+ arr_add(builder->str, 0);
+}
+
+StrBuilder str_builder_new(void) {
+ StrBuilder ret = {0};
+ str_builder_create(&ret);
+ return ret;
+}
+
+void str_builder_free(StrBuilder *builder) {
+ arr_free(builder->str);
+}
+
+void str_builder_clear(StrBuilder *builder) {
+ str_builder_free(builder);
+ str_builder_create(builder);
+}
+
+void str_builder_append(StrBuilder *builder, const char *s) {
+ assert(builder->str);
+
+ size_t s_len = strlen(s);
+ size_t prev_size = arr_len(builder->str);
+ size_t prev_len = prev_size - 1; // null terminator
+ // note: this zeroes the newly created elements, so we have a new null terminator
+ arr_set_len(builder->str, prev_size + s_len);
+ // -1 for null terminator
+ memcpy(builder->str + prev_len, s, s_len);
+}
+
+void str_builder_appendf(StrBuilder *builder, PRINTF_FORMAT_STRING const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3);
+void str_builder_appendf(StrBuilder *builder, const char *fmt, ...) {
+ // idk if you can always just pass NULL to vsnprintf
+ va_list args;
+ char fakebuf[2] = {0};
+ va_start(args, fmt);
+ int ret = vsnprintf(fakebuf, 1, fmt, args);
+ va_end(args);
+
+ if (ret < 0) return; // bad format or something
+ u32 n = (u32)ret;
+
+ size_t prev_size = arr_len(builder->str);
+ size_t prev_len = prev_size - 1; // null terminator
+ arr_set_len(builder->str, prev_size + n);
+ va_start(args, fmt);
+ vsnprintf(builder->str + prev_len, n + 1, fmt, args);
+ va_end(args);
+}
+
+// append n null bytes.
+void str_builder_append_null(StrBuilder *builder, size_t n) {
+ arr_set_len(builder->str, arr_len(builder->str) + n);
+}
+
+u32 str_builder_len(StrBuilder *builder) {
+ assert(builder->str);
+ return arr_len(builder->str) - 1;
+}
+