Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ SRCS = src/main.c \
src/lexer/token.c \
src/analysis/typecheck.c \
src/lsp/json_rpc.c \
src/lsp/json_rpc_error.c \
src/lsp/lsp_main.c \
src/lsp/lsp_analysis.c \
src/lsp/lsp_index.c \
Expand Down Expand Up @@ -192,6 +193,7 @@ test: $(TARGET) $(PLUGINS)
./tests/run_tests.sh
./tests/run_codegen_tests.sh
./tests/run_example_transpile.sh
./tests/run_lsp_tests.sh

# Build with alternative compilers
zig:
Expand Down
312 changes: 234 additions & 78 deletions src/lsp/json_rpc.c
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#include "json_rpc.h"
#include "cJSON.h"
#include "lsp_project.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

void lsp_check_file(const char *uri, const char *src, int id);
void lsp_check_file(const char *uri, const char *src);
void lsp_goto_definition(const char *uri, int line, int col, int id);
void lsp_hover(const char *uri, int line, int col, int id);
void lsp_completion(const char *uri, int line, int col, int id);
Expand Down Expand Up @@ -48,6 +50,203 @@ static void get_params(cJSON *root, char **uri, int *line, int *col)
}
}

void send_lsp_message_json(const cJSON *json)
{
char *body;
size_t len;

assert(json);

body = cJSON_PrintUnformatted(json);

len = strlen(body);

fprintf(stdout, "Content-Length: %zu\r\n\r\n", len);
fwrite(body, 1, len, stdout);
fflush(stdout);
Comment on lines +58 to +66
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

send_lsp_message_json assumes cJSON_PrintUnformatted always succeeds; if it returns NULL (OOM), strlen(body) will dereference NULL and crash. Handle a NULL return (e.g., bail out or send an internal error) before calling strlen/fwrite.

Copilot uses AI. Check for mistakes.

free(body);
}

