leaks fix

This commit is contained in:
2026-06-05 11:04:01 +02:00
parent 23f7701eb9
commit 2ad55b9132
6 changed files with 440 additions and 445 deletions
+2
View File
@@ -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
+1
View File
@@ -118,6 +118,7 @@ typedef enum
LSP_NOT_STARTED = 0,
LSP_INITIALIZING,
LSP_READY,
LSP_SHUTTING_DOWN,
LSP_SHUTDOWN,
} LspState;
+1 -1
View File
@@ -1,7 +1,7 @@
project('beluga', 'c',
version : '2.3',
default_options : [
'c_std=none',
'c_std=c99',
]
)
+24 -3
View File
@@ -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;
}
+2 -1
View File
@@ -15,6 +15,7 @@
#include "../include/input.h"
#include "../include/terminal.h"
#include "../include/split_screen.h"
#include "../include/lisp.h"
#include <stdio.h>
#include <stdlib.h>
@@ -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);
}
+385 -415
View File
@@ -1,483 +1,309 @@
//
// Created by Giorgio on 25/05/2026.
//
/**
* @file completion.c
* @brief LSP (Language Server Protocol) client implementation for Beluga
*/
#include "../include/completion.h"
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/syslimits.h>
#include <sys/wait.h>
#include <errno.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/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')
// 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;
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)
// 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;
}
// ── 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);
free(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);
// Send initialize request
char root_uri[PATH_MAX + 8];
snprintf(root_uri, sizeof(root_uri), "file://%s", root_dir);
cJSON *req = cJSON_CreateObject();
cJSON_AddStringToObject(req, "jsonrpc", "2.0");
cJSON_AddNumberToObject(req, "id", client->next_id++);
cJSON_AddStringToObject(req, "method", "initialize");
cJSON *params = cJSON_CreateObject();
cJSON_AddNumberToObject(params, "processId", getpid());
cJSON_AddStringToObject(params, "rootUri", root_uri);
// Capabilities
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);
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);
lsp_send(client, 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;
return 0;
}
}
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)
{
@@ -511,84 +337,228 @@ void lspRequestCompletion(LspClient* lsp, struct buffer_t* buf,
cJSON_AddItemToObject(req, "params", params);
msg = cJSON_PrintUnformatted(req);
lsp_send(lsp->write_fd, msg);
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;
void lspShutdown(LspClient *lsp)
{
if (!lsp || lsp->state == LSP_SHUTDOWN) return;
char uri[PATH_MAX + 8];
snprintf(uri, sizeof(uri), "file://%s", buf->fullname);
lsp->state = LSP_SHUTDOWN;
// 1. Send didClose for all open buffers
for (int i = 0; i < E.number_of_buffer; i++) {
if (E.buffers[i].b_lsp_open)
lspDidClose(lsp, &E.buffers[i]);
}
// 2. Send shutdown request (clangd expects this before exit)
cJSON *req = cJSON_CreateObject();
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);
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);
}
// 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);
/**
* @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 (!client || client->state != LSP_READY)
return 0;
char *msg = lsp_recv(client);
if (!msg)
return 0;
// Parse response
cJSON *root = cJSON_Parse(msg);
if (!root) {
free(msg);
return 0;
}
// 4. Send exit notification
lsp_send(lsp->write_fd,
"{\"jsonrpc\":\"2.0\",\"method\":\"exit\",\"params\":null}");
// Handle different message types
cJSON *id = cJSON_GetObjectItem(root, "id");
cJSON *method = cJSON_GetObjectItem(root, "method");
cJSON *result = cJSON_GetObjectItem(root, "result");
// 5. Close write pipe first — reader thread will get EOF and exit
close(lsp->write_fd);
lsp->write_fd = -1;
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;
}
}
// 6. Wake the main loop so it doesn't stay blocked in select()
write(lsp->wake_pipe[1], "q", 1);
if (method) {
const char *method_str = method->valuestring;
// 7. Wait for reader thread to finish
pthread_join(lsp->reader_thread, NULL);
// Handle completion results
if (strcmp(method_str, "textDocument/completion") == 0) {
// Process completion items
// ... (completion handling code)
}
}
// 8. Close remaining fds
close(lsp->read_fd);
lsp->read_fd = -1;
close(lsp->wake_pipe[0]);
close(lsp->wake_pipe[1]);
// 9. Destroy synchronization primitives
pthread_mutex_destroy(&lsp->lock);
pthread_cond_destroy (&lsp->ready_cond);
// 10. Reap the clangd process
waitpid(lsp->pid, NULL, 0);
free(lsp);
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(client, &E.buffers[i]);
E.buffers[i].b_lsp_open = 0;
}
}
// 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");
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);
}
// Force state to shutdown
client->state = LSP_SHUTDOWN;
// ── Clean up pipes ──────────────────────────────────────────────────────
// 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;
}
// 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;
}
// ── Kill LSP server process ──────────────────────────────────────────────
if (client->pid > 0) {
// First try graceful termination
kill(client->pid, SIGTERM);
// Wait briefly for process to exit
usleep(100000); // 100ms
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);
}