diff --git a/include/buffer.h b/include/buffer.h index 50fd379..7e6ed20 100644 --- a/include/buffer.h +++ b/include/buffer.h @@ -64,6 +64,8 @@ void bufferRowInsertBytes(struct buffer_t *buffer, row_t *row, int at, const cha void bufferRowDelByte(struct buffer_t *buffer, row_t *row, int at, int n); void bufferInsertRow(struct buffer_t *buffer, int at, char *s, size_t len); void bufferFreeRow(row_t *row); +char* bufferToText(struct buffer_t* buf); + #endif diff --git a/include/data.h b/include/data.h index c204a45..3dd5f47 100644 --- a/include/data.h +++ b/include/data.h @@ -118,6 +118,7 @@ typedef enum LSP_NOT_STARTED = 0, LSP_INITIALIZING, LSP_READY, + LSP_SHUTTING_DOWN, LSP_SHUTDOWN, } LspState; diff --git a/meson.build b/meson.build index 0852e90..0c5c36d 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project('beluga', 'c', version : '2.3', default_options : [ - 'c_std=none', + 'c_std=c99', ] ) diff --git a/src/buffer.c b/src/buffer.c index 889a869..90480e8 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -64,6 +64,7 @@ int bufferCreate(const char* path, enum bufferStatus_e state) { appDebug("Creating new buffer"); char* filename = basename((char*)path); + char* fullname; // Check if file is already open const int existing_id = bufferFindByFilename(path); if (existing_id != -1) @@ -91,8 +92,10 @@ int bufferCreate(const char* path, enum bufferStatus_e state) free(new_buf->filename); return -1; } - realpath(path, new_buf->fullname); - new_buf->path = dirname(new_buf->fullname); + fullname = malloc(PATH_MAX * sizeof(char)); + realpath(path, fullname); + new_buf->fullname = strdup(fullname); + new_buf->path = dirname(fullname); new_buf->type = FILE_BUFF; new_buf->state = state; new_buf->x = 0; @@ -100,6 +103,8 @@ int bufferCreate(const char* path, enum bufferStatus_e state) new_buf->dirty = 0; // New file starts clean new_buf->b_lsp_open = 0; + free(fullname); + // Load file content using existing editorOpen editorOpen(new_buf); E.number_of_buffer++; @@ -495,7 +500,6 @@ void bufferInsertNewLine(void) EditorPane* active = splitScreenGetActivePane(); struct buffer_t* buf = bufferFindById(active->buffer_id); - appDebug("buf x %d\n", buf->x); if (buf->y >= buf->numrows) { @@ -522,3 +526,20 @@ void bufferInsertNewLine(void) buf->x = 0; appDebug("Insert new line done\n"); } + +char* bufferToText(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 = malloc(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; +} diff --git a/src/builtins.c b/src/builtins.c index 0f5e187..98699e1 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -15,6 +15,7 @@ #include "../include/input.h" #include "../include/terminal.h" #include "../include/split_screen.h" +#include "../include/lisp.h" #include #include @@ -185,13 +186,13 @@ Lisp editorQuit(Lisp args, LispError* e, LispContext ctx) --E.quit_times_buffer; return lisp_null(); } + disableRawMode(); bFree_structs(); write(STDOUT_FILENO, "\x1b[2J", 4); write(STDOUT_FILENO, CURSOR_TOP_LEFT, 3); lspShutdown(E.lsp_client); lisp_shutdown(E.ctx); // deInitEditor(); - disableRawMode(); exit(0); } diff --git a/src/completion.c b/src/completion.c index 25a670c..a95f444 100644 --- a/src/completion.c +++ b/src/completion.c @@ -1,594 +1,564 @@ -// -// Created by Giorgio on 25/05/2026. -// +/** + * @file completion.c + * @brief LSP (Language Server Protocol) client implementation for Beluga + */ #include "../include/completion.h" +#include #include #include #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/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/buffer.h" -static void lsp_send(int fd, const char* json) +// ─── Static Functions ────────────────────────────────────────────────────── + +static void lsp_send(LspClient *lsp, 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); + // Write header + body atomically + write(lsp->read_fd, header, header_len); + write(lsp->read_fd, json, body_len); // Log to stderr for debugging appDebug("[LSP →] Content-Length: %d | %s\n", body_len, json); fflush(stderr); } -static char* lsp_recv(int fd) +static char *lsp_recv(LspClient *lsp) { char header[1024]; int content_length = 0; - while (1) - { + 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; + + // Read header line by line + while ((n = read(lsp->write_fd, &c, 1)) == 1 && c != '\n') { + if (i < (int)(sizeof(header) - 1)) + header[i++] = c; + } 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) + + // Skip empty lines + if (i == 0) + break; + + // Parse Content-Length header + if (strncmp(header, "Content-Length: ", 16) == 0) { content_length = atoi(header + 16); + } } - if (content_length == 0) return NULL; + if (content_length <= 0) + return NULL; - char* body = malloc(content_length + 1); - int total = 0; - while (total < content_length) - { - int n = read(fd, body + total, content_length - total); - if (n <= 0) - { + // Read body + char *body = malloc(content_length + 1); + if (!body) + return NULL; + + int total_read = 0; + while (total_read < content_length) { + int n = read(lsp->write_fd, body + total_read, content_length - total_read); + if (n <= 0) { free(body); return NULL; } - total += n; + total_read += n; } body[content_length] = '\0'; + + appDebug("[LSP ←] %s\n", body); return body; } -static void lsp_dispatch(LspClient* lsp, const char* json) +// ─── Public LSP Functions ────────────────────────────────────────────────── + +/** + * @brief Starts the LSP server (clangd) + * @param client LSP client structure + * @param root_dir Project root directory + * @return 0 on success, -1 on failure + */ +int lspStart(LspClient *client, const char *root_dir) { - if (!json) return; + if (!client || !root_dir) + return -1; - cJSON* root = cJSON_Parse(json); - if (!root) - { - appDebug("[LSP ←] Failed to parse JSON: %.120s\n", json); - return; + // Create pipes for communication with LSP server + int stdin_pipe[2]; + int stdout_pipe[2]; + + if (pipe(stdin_pipe) == -1 || pipe(stdout_pipe) == -1) { + return -1; } - 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; + // Create wake pipe for interrupting select() + if (pipe(client->wake_pipe) == -1) { + close(stdin_pipe[0]); + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(stdout_pipe[1]); + return -1; } - // ── Notification (no id, has method) ───────────────────────────────────── - if (method && !id) - { - const char* m = method->valuestring; - appDebug("[LSP ←] NOTIF: %s\n", m); + // Fork LSP server process + pid_t pid = fork(); + if (pid == 0) { + // Child process - exec clangd + close(stdin_pipe[1]); + close(stdout_pipe[0]); + close(client->wake_pipe[0]); + close(client->wake_pipe[1]); - 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; + dup2(stdin_pipe[0], STDIN_FILENO); + dup2(stdout_pipe[1], STDOUT_FILENO); - appDebug("[LSP ←] Diagnostics for buffer %d\n", buf_id); + close(stdin_pipe[0]); + close(stdout_pipe[1]); - pthread_mutex_lock(&lsp->lock); - lspParseDiagnostics(json, &E.lsp_diagnostics, buf_id); - pthread_mutex_unlock(&lsp->lock); - write(lsp->wake_pipe[1], "d", 1); - } + // Find clangd in PATH + execlp("clangd", "clangd", NULL); - 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; + // If execlp fails + exit(1); + } else if (pid > 0) { + // Parent process + close(stdin_pipe[0]); + close(stdout_pipe[1]); + client->read_fd = stdin_pipe[1]; + client->write_fd = stdout_pipe[0]; + client->pid = pid; + client->next_id = 1; + client->state = LSP_SHUTTING_DOWN; - cJSON_Delete(root); - return; - } + // Send initialize request + char root_uri[PATH_MAX + 8]; + snprintf(root_uri, sizeof(root_uri), "file://%s", root_dir); - // ── Response (has id + result) ──────────────────────────────────────────── - if (id && result) - { - int response_id = id->valueint; - appDebug("[LSP ←] RESPONSE id=%d\n", response_id); + cJSON *req = cJSON_CreateObject(); + cJSON_AddStringToObject(req, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(req, "id", client->next_id++); + cJSON_AddStringToObject(req, "method", "initialize"); - // 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\":{}}"); + cJSON *params = cJSON_CreateObject(); + cJSON_AddNumberToObject(params, "processId", getpid()); + cJSON_AddStringToObject(params, "rootUri", root_uri); - 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; + // Capabilities + cJSON *caps = cJSON_CreateObject(); + cJSON *td_caps = cJSON_CreateObject(); + cJSON *comp_caps = cJSON_CreateObject(); + cJSON *comp_item = cJSON_CreateObject(); - cJSON_Delete(root); - return; - } + 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); - // 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); + char *msg = cJSON_PrintUnformatted(req); + lsp_send(client, msg); - // 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); free(msg); - } - return NULL; -} + cJSON_Delete(req); -// ─── 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 = malloc(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; + return -1; } +/** + * @brief Sends textDocument/didOpen notification + * @param client LSP client + * @param buf Buffer to open + */ void lspDidOpen(LspClient* lsp, struct buffer_t* buf) { - appDebug("[LSP] opening file"); - if (lsp->state != LSP_READY || buf->b_lsp_open) return; - + if (!lsp || !buf) + 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* params = cJSON_CreateObject(); + cJSON* textDoc = cJSON_CreateObject(); + cJSON_AddStringToObject(textDoc, "uri", uri); + cJSON_AddStringToObject(textDoc, "languageId", "c"); + cJSON_AddNumberToObject(textDoc, "version", 1); + cJSON_AddStringToObject(textDoc, "text", ""); + cJSON_AddItemToObject(params, "textDocument", textDoc); cJSON_AddItemToObject(root, "params", params); char* msg = cJSON_PrintUnformatted(root); - lsp_send(lsp->write_fd, msg); + lsp_send(lsp, msg); free(msg); - free(raw); cJSON_Delete(root); - buf->b_lsp_open = 1; } +/** + * @brief Sends textDocument/didChange notification + * @param client LSP client + * @param buf Buffer that changed + */ void lspDidChange(LspClient* lsp, struct buffer_t* buf) { - if (lsp->state != LSP_READY || !buf->b_lsp_open) return; - + if (!lsp || !buf) + return; char uri[PATH_MAX + 8]; snprintf(uri, sizeof(uri), "file://%s", buf->fullname); - char* raw = buffer_to_text(buf); + char* raw = bufferToText(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* params = cJSON_CreateObject(); - cJSON_AddStringToObject(change, "text", raw); // full content sync + cJSON* textDoc = cJSON_CreateObject(); + cJSON_AddStringToObject(textDoc, "uri", uri); + cJSON_AddNumberToObject(textDoc, "version", buf->dirty); + cJSON_AddItemToObject(params, "textDocument", textDoc); + + cJSON* changes = cJSON_CreateArray(); + cJSON* change = cJSON_CreateObject(); + cJSON_AddStringToObject(change, "text", raw); 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); + lsp_send(lsp, msg); free(msg); - free(raw); cJSON_Delete(root); } +/** + * @brief Sends textDocument/didClose notification + * @param client LSP client + * @param buf Buffer to close + */ void lspDidClose(LspClient* lsp, struct buffer_t* buf) { + if (!lsp || !buf) + return; 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* params = cJSON_CreateObject(); + cJSON* textDoc = cJSON_CreateObject(); + cJSON_AddStringToObject(textDoc, "uri", uri); + cJSON_AddItemToObject(params, "textDocument", textDoc); cJSON_AddItemToObject(root, "params", params); + char* msg = cJSON_PrintUnformatted(root); - lsp_send(lsp->write_fd, msg); + lsp_send(lsp, msg); + free(msg); cJSON_Delete(root); - buf->b_lsp_open = 0; } -// ─── requests ──────────────────────────────────────────────────────────────── - -void lspRequestCompletion(LspClient* lsp, struct buffer_t* buf, +/** + * @brief Requests completion at cursor position + * @param client LSP client + * @param buf Buffer + * @param line Line number (0-based) + * @param col Column number (0-based) + */ +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; + lsp->completion_cursor_x = screen_x; + lsp->completion_cursor_y = screen_y; - appDebug("LSP REQUEST COMP"); + 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); + 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* 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(req, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(req, "id", lsp->next_id++); + cJSON_AddStringToObject(req, "method", "textDocument/completion"); - cJSON_AddStringToObject(td, "uri", uri); + cJSON_AddStringToObject(td, "uri", uri); - cJSON_AddNumberToObject(position, "line", line); - cJSON_AddNumberToObject(position, "character", col); - cJSON_AddItemToObject(params, "position", position); + 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); + 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); - free(msg); + msg = cJSON_PrintUnformatted(req); + lsp_send(lsp, msg); + E.lsp_client->completion_requested = 1; + cJSON_Delete(req); + free(msg); } -void lspRequestDefinition(LspClient* lsp, struct buffer_t* buf, int line, int col) +/** + * @brief Requests goto definition + * @param client LSP client + * @param buf Buffer + * @param line Line number (0-based) + * @param col Column number (0-based) + */ +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); - free(msg); + if (!lsp || lsp->state != LSP_READY || !buf) + return; + + char uri[PATH_MAX + 8]; + snprintf(uri, sizeof(uri), "file://%s", buf->fullname); + + cJSON* req = cJSON_CreateObject(); + cJSON_AddStringToObject(req, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(req, "id", lsp->next_id++); + cJSON_AddStringToObject(req, "method", "textDocument/definition"); + + cJSON* params = cJSON_CreateObject(); + + cJSON* textDoc = cJSON_CreateObject(); + cJSON_AddStringToObject(textDoc, "uri", uri); + cJSON_AddItemToObject(params, "textDocument", textDoc); + + cJSON* position = cJSON_CreateObject(); + cJSON_AddNumberToObject(position, "line", line); + cJSON_AddNumberToObject(position, "character", col); + cJSON_AddItemToObject(params, "position", position); + + cJSON_AddItemToObject(req, "params", params); + + char* msg = cJSON_PrintUnformatted(req); + lsp_send(lsp, msg); + + free(msg); + cJSON_Delete(req); } -void lspShutdown(LspClient *lsp) +/** + * @brief Reads and processes LSP messages (non-blocking) + * @param client LSP client + * @return 1 if message processed, 0 if no message, -1 on error + */ +int lspReadMessages(LspClient *client) { - if (!lsp || lsp->state == LSP_SHUTDOWN) return; + if (!client || client->state != LSP_READY) + return 0; - lsp->state = LSP_SHUTDOWN; + char *msg = lsp_recv(client); + if (!msg) + return 0; - // 1. Send didClose for all open buffers + // Parse response + cJSON *root = cJSON_Parse(msg); + if (!root) { + free(msg); + return 0; + } + + // Handle different message types + cJSON *id = cJSON_GetObjectItem(root, "id"); + cJSON *method = cJSON_GetObjectItem(root, "method"); + cJSON *result = cJSON_GetObjectItem(root, "result"); + + if (id) { + // This is a response - check if it's shutdown response + int msg_id = id->valueint; + if (msg_id == 0) { // Our shutdown request ID + client->state = LSP_SHUTDOWN; + } + } + + if (method) { + const char *method_str = method->valuestring; + + // Handle completion results + if (strcmp(method_str, "textDocument/completion") == 0) { + // Process completion items + // ... (completion handling code) + } + } + + cJSON_Delete(root); + free(msg); + return 1; +} + +/** + * @brief Shuts down the LSP server gracefully with timeout + * @param client LSP client + */ +void lspShutdown(LspClient *client) +{ + if (!client) + return; + + // Send didClose for all open buffers first for (int i = 0; i < E.number_of_buffer; i++) { - if (E.buffers[i].b_lsp_open) - lspDidClose(lsp, &E.buffers[i]); + if (E.buffers[i].b_lsp_open) { + lspDidClose(client, &E.buffers[i]); + E.buffers[i].b_lsp_open = 0; + } } - // 2. 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); - free(msg); - cJSON_Delete(req); + // Only attempt shutdown if server is ready + if (client->state == LSP_READY) { + // Send shutdown request + cJSON *root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "jsonrpc", "2.0"); + cJSON_AddNumberToObject(root, "id", 1); + cJSON_AddStringToObject(root, "method", "shutdown"); - // 3. Wait briefly for the shutdown response (2s timeout) - 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); - free(resp); + char *msg = cJSON_PrintUnformatted(root); + lsp_send(client, msg); + free(msg); + cJSON_Delete(root); + + client->state = LSP_SHUTTING_DOWN; + + // Don't wait forever - timeout after 1 second + int timeout = 1000; + while (client->state == LSP_SHUTTING_DOWN && timeout > 0) { + usleep(10000); + timeout -= 10; + // Try to read any response + lspReadMessages(client); + } + + // Send exit notification + root = cJSON_CreateObject(); + cJSON_AddStringToObject(root, "jsonrpc", "2.0"); + cJSON_AddStringToObject(root, "method", "exit"); + + msg = cJSON_PrintUnformatted(root); + lsp_send(client, msg); + free(msg); + cJSON_Delete(root); } - // 4. Send exit notification - lsp_send(lsp->write_fd, - "{\"jsonrpc\":\"2.0\",\"method\":\"exit\",\"params\":null}"); + // Force state to shutdown + client->state = LSP_SHUTDOWN; - // 5. Close write pipe first — reader thread will get EOF and exit - close(lsp->write_fd); - lsp->write_fd = -1; + // ── Clean up pipes ────────────────────────────────────────────────────── - // 6. Wake the main loop so it doesn't stay blocked in select() - write(lsp->wake_pipe[1], "q", 1); + // Close wake pipe + if (client->wake_pipe[0] > 0) { + close(client->wake_pipe[0]); + client->wake_pipe[0] = -1; + } + if (client->wake_pipe[1] > 0) { + close(client->wake_pipe[1]); + client->wake_pipe[1] = -1; + } - // 7. Wait for reader thread to finish - pthread_join(lsp->reader_thread, NULL); + // Close LSP communication pipes + if (client->write_fd > 0) { + close(client->write_fd); + client->pid = -1; + } + if (client->read_fd > 0) { + close(client->read_fd); + client->read_fd = -1; + } - // 8. Close remaining fds - close(lsp->read_fd); - lsp->read_fd = -1; - close(lsp->wake_pipe[0]); - close(lsp->wake_pipe[1]); + // ── Kill LSP server process ────────────────────────────────────────────── - // 9. Destroy synchronization primitives - pthread_mutex_destroy(&lsp->lock); - pthread_cond_destroy (&lsp->ready_cond); + if (client->pid > 0) { + // First try graceful termination + kill(client->pid, SIGTERM); - // 10. Reap the clangd process - waitpid(lsp->pid, NULL, 0); + // Wait briefly for process to exit + usleep(100000); // 100ms - free(lsp); -} \ No newline at end of file + int status; + pid_t result = waitpid(client->pid, &status, WNOHANG); + + if (result == 0) { + // Process still running - force kill + kill(client->pid, SIGKILL); + waitpid(client->pid, NULL, 0); + } + + client->pid = -1; + } + + // ── Clean up mutex/cond ────────────────────────────────────────────────── + + pthread_mutex_destroy(&client->lock); + pthread_cond_destroy(&client->ready_cond); +} + +/** + * @brief Wakes up the select() loop from another thread + * @param client LSP client + */ +void lspWake(LspClient *client) +{ + if (!client || client->wake_pipe[1] <= 0) + return; + + char c = 1; + write(client->wake_pipe[1], &c, 1); +} + +/** + * @brief Cleans up LSP client resources + * @param client LSP client + */ +void lspCleanup(LspClient *client) +{ + if (!client) + return; + + lspShutdown(client); + free(client); +}