cJSON *create_response(const cJSON *id_item,
const cJSON *result,
const cJSON *error)
{
cJSON *res;

assert(id_item || (error && !id_item));
assert((result && !error) || (!result && error));

res = cJSON_CreateObject();
if (!res) {
return NULL;
}

cJSON_AddStringToObject(res, "jsonrpc", "2.0");

cJSON_AddItemToObject(res, "id", cJSON_Duplicate(id_item, 1));

if (result)
{
cJSON_AddItemToObject(res, "result", cJSON_Duplicate(result, 1));
}
else {
Comment on lines +87 to +93
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_response drops the id field when id_item is NULL because cJSON_AddItemToObject(..., NULL) fails (see src/lsp/cJSON.c:2108). JSON-RPC error responses must include an explicit "id": null in this case. Use cJSON_AddNullToObject(res, "id") when id_item is NULL.

Suggested change
cJSON_AddItemToObject(res, "id", cJSON_Duplicate(id_item, 1));
if (result)
{
cJSON_AddItemToObject(res, "result", cJSON_Duplicate(result, 1));
}
else {
if (id_item)
{
cJSON_AddItemToObject(res, "id", cJSON_Duplicate(id_item, 1));
}
else
{
cJSON_AddNullToObject(res, "id");
}
if (result)
{
cJSON_AddItemToObject(res, "result", cJSON_Duplicate(result, 1));
}
else
{

Copilot uses AI. Check for mistakes.
cJSON_AddItemToObject(res, "error", cJSON_Duplicate(error, 1));
}

return res;
}

void handle_initialize(const cJSON *json, const cJSON *id_item)
{
cJSON *params = cJSON_GetObjectItem(json, "params");
char *root = NULL;
if (params)
{
cJSON *rp = cJSON_GetObjectItem(params, "rootPath");
if (rp && rp->valuestring)
{
root = strdup(rp->valuestring);
}
else
{
cJSON *ru = cJSON_GetObjectItem(params, "rootUri");
if (ru && ru->valuestring)
{
root = strdup(ru->valuestring);
}
}
}

if (root && strncmp(root, "file://", 7) == 0)
{
char *clean = strdup(root + 7);
free(root);
root = clean;
}

lsp_project_init(root ? root : ".");
if (root)
{
free(root);
}

cJSON *result = cJSON_CreateObject();

cJSON *serverInfo = cJSON_AddObjectToObject(result, "serverInfo");
cJSON_AddStringToObject(serverInfo, "name", "ZenC LS");
cJSON_AddStringToObject(serverInfo, "version", "1.0.0");

// server capabilities
cJSON *caps = cJSON_AddObjectToObject(result, "capabilities");

cJSON *sync = cJSON_AddObjectToObject(caps, "textDocumentSync");
cJSON_AddBoolToObject(sync, "openClose", true);
cJSON_AddNumberToObject(sync, "change", 1);

cJSON_AddBoolToObject(caps, "definitionProvider", true);
cJSON_AddBoolToObject(caps, "hoverProvider", true);
cJSON_AddBoolToObject(caps, "referencesProvider", true);
cJSON_AddBoolToObject(caps, "documentSymbolProvider", true);

cJSON *sig = cJSON_AddObjectToObject(caps, "signatureHelpProvider");
cJSON *sig_trig = cJSON_AddArrayToObject(sig, "triggerCharacters");
cJSON_AddItemToArray(sig_trig, cJSON_CreateString("("));

cJSON *comp = cJSON_AddObjectToObject(caps, "completionProvider");
cJSON *comp_trig = cJSON_AddArrayToObject(comp, "triggerCharacters");
cJSON_AddItemToArray(comp_trig, cJSON_CreateString("."));

cJSON *response = create_response(id_item, result, NULL);
send_lsp_message_json(response);

cJSON_Delete(result);
cJSON_Delete(response);
Comment on lines +160 to +164
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_initialize calls create_response(id_item, ...) without checking whether id_item is present. If initialize is sent as a notification (no id), this will hit the create_response assertions (or produce an invalid response). Guard this path: either treat missing id as a notification (initialize but do not respond), or return a JSON-RPC Invalid Request error with id: null.

Suggested change
cJSON *response = create_response(id_item, result, NULL);
send_lsp_message_json(response);
cJSON_Delete(result);
cJSON_Delete(response);
if (id_item)
{
cJSON *response = create_response(id_item, result, NULL);
send_lsp_message_json(response);
cJSON_Delete(response);
}
cJSON_Delete(result);

Copilot uses AI. Check for mistakes.
}

void handle_shutdown(cJSON *id_item)
{
assert(id_item);

fprintf(stderr, "zls: shutdown received\n");

// lsp_state.shutdown = 1;
// TODO: after shutdown every request except exit gonna send JSONRPC_INVALID_REQUEST

cJSON *result = cJSON_CreateNull();

cJSON *response = create_response(id_item, result, NULL);

send_lsp_message_json(response);

cJSON_Delete(result);
cJSON_Delete(response);
}

void handle_exit(void)
{
fprintf(stderr, "zls: exit received\n");
// TODO: add the lsp clean here

// TODO: exit 0 if shutdown is call before exit else exit 1
exit(0);
}
Comment on lines +186 to +193
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions spec-compliant shutdown/exit handling, but handle_exit always exits with code 0 and still has TODOs. LSP requires exit code 0 only if a prior shutdown request was received; otherwise exit code 1. Track shutdown state and implement the correct exit code behavior.

Copilot uses AI. Check for mistakes.

void handle_did_open(const cJSON *json)
{
cJSON *params = cJSON_GetObjectItem(json, "params");
if (!params)
{
return;
}
cJSON *doc = cJSON_GetObjectItem(params, "textDocument");
if (!doc)
{
return;
}
cJSON *uri = cJSON_GetObjectItem(doc, "uri");
if (!uri || !cJSON_IsString(uri))
{
return;
}
cJSON *text = cJSON_GetObjectItem(doc, "text");
if (!text || !cJSON_IsString(text))
{
return;
}
lsp_check_file(uri->valuestring, text->valuestring);
}

void handle_did_change(const cJSON *json)
{
cJSON *params = cJSON_GetObjectItem(json, "params");
if (!params)
{
return;
}
cJSON *changes = cJSON_GetObjectItem(params, "contentChanges");
if (!changes)
{
return;
}
cJSON *doc = cJSON_GetObjectItem(params, "textDocument");
if (!doc)
{
return;
}
cJSON *uri = cJSON_GetObjectItem(doc, "uri");
if (!uri || !cJSON_IsString(uri))
{
return;
}
cJSON *text = cJSON_GetObjectItem(changes, "text");
if (!text || !cJSON_IsString(text))
{
Comment on lines +242 to +244
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentChanges in textDocument/didChange is an array per LSP; using cJSON_GetObjectItem(changes, "text") will always return NULL and the change will be ignored. Extract the first array element (or iterate) and read its text field.

Copilot uses AI. Check for mistakes.
return;
}
lsp_check_file(uri->valuestring, text->valuestring);
}

void handle_request(const char *json_str)
{
cJSON *json = cJSON_Parse(json_str);
Expand All @@ -58,94 +257,39 @@ void handle_request(const char *json_str)

int id = 0;
cJSON *id_item = cJSON_GetObjectItem(json, "id");
if (id_item)
{
id = id_item->valueint;
}
if (id_item && !(cJSON_IsNumber(id_item) || cJSON_IsString(id_item)))
{
invalid_request(NULL);
cJSON_Delete(json);
return;
}
if (id_item)
{
// FIXME: not always int but can be string too
id = id_item->valueint;
}
Comment on lines +266 to +270
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code accepts string IDs but still reads id_item->valueint and passes an int id to most handlers; for string IDs valueint becomes 0, causing responses with the wrong id. To be JSON-RPC compliant, propagate the original id_item (number or string) into responses (e.g., change handler signatures to take const cJSON *id_item, or convert to a string/variant) instead of truncating to int.

Copilot uses AI. Check for mistakes.

cJSON *method_item = cJSON_GetObjectItem(json, "method");
if (!method_item || !method_item->valuestring)
if (!method_item || !cJSON_IsString(method_item))
{
invalid_request(id_item);
cJSON_Delete(json);
return;
}
char *method = method_item->valuestring;

if (strcmp(method, "initialize") == 0)
{
cJSON *params = cJSON_GetObjectItem(json, "params");
char *root = NULL;
if (params)
{
cJSON *rp = cJSON_GetObjectItem(params, "rootPath");
if (rp && rp->valuestring)
{
root = strdup(rp->valuestring);
}
else
{
cJSON *ru = cJSON_GetObjectItem(params, "rootUri");
if (ru && ru->valuestring)
{
root = strdup(ru->valuestring);
}
}
}

if (root && strncmp(root, "file://", 7) == 0)
{
char *clean = strdup(root + 7);
free(root);
root = clean;
}

lsp_project_init(root ? root : ".");
if (root)
{
free(root);
}

const char *response =
"{\"jsonrpc\":\"2.0\",\"id\":0,\"result\":{"
"\"serverInfo\":{\"name\":\"ZenC LS\",\"version\": \"1.0.0\"},"
"\"capabilities\":{\"textDocumentSync\":{\"openClose\":true,\"change\":1},"
"\"definitionProvider\":true,\"hoverProvider\":true,"
"\"referencesProvider\":true,\"documentSymbolProvider\":true,"
"\"signatureHelpProvider\":{\"triggerCharacters\":[\"(\"]},"
"\"completionProvider\":{"
"\"triggerCharacters\":[\".\"]}}}}";
fprintf(stdout, "Content-Length: %zu\r\n\r\n%s", strlen(response), response);
fflush(stdout);
}
else if (strcmp(method, "textDocument/didOpen") == 0 ||
strcmp(method, "textDocument/didChange") == 0)
{
cJSON *params = cJSON_GetObjectItem(json, "params");
if (params)
{
cJSON *doc = cJSON_GetObjectItem(params, "textDocument");
if (doc)
{
cJSON *uri = cJSON_GetObjectItem(doc, "uri");
cJSON *text = cJSON_GetObjectItem(doc, "text");
// For didChange, text is inside contentChanges
if (!text && strcmp(method, "textDocument/didChange") == 0)
{
cJSON *changes = cJSON_GetObjectItem(params, "contentChanges");
if (changes && cJSON_GetArraySize(changes) > 0)
{
cJSON *change = cJSON_GetArrayItem(changes, 0);
text = cJSON_GetObjectItem(change, "text");
}
}

if (uri && uri->valuestring && text && text->valuestring)
{
lsp_check_file(uri->valuestring, text->valuestring, id);
}
}
}
handle_initialize(json, id_item);
}
else if (strcmp(method, "textDocument/didOpen") == 0)
{
handle_did_open(json);
}
else if (strcmp(method, "textDocument/didChange") == 0)
{
handle_did_change(json);
}
else if (strcmp(method, "textDocument/definition") == 0)
{
char *uri = NULL;
Expand Down Expand Up @@ -212,6 +356,18 @@ void handle_request(const char *json_str)
free(uri);
}
}
else if (strcmp(method, "shutdown") == 0)
{
handle_shutdown(id_item);
}
else if (strcmp(method, "exit") == 0)
{
handle_exit();
}
else
{
method_not_found(id_item);
}

cJSON_Delete(json);
}
Loading
Loading