// // Created by Giorgio on 25/05/2026. // #include "../include/completion.h" #include #include #include #include #include "include/append_buffer.h" #include "include/cJSON.h" #include "include/data.h" #include "include/lsp_ui.h" #include "include/split_screen.h" #include "include/terminal.h" #include "include/utils.h" static void lsp_send(int fd, const char* json) { int body_len = strlen(json); char header[1024]; int header_len = snprintf(header, sizeof(header), "Content-Length: %d\r\n\r\n", body_len); // Write header + body atomically in two writes, no dprintf mixing write(fd, header, header_len); write(fd, json, body_len); // Log to stderr for debugging appDebug("[LSP →] Content-Length: %d | %s\n", body_len, json); fflush(stderr); } static int lsp_uri_to_buffer_id(const char* uri) { const char *path = uri; if (strncmp(uri, "file://", 7) == 0) path = uri + 7; // path is now "/absolute/path" — realpath output matches this directly for (int i = 0; i < E.number_of_buffer; i++) { if (E.buffers[i].filename == NULL) continue; appDebug("[URI MATCH] comparing '%s' vs '%s'\n", E.buffers[i].fullname, path); if (strcmp(E.buffers[i].fullname, path) == 0) return E.buffers[i].buffer_id; } return -1; } static char* lsp_recv(int fd) { char header[1024]; int content_length = 0; while (1) { int i = 0; char c; int n; // Read until \n while ((n = read(fd, &c, 1)) == 1 && c != '\n') header[i++] = c; if (n <= 0) return NULL; header[i] = '\0'; // Strip \r explicitly while (i > 0 && (header[i - 1] == '\r' || header[i - 1] == ' ')) header[--i] = '\0'; if (i == 0) break; // blank line = end of headers if (strncmp(header, "Content-Length: ", 16) == 0) content_length = atoi(header + 16); } if (content_length == 0) return NULL; char* body = bAlloc(content_length + 1); int total = 0; while (total < content_length) { int n = read(fd, body + total, content_length - total); if (n <= 0) { bFree(body); return NULL; } total += n; } body[content_length] = '\0'; return body; } static void lsp_dispatch(LspClient* lsp, const char* json) { if (!json) return; cJSON* root = cJSON_Parse(json); if (!root) { appDebug("[LSP ←] Failed to parse JSON: %.120s\n", json); return; } cJSON* method = cJSON_GetObjectItem(root, "method"); cJSON* id = cJSON_GetObjectItem(root, "id"); cJSON* result = cJSON_GetObjectItem(root, "result"); cJSON* error = cJSON_GetObjectItem(root, "error"); // ── Error response ──────────────────────────────────────────────────────── if (error) { cJSON* msg = cJSON_GetObjectItem(error, "message"); appDebug("[LSP ←] ERROR: %s\n", msg ? msg->valuestring : "(no message)"); cJSON_Delete(root); return; } // ── Notification (no id, has method) ───────────────────────────────────── if (method && !id) { const char* m = method->valuestring; appDebug("[LSP ←] NOTIF: %s\n", m); if (strcmp(m, "textDocument/publishDiagnostics") == 0) { // Find which buffer this diagnostic belongs to cJSON* params = cJSON_GetObjectItem(root, "params"); cJSON* uri = cJSON_GetObjectItem(params, "uri"); int buf_id = splitScreenGetActivePane()->buffer_id; appDebug("[LSP ←] Diagnostics for buffer %d\n", buf_id); pthread_mutex_lock(&lsp->lock); lspParseDiagnostics(json, &E.lsp_diagnostics, buf_id); pthread_mutex_unlock(&lsp->lock); write(lsp->wake_pipe[1], "d", 1); } else if (strcmp(m, "window/logMessage") == 0) { cJSON* params = cJSON_GetObjectItem(root, "params"); cJSON* message = cJSON_GetObjectItem(params, "message"); appDebug("[LSP ←] LOG: %s\n", message ? message->valuestring : ""); } E.lsp_client->completion_just_arrived = 1; cJSON_Delete(root); return; } // ── Response (has id + result) ──────────────────────────────────────────── if (id && result) { int response_id = id->valueint; appDebug("[LSP ←] RESPONSE id=%d\n", response_id); // initialize response → send initialized + mark ready if (lsp->state == LSP_INITIALIZING) { appDebug("[LSP ←] Initialize OK, sending initialized\n"); lsp_send(lsp->write_fd, "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}"); pthread_mutex_lock(&lsp->lock); lsp->state = LSP_READY; pthread_cond_signal(&lsp->ready_cond); pthread_mutex_unlock(&lsp->lock); E.lsp_client->completion_just_arrived = 1; cJSON_Delete(root); return; } // completion response → parse items and show popup cJSON* items = cJSON_GetObjectItem(result, "items"); if (items && cJSON_IsArray(items)) { int count = cJSON_GetArraySize(items); appDebug("[LSP ←] Completion: %d items\n", count); // Print each item to stderr for debugging cJSON* item; int i = 0; cJSON_ArrayForEach(item, items) { cJSON* label = cJSON_GetObjectItem(item, "label"); cJSON* detail = cJSON_GetObjectItem(item, "detail"); cJSON* kind = cJSON_GetObjectItem(item, "kind"); appDebug(" [%d] kind=%-2d %-40s %s\n", i++, kind ? kind->valueint : 0, label ? label->valuestring : "(no label)", detail ? detail->valuestring : ""); if (i >= 10) { appDebug(" ... (%d more)\n", count - 10); break; } } pthread_mutex_lock(&lsp->lock); lspParseCompletion(json, &E.lsp_completion, lsp->completion_cursor_x, lsp->completion_cursor_y); pthread_mutex_unlock(&lsp->lock); E.lsp_client->completion_just_arrived = 1; appDebug("[POPUP] visible=%d count=%d origin=(%d,%d)\n", E.lsp_completion.visible, E.lsp_completion.count, E.lsp_completion.origin_x, E.lsp_completion.origin_y); write(lsp->wake_pipe[1], "c", 1); cJSON_Delete(root); return; } // definition response → jump to location cJSON* uri_item = cJSON_GetObjectItem(result, "uri"); if (uri_item) { cJSON* range = cJSON_GetObjectItem(result, "range"); cJSON* start = cJSON_GetObjectItem(range, "start"); int line = cJSON_GetObjectItem(start, "line")->valueint; int col = cJSON_GetObjectItem(start, "character")->valueint; appDebug("[LSP ←] Definition: %s:%d:%d\n", uri_item->valuestring, line, col); E.lsp_client->completion_just_arrived = 1; // TODO: jump to that location cJSON_Delete(root); return; } appDebug("[LSP ←] Unhandled response id=%d: %.80s\n", response_id, json); } cJSON_Delete(root); } static void* lsp_reader(void* arg) { LspClient* lsp = (LspClient*)arg; while (lsp->state != LSP_SHUTDOWN) { char* msg = lsp_recv(lsp->read_fd); if (!msg) break; // ← pipe closed or error, exit cleanly lsp_dispatch(lsp, msg); bFree(msg); } return NULL; } // ─── lifecycle ─────────────────────────────────────────────────────────────── int lspStart(LspClient *lsp, const char *project_root) { // ── Pipes ───────────────────────────────────────────────────────────────── int to_clangd[2], from_clangd[2]; if (pipe(to_clangd) < 0 || pipe(from_clangd) < 0) { fprintf(stderr, "[LSP] pipe() failed\n"); free(lsp); return 0; } if (pipe(lsp->wake_pipe) < 0) { fprintf(stderr, "[LSP] wake pipe() failed\n"); free(lsp); return 0; } // ── Fork clangd ─────────────────────────────────────────────────────────── lsp->pid = fork(); if (lsp->pid < 0) { fprintf(stderr, "[LSP] fork() failed\n"); free(lsp); return 0; } if (lsp->pid == 0) { // Child — become clangd dup2(to_clangd[0], STDIN_FILENO); dup2(from_clangd[1], STDOUT_FILENO); close(to_clangd[1]); close(from_clangd[0]); close(lsp->wake_pipe[0]); close(lsp->wake_pipe[1]); execlp("clangd", "clangd", "--log=error", "--completion-style=detailed", NULL); fprintf(stderr, "[LSP] execlp failed — is clangd installed?\n"); _exit(1); } // Parent — keep write end of to_clangd, read end of from_clangd close(to_clangd[0]); close(from_clangd[1]); lsp->write_fd = to_clangd[1]; lsp->read_fd = from_clangd[0]; lsp->next_id = 1; lsp->state = LSP_INITIALIZING; // ── Threading ───────────────────────────────────────────────────────────── pthread_mutex_init(&lsp->lock, NULL); pthread_cond_init (&lsp->ready_cond, NULL); // Start reader thread BEFORE sending initialize // so it can handle the response pthread_create(&lsp->reader_thread, NULL, lsp_reader, lsp); // ── Send initialize ─────────────────────────────────────────────────────── char abs_root[PATH_MAX]; if (realpath(project_root, abs_root) == NULL) strncpy(abs_root, project_root, PATH_MAX - 1); cJSON *req = cJSON_CreateObject(); cJSON *params = cJSON_CreateObject(); cJSON *caps = cJSON_CreateObject(); cJSON *td_caps = cJSON_CreateObject(); cJSON *comp_caps = cJSON_CreateObject(); cJSON *comp_item = cJSON_CreateObject(); cJSON_AddStringToObject(req, "jsonrpc", "2.0"); cJSON_AddNumberToObject(req, "id", lsp->next_id++); cJSON_AddStringToObject(req, "method", "initialize"); // rootUri char root_uri[PATH_MAX + 8]; snprintf(root_uri, sizeof(root_uri), "file://%s", abs_root); cJSON_AddNumberToObject(params, "processId", getpid()); cJSON_AddStringToObject(params, "rootUri", root_uri); // Capabilities — tell clangd what we support cJSON_AddBoolToObject (comp_item, "snippetSupport", 0); cJSON_AddBoolToObject (comp_item, "commitCharactersSupport", 0); cJSON_AddItemToObject (comp_caps, "completionItem", comp_item); cJSON_AddItemToObject (td_caps, "completion", comp_caps); cJSON_AddItemToObject (td_caps, "hover", cJSON_CreateObject()); cJSON_AddItemToObject (td_caps, "definition", cJSON_CreateObject()); cJSON_AddItemToObject (td_caps, "publishDiagnostics", cJSON_CreateObject()); cJSON_AddItemToObject (caps, "textDocument", td_caps); cJSON_AddItemToObject (params, "capabilities", caps); cJSON_AddItemToObject (req, "params", params); char *msg = cJSON_PrintUnformatted(req); lsp_send(lsp->write_fd, msg); free(msg); cJSON_Delete(req); // ── Wait for LSP_READY ──────────────────────────────────────────────────── // Reader thread will handle the initialize response, // send "initialized", and signal ready_cond pthread_mutex_lock(&lsp->lock); while (lsp->state != LSP_READY) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 5; // 5 second timeout — clangd should respond fast int rc = pthread_cond_timedwait(&lsp->ready_cond, &lsp->lock, &ts); if (rc == ETIMEDOUT) { fprintf(stderr, "[LSP] timeout waiting for initialize response\n"); pthread_mutex_unlock(&lsp->lock); // Don't kill clangd — it might still come up, just return what we have return 1; } } pthread_mutex_unlock(&lsp->lock); fprintf(stderr, "[LSP] ready — clangd initialized at %s\n", abs_root); return 1; } // ─── document sync ─────────────────────────────────────────────────────────── // Build the full buffer text into a bAlloc'd string static char* buffer_to_text(struct buffer_t* buf) { int total = 0; for (int i = 0; i < buf->numrows; i++) total += buf->row[i].size + 1; // +1 for \n char* text = bAlloc(total + 1); char* p = text; for (int i = 0; i < buf->numrows; i++) { memcpy(p, buf->row[i].chars, buf->row[i].size); p += buf->row[i].size; *p++ = '\n'; } *p = '\0'; return text; } void lspDidOpen(LspClient* lsp, struct buffer_t* buf) { if (lsp->state != LSP_READY || buf->b_lsp_open) return; char uri[PATH_MAX + 8]; snprintf(uri, sizeof(uri), "file://%s", buf->fullname); const char* lang = "c"; if (strstr(buf->filename, ".cpp") || strstr(buf->filename, ".cc")) lang = "cpp"; char* raw = buffer_to_text(buf); // Let cJSON handle ALL escaping — don't touch the text yourself cJSON* root = cJSON_CreateObject(); cJSON* params = cJSON_CreateObject(); cJSON* td = cJSON_CreateObject(); cJSON_AddStringToObject(root, "jsonrpc", "2.0"); cJSON_AddStringToObject(root, "method", "textDocument/didOpen"); cJSON_AddStringToObject(td, "uri", uri); cJSON_AddStringToObject(td, "languageId", lang); cJSON_AddNumberToObject(td, "version", 1); cJSON_AddStringToObject(td, "text", raw); // cJSON escapes this cJSON_AddItemToObject(params, "textDocument", td); cJSON_AddItemToObject(root, "params", params); char* msg = cJSON_PrintUnformatted(root); lsp_send(lsp->write_fd, msg); bFree(msg); bFree(raw); cJSON_Delete(root); buf->b_lsp_open = 1; } void lspDidChange(LspClient* lsp, struct buffer_t* buf) { if (lsp->state != LSP_READY || !buf->b_lsp_open) return; char uri[PATH_MAX + 8]; snprintf(uri, sizeof(uri), "file://%s", buf->fullname); char* raw = buffer_to_text(buf); cJSON* root = cJSON_CreateObject(); cJSON* params = cJSON_CreateObject(); cJSON* td = cJSON_CreateObject(); cJSON* changes = cJSON_CreateArray(); cJSON* change = cJSON_CreateObject(); cJSON_AddStringToObject(root, "jsonrpc", "2.0"); cJSON_AddStringToObject(root, "method", "textDocument/didChange"); cJSON_AddStringToObject(td, "uri", uri); cJSON_AddNumberToObject(td, "version", buf->dirty); cJSON_AddStringToObject(change, "text", raw); // full content sync cJSON_AddItemToArray(changes, change); cJSON_AddItemToObject(params, "textDocument", td); cJSON_AddItemToObject(params, "contentChanges", changes); cJSON_AddItemToObject(root, "params", params); char* msg = cJSON_PrintUnformatted(root); lsp_send(lsp->write_fd, msg); bFree(msg); bFree(raw); cJSON_Delete(root); } void lspDidClose(LspClient* lsp, struct buffer_t* buf) { char uri[PATH_MAX + 8]; snprintf(uri, sizeof(uri), "file://%s", buf->fullname); cJSON* root = cJSON_CreateObject(); cJSON* params = cJSON_CreateObject(); cJSON* td = cJSON_CreateObject(); cJSON_AddStringToObject(root, "jsonrpc", "2.0"); cJSON_AddStringToObject(root, "method", "textDocument/didClose"); cJSON_AddStringToObject(td, "uri", uri); // ← fixed cJSON_AddItemToObject(params, "textDocument", td); cJSON_AddItemToObject(root, "params", params); char* msg = cJSON_PrintUnformatted(root); lsp_send(lsp->write_fd, msg); bFree(msg); cJSON_Delete(root); buf->b_lsp_open = 0; } // ─── requests ──────────────────────────────────────────────────────────────── void lspRequestCompletion(LspClient* lsp, struct buffer_t* buf, int line, int col, int screen_x, int screen_y) { if (lsp->state != LSP_READY) return; lsp->completion_cursor_x = screen_x; lsp->completion_cursor_y = screen_y; appDebug("LSP REQUEST COMP"); char* msg; char uri[PATH_MAX + 8]; snprintf(uri, sizeof(uri), "file://%s", buf->fullname); appDebug("FULLNAME : %s\n", buf->fullname); cJSON* req = cJSON_CreateObject(); cJSON* params = cJSON_CreateObject(); cJSON* td = cJSON_CreateObject(); cJSON* position = cJSON_CreateObject(); cJSON_AddStringToObject(req, "jsonrpc", "2.0"); cJSON_AddNumberToObject(req, "id", lsp->next_id++); cJSON_AddStringToObject(req, "method", "textDocument/completion"); cJSON_AddStringToObject(td, "uri", uri); cJSON_AddNumberToObject(position, "line", line); cJSON_AddNumberToObject(position, "character", col); cJSON_AddItemToObject(params, "position", position); cJSON_AddItemToObject(params, "textDocument", td); cJSON_AddItemToObject(req, "params", params); msg = cJSON_PrintUnformatted(req); lsp_send(lsp->write_fd, msg); E.lsp_client->completion_requested = 1; cJSON_Delete(req); bFree(msg); } void lspRequestDefinition(LspClient* lsp, struct buffer_t* buf, int line, int col) { if (lsp->state != LSP_READY) return; char* msg; asprintf(&msg, "{\"jsonrpc\":\"2.0\",\"id\":%d,\"method\":\"textDocument/definition\"," "\"params\":{\"textDocument\":{\"uri\":\"file://%s\"}," "\"position\":{\"line\":%d,\"character\":%d}}}", lsp->next_id++, buf->filename, line, col); lsp_send(lsp->write_fd, msg); bFree(msg); } void lspShutdown(LspClient* lsp) { if (!lsp || lsp->state == LSP_SHUTDOWN) return; lsp->state = LSP_SHUTDOWN; // 1. Send shutdown request (clangd expects this before exit) cJSON* req = cJSON_CreateObject(); cJSON_AddStringToObject(req, "jsonrpc", "2.0"); cJSON_AddNumberToObject(req, "id", lsp->next_id++); cJSON_AddStringToObject(req, "method", "shutdown"); cJSON_AddNullToObject(req, "params"); char* msg = cJSON_PrintUnformatted(req); lsp_send(lsp->write_fd, msg); bFree(msg); cJSON_Delete(req); // 2. Wait briefly for the shutdown response // (don't block forever — clangd has 2s to reply) struct timeval tv = {.tv_sec = 2, .tv_usec = 0}; fd_set fds; FD_ZERO(&fds); FD_SET(lsp->read_fd, &fds); if (select(lsp->read_fd + 1, &fds, NULL, NULL, &tv) > 0) { char* resp = lsp_recv(lsp->read_fd); bFree(resp); } // 3. Send exit notification lsp_send(lsp->write_fd, "{\"jsonrpc\":\"2.0\",\"method\":\"exit\",\"params\":null}"); // 4. Close pipes — this signals the reader thread to stop close(lsp->write_fd); close(lsp->read_fd); // 5. Wait for reader thread to finish pthread_join(lsp->reader_thread, NULL); pthread_mutex_destroy(&lsp->lock); // 6. Reap the clangd process waitpid(lsp->pid, NULL, 0); bFree(lsp); }