diff options
-rw-r--r-- | base.h | 5 | ||||
-rw-r--r-- | json.c | 101 | ||||
-rw-r--r-- | lsp.c | 362 | ||||
-rw-r--r-- | main.c | 37 | ||||
-rw-r--r-- | test.rs | 1 | ||||
-rw-r--r-- | util.c | 81 |
6 files changed, 457 insertions, 130 deletions
@@ -116,6 +116,11 @@ typedef unsigned long long ullong; #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 @@ -25,7 +25,7 @@ typedef struct { u32 elements; } JSONArray; -enum { +typedef enum { // note: json doesn't actually include undefined. // this is only for returning things from json_get etc. JSON_UNDEFINED, @@ -36,10 +36,10 @@ enum { JSON_STRING, JSON_OBJECT, JSON_ARRAY -}; +} JSONValueType; struct JSONValue { - u8 type; + JSONValueType type; union { double number; JSONString string; @@ -59,6 +59,26 @@ typedef struct { #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); @@ -446,10 +466,16 @@ JSONValue json_get(const JSON *json, const char *path) { 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. -static void json_string_get(const JSON *json, const JSONString *string, char *buf, size_t buf_sz) { +void json_string_get(const JSON *json, const JSONString *string, char *buf, size_t buf_sz) { const char *text = json->text; if (buf_sz == 0) { assert(0); @@ -496,6 +522,15 @@ static void json_string_get(const JSON *json, const JSONString *string, char *bu *buf = '\0'; } +// returns a malloc'd null-terminated string. +static char *json_string_get_alloc(const JSON *json, const 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) { @@ -555,23 +590,67 @@ static void json_test_time_small(void) { } #endif -static void json_debug_print(const JSON *json) { +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" -// the return value is the # of bytes in the escaped string. -static size_t json_escape_to(char *out, size_t out_sz, const char *in) { - (void)out;(void)out_sz;(void)in; - // @TODO - abort(); +// 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. -static char *json_escape(const char *str) { +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); @@ -1,36 +1,86 @@ +// @TODO: documentation +// @TODO : make sure offsets are utf-16! // @TODO: 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 + LSP_EXIT, + + // server-to-client + LSP_SHOW_MESSAGE, + LSP_LOG_MESSAGE } LSPRequestType; typedef struct { // buffer language Language language; - // these will be free'd. + // 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; typedef struct { + LSPMessageType type; + union { + LSPRequest request; + JSON response; + } u; +} LSPMessage; + +typedef struct { Process process; u64 request_id; - JSON *responses; - SDL_mutex *responses_mutex; - LSPRequest *requests; + LSPMessage *messages; + SDL_mutex *messages_mutex; + LSPRequest *requests_client2server; + LSPRequest *requests_server2client; SDL_mutex *requests_mutex; bool initialized; // has the response to the initialize request been sent? SDL_Thread *communication_thread; @@ -62,17 +112,6 @@ bool lsp_get_error(LSP *lsp, char *error, size_t error_size, bool clear) { SDL_UnlockMutex(lsp->error_mutex);\ } while (0) -static void write_request_content(LSP *lsp, const char *content) { - char header[128]; - size_t content_size = strlen(content); - strbuf_printf(header, "Content-Length: %zu\r\n\r\n", content_size); - #if 0 - printf("\x1b[1m%s%s\x1b[0m\n", header, content); - #endif - process_write(&lsp->process, header, strlen(header)); - process_write(&lsp->process, content, content_size); -} - static const char *lsp_language_id(Language lang) { switch (lang) { case LANG_CONFIG: @@ -112,67 +151,98 @@ static const char *lsp_language_id(Language lang) { static u64 write_request(LSP *lsp, const LSPRequest *request) { unsigned long long id = lsp->request_id++; + 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\","); + 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: { - char content[1024]; - strbuf_printf(content, - "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"initialize\",\"params\":{" + str_builder_appendf(&builder, + "\"id\":%llu,\"method\":\"initialize\",\"params\":{" "\"processId\":%d," "\"capabilities\":{}" - "}}", id, process_get_id()); - write_request_content(lsp, content); - } break; - case LSP_INITIALIZED: { - write_request_content(lsp, "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\"}"); + "}", id, 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 *did_open = a_sprintf( - "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"textDocument/open\",\"params\":{" + char *escaped_text = json_escape(open->file_contents); + + str_builder_appendf(&builder, + "\"id\":%llu,\"method\":\"textDocument/open\",\"params\":{" "textDocument:{" "uri:\"file://%s\"," "languageId:\"%s\"," "version:1," - "text:\"", - id, escaped_filename, lsp_language_id(open->language)); + "text:\"%s\"}}", + id, + escaped_filename, + lsp_language_id(open->language), + escaped_text); + free(escaped_text); free(escaped_filename); - - size_t did_open_sz = strlen(did_open) + 2 * strlen(open->file_contents) + 16; - did_open = realloc(did_open, did_open_sz); - - size_t n = json_escape_to(did_open + strlen(did_open), - did_open_sz - 10 - strlen(did_open), - open->file_contents); - char *p = did_open + n; - sprintf(p, "\"}}}"); - - free(did_open); - - write_request_content(lsp, did_open); } break; case LSP_COMPLETION: { - char content[1024]; - // no params needed - strbuf_printf(content, - "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"textDocument/completion\",\"params\":{" - "}}", id); - write_request_content(lsp, content); - } break; - case LSP_SHUTDOWN: { - char content[1024]; - strbuf_printf(content, - "{\"jsonrpc\":\"2.0\",\"id\":%llu,\"method\":\"shutdown\"}", id); - write_request_content(lsp, content); - } break; - case LSP_EXIT: { - write_request_content(lsp, "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}"); + const LSPRequestCompletion *completion = &request->data.completion; + char *escaped_path = json_escape(completion->position.path); + str_builder_appendf(&builder,"\"id\":%llu,\"method\":\"textDocument/completion\",\"params\":{" + "textDocument:\"%s\"," + "position:{" + "line:%lu," + "character:%lu" + "}" + "}", + id, + escaped_path, + (ulong)completion->position.line, + (ulong)completion->position.character); + free(escaped_path); } break; + case LSP_SHUTDOWN: + str_builder_appendf(&builder, "\"id\":%llu,\"method\":\"shutdown\"", id); + break; + case LSP_EXIT: + str_builder_appendf(&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); + return (u64)id; } @@ -195,55 +265,121 @@ static bool has_response(const char *data, size_t data_len, u64 *p_offset, u64 * void lsp_send_request(LSP *lsp, const LSPRequest *request) { SDL_LockMutex(lsp->requests_mutex); - arr_add(lsp->requests, *request); + arr_add(lsp->requests_client2server, *request); SDL_UnlockMutex(lsp->requests_mutex); } -static void process_response(LSP *lsp, const JSON *json) { +static bool parse_server2client_request(LSP *lsp, JSON *json, LSPRequest *request) { + JSONValue method = json_get(json, "method"); + if (method.type != JSON_STRING) { + lsp_set_error(lsp, "Bad type for request method: %s", json_type_to_str(method.type)); + } + + char str[64] = {0}; + json_string_get(json, &method.val.string, str, sizeof str); + + if (streq(str, "window/showMessage")) { + request->type = LSP_SHOW_MESSAGE; + goto window_message; + } else if (streq(str, "window/logMessage")) { + request->type = LSP_LOG_MESSAGE; + window_message:; + JSONValue type = json_get(json, "params.type"); + JSONValue message = json_get(json, "params.message"); + if (type.type != JSON_NUMBER) { + lsp_set_error(lsp, "Expected MessageType, got %s", json_type_to_str(type.type)); + return false; + } + if (message.type != JSON_STRING) { + lsp_set_error(lsp, "Expected message string, got %s", json_type_to_str(message.type)); + 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(str, "$/")) { + // we can safely ignore this + } else { + lsp_set_error(lsp, "Unrecognized request method: %s", str); + } + return false; +} + +static void process_message(LSP *lsp, JSON *json) { #if 1 printf("\x1b[3m"); json_debug_print(json); printf("\x1b[0m\n"); #endif + bool keep_json = false; + 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"); - JSONValue id = json_get(json, "id"); + if (result.type != JSON_UNDEFINED) { + JSONValue id = json_get(json, "id"); - if (result.type == JSON_UNDEFINED || id.type != JSON_NUMBER) { - // uh oh - 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); + 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}, + }; + u64 initialized_id = write_request(lsp, &initialized); + // this should be the second request. + (void)initialized_id; + assert(initialized_id == 1); + // we can now send requests which have nothing to do with initialization + lsp->initialized = true; } else { - lsp_set_error(lsp, "Server error (no message)"); + SDL_LockMutex(lsp->messages_mutex); + LSPMessage *message = arr_addp(lsp->messages); + message->type = LSP_RESPONSE; + message->u.response = *json; + SDL_UnlockMutex(lsp->messages_mutex); + keep_json = true; + } + } 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 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}, - }; - u64 initialized_id = write_request(lsp, &initialized); - // this should be the second request. - (void)initialized_id; - assert(initialized_id == 1); - // we can now send requests which have nothing to do with initialization - lsp->initialized = true; } else { - SDL_LockMutex(lsp->responses_mutex); - arr_add(lsp->responses, *json); - SDL_UnlockMutex(lsp->responses_mutex); + lsp_set_error(lsp, "Bad message from server (no result, no method)."); } + ret: + if (!keep_json) + json_free(json); } -// receive responses from LSP, up to max_size bytes. +// receive responses/requests/notifications from LSP, up to max_size bytes. static void lsp_receive(LSP *lsp, size_t max_size) { { @@ -283,9 +419,8 @@ static void lsp_receive(LSP *lsp, size_t max_size) { if (json_parse(&json, copy)) { assert(json.text == copy); json.is_text_copied = true; - process_response(lsp, &json); + process_message(lsp, &json); } else { - lsp_set_error(lsp, "couldn't parse response JSON: %s", json.error); json_free(&json); } @@ -298,22 +433,44 @@ static void lsp_receive(LSP *lsp, size_t max_size) { } } -static void free_request(LSPRequest *r) { +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_COMPLETION: 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; + } +} + +void lsp_message_free(LSPMessage *message) { + switch (message->type) { + case LSP_REQUEST: + lsp_request_free(&message->u.request); + break; + case LSP_RESPONSE: + json_free(&message->u.response); + break; } } @@ -326,10 +483,10 @@ static bool lsp_send(LSP *lsp) { LSPRequest *requests = NULL; SDL_LockMutex(lsp->requests_mutex); - size_t n_requests = arr_len(lsp->requests); + size_t n_requests = arr_len(lsp->requests_client2server); requests = calloc(n_requests, sizeof *requests); - memcpy(requests, lsp->requests, n_requests * sizeof *requests); - arr_clear(lsp->requests); + memcpy(requests, lsp->requests_client2server, n_requests * sizeof *requests); + arr_clear(lsp->requests_client2server); SDL_UnlockMutex(lsp->requests_mutex); bool quit = false; @@ -340,7 +497,7 @@ static bool lsp_send(LSP *lsp) { // whatever. write_request(lsp, r); } - free_request(r); + lsp_request_free(r); if (SDL_SemTryWait(lsp->quit_sem) == 0) { quit = true; @@ -416,27 +573,32 @@ bool lsp_create(LSP *lsp, const char *analyzer_command) { write_request(lsp, &initialize); lsp->quit_sem = SDL_CreateSemaphore(0); - lsp->responses_mutex = SDL_CreateMutex(); + lsp->messages_mutex = SDL_CreateMutex(); lsp->communication_thread = SDL_CreateThread(lsp_communication_thread, "LSP communicate", lsp); return true; } -bool lsp_next_response(LSP *lsp, JSON *json) { +bool lsp_next_message(LSP *lsp, LSPMessage *message) { bool any = false; - SDL_LockMutex(lsp->responses_mutex); - if (arr_len(lsp->responses)) { - *json = lsp->responses[0]; - arr_remove(lsp->responses, 0); + SDL_LockMutex(lsp->messages_mutex); + if (arr_len(lsp->messages)) { + *message = lsp->messages[0]; + arr_remove(lsp->messages, 0); any = true; } - SDL_UnlockMutex(lsp->responses_mutex); + 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); } @@ -45,7 +45,9 @@ no_warn_end #endif #include "unicode.h" +#include "arr.c" #include "util.c" + #if _WIN32 #include "filesystem-win.c" #elif __unix__ @@ -53,7 +55,6 @@ no_warn_end #else #error "Unrecognized operating system." #endif -#include "arr.c" #include "math.c" #if _WIN32 @@ -294,13 +295,37 @@ int main(int argc, char **argv) { } usleep(500000);//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/ted/test.rs"), + .line = 21, + .character = 14, + } + }; lsp_send_request(&lsp, &test_req); while (1) { - JSON response = {0}; - if (lsp_next_response(&lsp, &response)) { - json_debug_print(&response); - printf("\n"); - break; + LSPMessage message = {0}; + if (lsp_next_message(&lsp, &message)) { + if (message.type == LSP_RESPONSE) { + json_debug_print(&message.u.response); + printf("\n"); + } 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)) { @@ -19,6 +19,7 @@ fn main() -> Result<()> { line.pop(); lines.push(line); } +let x = lines. for line in lines { println!("{}", line); } @@ -97,19 +97,6 @@ static char *str_dup(char const *src) { #define strbuf_catf(str, ...) assert(sizeof str != 4 && sizeof str != 8), \ str_catf(str, sizeof str, __VA_ARGS__) -#if __unix__ -static char *a_sprintf(const char *fmt, ...) ATTRIBUTE_PRINTF(1, 2); -static char *a_sprintf(const char *fmt, ...) { - va_list args; - va_start(args, fmt); - char *str = NULL; - vasprintf(&str, fmt, args); - return str; -} -#else -#error "@TODO" -#endif - // on 16-bit systems, this is 16383. on 32/64-bit systems, this is 1073741823 // it is unusual to have a string that long. #define STRLEN_SAFE_MAX (UINT_MAX >> 2) @@ -372,3 +359,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; +} + |