diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | arr.c | 30 | ||||
-rw-r--r-- | base.h | 19 | ||||
-rw-r--r-- | buffer.c | 144 | ||||
-rw-r--r-- | json.c | 667 | ||||
-rw-r--r-- | lsp.c | 749 | ||||
-rw-r--r-- | lsp.h | 180 | ||||
-rw-r--r-- | main.c | 84 | ||||
-rw-r--r-- | process-posix.c | 168 | ||||
-rw-r--r-- | process-win.c | 2 | ||||
-rw-r--r-- | process.h | 22 | ||||
-rw-r--r-- | ted.c | 5 | ||||
-rw-r--r-- | ted.h | 3 | ||||
-rw-r--r-- | test.rs | 1 | ||||
-rw-r--r-- | util.c | 114 |
15 files changed, 2047 insertions, 145 deletions
@@ -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 @@ -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; @@ -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\"") \ @@ -2040,6 +2040,9 @@ void buffer_paste(TextBuffer *buffer) { } } +LSP *buffer_lsp(TextBuffer *buffer) { + return ted_get_lsp(buffer->ted, buffer_language(buffer)); +} // if an error occurs, buffer is left untouched (except for the error field) and the function returns false. Status buffer_load_file(TextBuffer *buffer, char const *filename) { @@ -2060,87 +2063,92 @@ Status buffer_load_file(TextBuffer *buffer, char const *filename) { success = false; } else { u8 *file_contents = buffer_calloc(buffer, 1, file_size); - if (file_contents) { - lines_capacity = 4; - lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines - if (lines) { - nlines = 1; - size_t bytes_read = fread(file_contents, 1, file_size, fp); - if (bytes_read == file_size) { - char32_t c = 0; - for (u8 *p = file_contents, *end = p + file_size; p != end; ) { - if (*p == '\r' && p != end-1 && p[1] == '\n') { - // CRLF line endings - p += 2; - c = '\n'; - } else { - size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); - if (n == 0) { - // null character - c = 0; - ++p; - } else if (n >= (size_t)(-2)) { - // invalid UTF-8 - success = false; - buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); - break; - } else { - p += n; - } - } - if (c == '\n') { - if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) - ++nlines; - } else { - u32 line_idx = nlines - 1; - Line *line = &lines[line_idx]; - buffer_line_append_char(buffer, line, c); - } - } + lines_capacity = 4; + lines = buffer_calloc(buffer, lines_capacity, sizeof *buffer->lines); // initial lines + nlines = 1; + size_t bytes_read = fread(file_contents, 1, file_size, fp); + if (bytes_read == file_size) { + char32_t c = 0; + for (u8 *p = file_contents, *end = p + file_size; p != end; ) { + if (*p == '\r' && p != end-1 && p[1] == '\n') { + // CRLF line endings + p += 2; + c = '\n'; } else { - success = false; + size_t n = unicode_utf8_to_utf32(&c, (char *)p, (size_t)(end - p)); + if (n == 0) { + // null character + c = 0; + ++p; + } else if (n >= (size_t)(-2)) { + // invalid UTF-8 + success = false; + buffer_seterr(buffer, "Invalid UTF-8 (position: %td).", p - file_contents); + break; + } else { + p += n; + } } - if (!success) { - // something went wrong; we need to free all the memory we used - for (u32 i = 0; i < nlines; ++i) - buffer_line_free(&lines[i]); - free(lines); + if (c == '\n') { + if (buffer_lines_set_min_capacity(buffer, &lines, &lines_capacity, nlines + 1)) + ++nlines; + } else { + u32 line_idx = nlines - 1; + Line *line = &lines[line_idx]; + buffer_line_append_char(buffer, line, c); } } - free(file_contents); } - if (ferror(fp)) { + if (ferror(fp)) { buffer_seterr(buffer, "Error reading from file."); success = false; } + if (!success) { + // something went wrong; we need to free all the memory we used + for (u32 i = 0; i < nlines; ++i) + buffer_line_free(&lines[i]); + free(lines); + } + + if (success) { + char *filename_copy = buffer_strdup(buffer, filename); + if (!filename_copy) success = false; + if (success) { + // everything is good + buffer_clear(buffer); + buffer->lines = lines; + buffer->nlines = nlines; + buffer->frame_earliest_line_modified = 0; + buffer->frame_latest_line_modified = nlines - 1; + buffer->lines_capacity = lines_capacity; + buffer->filename = filename_copy; + buffer->last_write_time = time_last_modified(buffer->filename); + if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { + // can't write to this file; make the buffer view only. + buffer->view_only = true; + } + } + + LSP *lsp = buffer_lsp(buffer); + if (lsp) { + // send didOpen + LSPRequest request = {.type = LSP_OPEN}; + LSPRequestOpen *open = &request.data.open; + open->file_contents = (char *)file_contents; + open->path = str_dup(filename); + open->language = buffer_language(buffer); + lsp_send_request(lsp, &request); + file_contents = NULL; // don't free + } + } + + free(file_contents); } - if (fclose(fp) != 0) { - buffer_seterr(buffer, "Error closing file."); - success = false; - } + fclose(fp); } else { buffer_seterr(buffer, "Couldn't open file %s: %s.", filename, strerror(errno)); success = false; } - if (success) { - char *filename_copy = buffer_strdup(buffer, filename); - if (!filename_copy) success = false; - if (success) { - // everything is good - buffer_clear(buffer); - buffer->lines = lines; - buffer->nlines = nlines; - buffer->frame_earliest_line_modified = 0; - buffer->frame_latest_line_modified = nlines - 1; - buffer->lines_capacity = lines_capacity; - buffer->filename = filename_copy; - buffer->last_write_time = time_last_modified(buffer->filename); - if (!(fs_path_permission(filename) & FS_PERMISSION_WRITE)) { - // can't write to this file; make the buffer view only. - buffer->view_only = true; - } - } - } return success; } @@ -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, 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: { + JSONString string = value.val.string; + printf("\"%.*s\"", + (int)string.len, + json->text + string.pos); + } break; + case JSON_ARRAY: { + 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: { + 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, 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, 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 @@ -0,0 +1,749 @@ +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: + 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->path); + 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. +// 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_INITIALIZED + || request->type == LSP_EXIT; + 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_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\":{}," + "\"rootUri\":null," + "\"workspaceFolders\":null" + "}", process_get_id()); + } break; + case LSP_INITIALIZED: + str_builder_append(&builder, "\"method\":\"initialized\""); + break; + case LSP_OPEN: { + const LSPRequestOpen *open = &request->data.open; + printf("text=%p\n",open->file_contents); + char *escaped_path = json_escape(open->path); + char *escaped_text = json_escape(open->file_contents); + // @TODO: escape directly into builder + str_builder_appendf(&builder, + "\"method\":\"textDocument/didOpen\",\"params\":{" + "\"textDocument\":{" + "\"uri\":\"file://%s\"," + "\"languageId\":\"%s\"," + "\"version\":1," + "\"text\":\"%s\"}}", + escaped_path, + lsp_language_id(open->language), + escaped_text); + free(escaped_text); + free(escaped_path); + } 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); + + 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"); + 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, LSPString string) { + assert(string.offset < arr_len(response->string_data)); + return &response->string_data[string.offset]; +} + +static LSPString 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 (LSPString){ + .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. + // this is what VSCode seems to do. + // i hate microsofot. + 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_position(LSP *lsp, const JSON *json, JSONValue pos_value, LSPPosition *pos) { + if (!lsp_expect_object(lsp, pos_value, "document position")) + return false; + JSONObject pos_object = pos_value.val.object; + JSONValue line = json_object_get(json, pos_object, "line"); + JSONValue character = json_object_get(json, pos_object, "character"); + if (!lsp_expect_number(lsp, line, "document line number") + || !lsp_expect_number(lsp, character, "document column number")) + return false; + pos->line = (u32)line.val.number; + pos->character = (u32)line.val.number; + return true; +} + +static bool parse_range(LSP *lsp, const JSON *json, JSONValue range_value, LSPRange *range) { + if (!lsp_expect_object(lsp, range_value, "document range")) + return false; + JSONObject range_object = range_value.val.object; + JSONValue start = json_object_get(json, range_object, "start"); + JSONValue end = json_object_get(json, range_object, "end"); + return parse_position(lsp, json, start, &range->start) + && parse_position(lsp, json, end, &range->end); +} + +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; + item->label = lsp_response_add_json_string(response, json, label); + + // defaults + item->sort_text = item->label; + item->text_edit = (LSPTextEdit) { + .type = LSP_TEXT_EDIT_PLAIN, + .at_cursor = true, + .range = {0}, + .new_text = item->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. + item->sort_text = lsp_response_add_json_string(response, + json, sort_text_value.val.string); + } + + JSONValue text_type_value = json_object_get(json, item_object, "insertTextFormat"); + if (text_type_value.type == JSON_NUMBER) { + double type = text_type_value.val.number; + if (type != LSP_TEXT_EDIT_PLAIN && type != LSP_TEXT_EDIT_SNIPPET) { + lsp_set_error(lsp, "Bad InsertTextFormat: %g", type); + return false; + } + item->text_edit.type = (LSPTextEditType)type; + } + + // @TODO: detail + + // @TODO(eventually): additionalTextEdits + // (try to find a case where this comes up) + + // what should happen when this completion is selected? + JSONValue text_edit_value = json_object_get(json, item_object, "textEdit"); + if (text_edit_value.type == JSON_OBJECT) { + JSONObject text_edit = text_edit_value.val.object; + item->text_edit.at_cursor = false; + + JSONValue range = json_object_get(json, text_edit, "range"); + if (!parse_range(lsp, json, range, &item->text_edit.range)) + return false; + + JSONValue new_text_value = json_object_get(json, text_edit, "newText"); + if (!lsp_expect_string(lsp, new_text_value, "completion newText")) + return false; + item->text_edit.new_text = lsp_response_add_json_string(response, + json, new_text_value.val.string); + } else { + // not using textEdit. check insertText. + JSONValue insert_text_value = json_object_get(json, item_object, "insertText"); + if (insert_text_value.type == JSON_STRING) { + // string which will be inserted if this completion is selected + item->text_edit.new_text = lsp_response_add_json_string(response, + json, insert_text_value.val.string); + } + } + + + } + + 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 id_value = json_get(json, "id"); + + // get the request associated with this (if any) + LSPRequest response_to = {0}; + if (id_value.type == JSON_NUMBER) { + u64 id = (u64)id_value.val.number; + arr_foreach_ptr(lsp->requests_sent, LSPRequest, req) { + if (req->id == id) { + response_to = *req; + arr_remove(lsp->requests_sent, (u32)(req - lsp->requests_sent)); + break; + } + } + } + + 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);; + + if (streq(err, "waiting for cargo metadata or cargo check")) { + // fine. be that way. i'll resend the goddamn request. + // i'll keep bombarding you with requests. + // maybe next time you should abide by the standard and only send an initialize response when youre actually ready to handle my requests. fuck you. + if (response_to.type) + lsp_send_request(lsp, &response_to); + } else { + lsp_set_error(lsp, "%s", err); + } + goto ret; + } + + JSONValue result = json_get(json, "result"); + if (result.type != JSON_UNDEFINED) { + if (response_to.type == LSP_INITIALIZE) { + // 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 { + LSPResponse response = {0}; + bool success = false; + switch (response_to.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: + lsp_request_free(&response_to); + 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); + } + + 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_DestroyMutex(lsp->requests_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); +} @@ -0,0 +1,180 @@ +// @TODO: +// - document this and lsp.c. +// - maximum queue size for requests/responses just in case? +// - delete old sent requests +// (if the server never sends a response) +// - TESTING: make rust-analyzer-slow (waits 10s before sending response) + +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 *path; + // 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; + // the **UTF-16** "character" offset within the line + u32 character; +} LSPDocumentPosition; + +typedef struct { + LSPDocumentPosition position; +} LSPRequestCompletion; + +typedef struct { + // id is set by lsp.c; you shouldn't set it. + u32 id; + LSPRequestType type; + union { + LSPRequestOpen open; + LSPRequestCompletion completion; + // for LSP_SHOW_MESSAGE and LSP_LOG_MESSAGE + LSPRequestMessage message; + } data; +} LSPRequest; + +typedef struct { + u32 offset; +} LSPString; + +typedef struct { + u32 line; + u32 character; +} LSPPosition; + +typedef struct { + LSPPosition start; + LSPPosition end; +} LSPRange; + +// see InsertTextFormat in the LSP spec. +typedef enum { + // plain text + LSP_TEXT_EDIT_PLAIN = 1, + // snippet e.g. "some_method($1, $2)$0" + LSP_TEXT_EDIT_SNIPPET = 2 +} LSPTextEditType; + +typedef struct { + LSPTextEditType type; + + // if set to true, `range` should be ignored + // -- this is a completion which uses insertText. + // how to handle this: + // "VS Code when code complete is requested in this example + // `con<cursor position>` and a completion item with an `insertText` of + // `console` is provided it will only insert `sole`" + bool at_cursor; + + LSPRange range; + LSPString new_text; +} LSPTextEdit; + +typedef struct { + LSPString label; + LSPTextEdit text_edit; + // note: the items are sorted here in this file, + // so you probably don't need to access this. + LSPString 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 LSP { + Process process; + u32 request_id; + LSPMessage *messages; + SDL_mutex *messages_mutex; + LSPRequest *requests_client2server; + LSPRequest *requests_server2client; + // we keep track of client-to-server requests + // so that we can process responses. + // also fucking rust-analyzer gives "waiting for cargo metadata or cargo check" + // WHY NOT JUST WAIT UNTIL YOUVE DONE THAT BEFORE SENDING THE INITIALIZE RESPONSE. YOU HAVE NOT FINISHED INITIALIZATION. YOU ARE LYING. + // YOU GIVE A -32801 ERROR CODE WHICH IS "ContentModified" -- WHAT THE FUCK? THATS JUST COMPLETLY WRONG + // so we need to re-send requests in that case. + LSPRequest *requests_sent; + 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; + +// @TODO: function declarations + +// 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 can 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); +void lsp_message_free(LSPMessage *message); +void lsp_send_request(LSP *lsp, const LSPRequest *request); +const char *lsp_response_string(const LSPResponse *response, LSPString string); +bool lsp_create(LSP *lsp, const char *analyzer_command); +bool lsp_next_message(LSP *lsp, LSPMessage *message); +void lsp_free(LSP *lsp); @@ -1,4 +1,8 @@ /* +@TODO: +- rust-analyzer bug reports: + - bad json can give "Unexpected error: client exited without proper shutdown sequence" + - rust-analyzer should wait until cargo metadata/check is done before sending initialize response FUTURE FEATURES: - configurable max buffer size + max view-only buffer size - better undo chaining (dechain on backspace?) @@ -48,7 +52,9 @@ no_warn_end #endif #include "unicode.h" +#include "arr.c" #include "util.c" + #if _WIN32 #include "filesystem-win.c" #elif __unix__ @@ -56,7 +62,6 @@ no_warn_end #else #error "Unrecognized operating system." #endif -#include "arr.c" #include "math.c" #if _WIN32 @@ -92,6 +97,7 @@ static void die(char const *fmt, ...) { #include "ted.h" #include "gl.c" #include "text.c" +#include "lsp.h" #include "string32.c" #include "syntax.c" @@ -108,6 +114,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(); @@ -286,6 +294,72 @@ int main(int argc, char **argv) { PROFILE_TIME(init_start) PROFILE_TIME(basic_init_start) + + if (0) { + // @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("(%d)%s => ", + item->text_edit.type, + lsp_response_string(response, item->label)); + printf("%s\n", + lsp_response_string(response, item->text_edit.new_text)); + } + } 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}; @@ -341,6 +415,14 @@ int main(int argc, char **argv) { } ted->last_save_time = -1e50; + + // @TODO TEMPORARY + ted->test_lsp = calloc(1, sizeof(LSP)); + if (!lsp_create(ted->test_lsp, "rust-analyzer")) { + printf("lsp_create: %s\n",ted->test_lsp->error); + exit(1); + } + // make sure signal handler has access to ted. error_signal_handler_ted = ted; 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 { @@ -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 @@ -66,6 +66,11 @@ Settings *ted_active_settings(Ted *ted) { return settings; } +LSP *ted_get_lsp(Ted *ted, Language lang) { + // @TODO + return ted->test_lsp; +} + u32 ted_color(Ted *ted, ColorSetting color) { return ted_active_settings(ted)->colors[color]; } @@ -353,6 +353,8 @@ typedef struct { } BuildError; typedef struct Ted { + struct LSP *test_lsp; // @TODO: something better + SDL_Window *window; Font *font_bold; Font *font; @@ -471,6 +473,7 @@ void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer); // the settings of the active buffer, or the default settings if there is no active buffer Settings *ted_active_settings(Ted *ted); void ted_load_configs(Ted *ted, bool reloading); +struct LSP *ted_get_lsp(Ted *ted, Language lang); static TextBuffer *find_search_buffer(Ted *ted); // first, we read all config files, then we parse them. // this is because we want less specific settings (e.g. settings applied @@ -19,6 +19,7 @@ fn main() -> Result<()> { line.pop(); lines.push(line); } +let x = lines. for line in lines { println!("{}", line); } @@ -142,9 +142,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); @@ -157,6 +162,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) @@ -239,18 +249,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(c->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. @@ -357,3 +377,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; +} + |