aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/app.c7
-rw-r--r--src/config.c6
-rw-r--r--src/ecex.c473
-rw-r--r--src/eval.c88
-rw-r--r--src/path.c86
5 files changed, 646 insertions, 14 deletions
diff --git a/src/app.c b/src/app.c
index b9840de..9e44e72 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1691,7 +1691,12 @@ static void key_callback(GLFWwindow *window,
if ((mods & GLFW_MOD_CONTROL) &&
!(mods & (GLFW_MOD_ALT | GLFW_MOD_SHIFT | GLFW_MOD_SUPER)) &&
is_layout_char_key(key, scancode, 'g')) {
- if (app->mode == APP_MODE_MX) {
+ if (app->mode == APP_MODE_EDIT && key_sequence_has_prefix(app, "C-g")) {
+ if (key_produces_text(key, scancode)) {
+ app->suppress_next_char = 1;
+ }
+ app_enter_prefix(app, "C-g");
+ } else if (app->mode == APP_MODE_MX) {
app_cancel_mx(app);
} else if (app->mode == APP_MODE_PROMPT) {
app_cancel_prompt(app);
diff --git a/src/config.c b/src/config.c
index 6e4b84f..0e0a2ab 100644
--- a/src/config.c
+++ b/src/config.c
@@ -202,6 +202,8 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_draw_tetris_preview_i),
HOST_SYMBOL(ecex_draw_rgba),
HOST_SYMBOL(ecex_find_file),
+ HOST_SYMBOL(ecex_find_file_at),
+ HOST_SYMBOL(ecex_find_project_file),
HOST_SYMBOL(ecex_save_current_buffer),
HOST_SYMBOL(ecex_write_current_buffer),
HOST_SYMBOL(ecex_compile),
@@ -211,6 +213,9 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_next_interactive_action),
HOST_SYMBOL(ecex_previous_interactive_action),
HOST_SYMBOL(ecex_indent_line_to),
+ HOST_SYMBOL(ecex_clangd_jump_to_definition),
+ HOST_SYMBOL(ecex_c_mode_set_tab_width),
+ HOST_SYMBOL(ecex_c_mode_tab_width),
HOST_SYMBOL(ecex_comment_region),
HOST_SYMBOL(ecex_uncomment_region),
HOST_SYMBOL(ecex_request_prompt),
@@ -223,6 +228,7 @@ static const host_symbol_t host_symbols[] = {
HOST_SYMBOL(ecex_path_dirname),
HOST_SYMBOL(ecex_path_basename_dup),
HOST_SYMBOL(ecex_path_normalize),
+ HOST_SYMBOL(ecex_project_root_for_file),
HOST_SYMBOL(ecex_path_is_dir),
HOST_SYMBOL(ecex_path_is_file),
HOST_SYMBOL(ecex_path_exists),
diff --git a/src/ecex.c b/src/ecex.c
index f3b4980..055bd76 100644
--- a/src/ecex.c
+++ b/src/ecex.c
@@ -7,6 +7,7 @@
#include "util.h"
#include "path.h"
+#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
@@ -36,6 +37,11 @@ extern int kill(pid_t pid, int sig);
#define ECEX_MESSAGES_BUFFER_NAME "*Messages*"
#define ECEX_MESSAGES_MAX_BYTES (1024u * 1024u)
#define ECEX_MESSAGES_TRIM_BYTES (256u * 1024u)
+#define ECEX_C_MODE_TAB_WIDTH_DEFAULT 4
+#define ECEX_C_MODE_TAB_WIDTH_MIN 1
+#define ECEX_C_MODE_TAB_WIDTH_MAX 16
+
+static int ecex_c_mode_tab_width_value = ECEX_C_MODE_TAB_WIDTH_DEFAULT;
ecex_window_t *ecex_current_window(ecex_t *ed);
static void ecex_clear_command_hooks(ecex_t *ed);
@@ -75,6 +81,16 @@ void ecex_mem_zero(void *ptr, size_t size) {
memset(ptr, 0, size);
}
+int ecex_c_mode_set_tab_width(int spaces) {
+ if (spaces < ECEX_C_MODE_TAB_WIDTH_MIN) spaces = ECEX_C_MODE_TAB_WIDTH_MIN;
+ if (spaces > ECEX_C_MODE_TAB_WIDTH_MAX) spaces = ECEX_C_MODE_TAB_WIDTH_MAX;
+ ecex_c_mode_tab_width_value = spaces;
+ return ecex_c_mode_tab_width_value;
+}
+
+int ecex_c_mode_tab_width(void) {
+ return ecex_c_mode_tab_width_value;
+}
int ecex_i32_get(const int *items, size_t index) {
if (!items) return 0;
@@ -1054,6 +1070,10 @@ void ecex_free(ecex_t *ed) {
ccdjit_module_free((ccdjit_module *)ed->jit_modules[i]);
}
+ for (size_t i = 0; i < ed->eval_module_count; i++) {
+ free(ed->eval_modules[i].key);
+ }
+
ecex_plugin_runtime_free(ed->plugins);
ed->plugins = NULL;
@@ -1086,6 +1106,7 @@ void ecex_free(ecex_t *ed) {
free(ed->buffer_hooks);
free(ed->completion_providers);
free(ed->major_modes);
+ free(ed->eval_modules);
free(ed->last_eval_source);
free(ed->last_eval_filename);
free(ed->last_compile_command);
@@ -3224,6 +3245,187 @@ static int ecex_buffer_path_equal(buffer_t *buffer, const char *path) {
return buffer && buffer->path && path && strcmp(buffer->path, path) == 0;
}
+static char *ecex_project_source_dir(const char *from_file) {
+ if (from_file && from_file[0]) {
+ if (ecex_path_is_dir(from_file)) return ecex_path_normalize(from_file);
+
+ char *dir = ecex_path_dirname(from_file);
+ if (!dir) return NULL;
+ char *normal = ecex_path_normalize(dir);
+ free(dir);
+ return normal;
+ }
+
+ char cwd[4096];
+ if (ecex_path_cwd(cwd, sizeof(cwd)) != 0) return ecex_strdup(".");
+ return ecex_path_normalize(cwd);
+}
+
+static char *ecex_project_existing_candidate(const char *base_dir, const char *path) {
+ if (!path || !path[0]) return NULL;
+
+ char *candidate = NULL;
+ if (path[0] == '/' || path[0] == '~') candidate = ecex_path_expand_user(path);
+ else candidate = ecex_path_join(base_dir, path);
+
+ if (candidate && ecex_path_exists(candidate)) return candidate;
+ free(candidate);
+ return NULL;
+}
+
+typedef struct ecex_project_file_resolver {
+ const char *root;
+ const char *path;
+ char *found;
+} ecex_project_file_resolver_t;
+
+static int ecex_project_try_include_dir(const char *dir, ecex_project_file_resolver_t *resolver) {
+ if (!dir || !dir[0] || !resolver || resolver->found) return resolver && resolver->found;
+
+ char *base = NULL;
+ if (dir[0] == '/' || dir[0] == '~') base = ecex_path_expand_user(dir);
+ else base = ecex_path_join(resolver->root, dir);
+ if (!base) return 0;
+
+ resolver->found = ecex_project_existing_candidate(base, resolver->path);
+ free(base);
+ return resolver->found != NULL;
+}
+
+static int ecex_project_flag_delim(int c) {
+ return isspace((unsigned char)c) ||
+ c == '"' || c == '\'' ||
+ c == ',' || c == ':' ||
+ c == '[' || c == ']' ||
+ c == '{' || c == '}';
+}
+
+static const char *ecex_project_next_flag_token(const char *p, char *out, size_t out_size) {
+ if (!p || !out || out_size == 0) return NULL;
+
+ size_t len = 0;
+ int in_token = 0;
+ while (*p) {
+ unsigned char c = (unsigned char)*p;
+
+ if (!in_token && c == '#') {
+ while (*p && *p != '\n') p++;
+ continue;
+ }
+
+ if (c == '\\' && p[1]) {
+ p++;
+ c = (unsigned char)*p;
+ } else if (ecex_project_flag_delim(c)) {
+ if (in_token) break;
+ p++;
+ continue;
+ }
+
+ in_token = 1;
+ if (len + 1 < out_size) out[len++] = (char)c;
+ p++;
+ }
+
+ out[len] = '\0';
+ return p;
+}
+
+static const char *ecex_project_inline_include_value(const char *arg) {
+ if (!arg) return NULL;
+ if (strncmp(arg, "-I", 2) == 0 && arg[2]) return arg[2] == '=' ? arg + 3 : arg + 2;
+ if (strncmp(arg, "-iquote", 7) == 0 && arg[7]) return arg[7] == '=' ? arg + 8 : arg + 7;
+ if (strncmp(arg, "-isystem", 8) == 0 && arg[8]) return arg[8] == '=' ? arg + 9 : arg + 8;
+ if (strncmp(arg, "-idirafter", 10) == 0 && arg[10]) return arg[10] == '=' ? arg + 11 : arg + 10;
+ if (strncmp(arg, "--include-directory=", 20) == 0) return arg + 20;
+ if (strncmp(arg, "include=", 8) == 0) return arg + 8;
+ return NULL;
+}
+
+static int ecex_project_include_needs_value(const char *arg) {
+ return arg &&
+ (strcmp(arg, "-I") == 0 ||
+ strcmp(arg, "-iquote") == 0 ||
+ strcmp(arg, "-isystem") == 0 ||
+ strcmp(arg, "-idirafter") == 0 ||
+ strcmp(arg, "--include-directory") == 0 ||
+ strcmp(arg, "include") == 0);
+}
+
+static int ecex_project_scan_include_flags(const char *text, ecex_project_file_resolver_t *resolver) {
+ if (!text || !resolver) return 0;
+
+ int pending_include = 0;
+ const char *p = text;
+ char token[1024];
+ while ((p = ecex_project_next_flag_token(p, token, sizeof(token))) && token[0]) {
+ if (pending_include) {
+ if (strcmp(token, "=") == 0) continue;
+ pending_include = 0;
+ if (token[0] != '-' && ecex_project_try_include_dir(token, resolver)) return 1;
+ }
+
+ const char *inline_value = ecex_project_inline_include_value(token);
+ if (inline_value && inline_value[0]) {
+ if (ecex_project_try_include_dir(inline_value, resolver)) return 1;
+ } else if (ecex_project_include_needs_value(token)) {
+ pending_include = 1;
+ }
+ }
+
+ return 0;
+}
+
+static int ecex_project_scan_include_file(const char *root,
+ const char *name,
+ ecex_project_file_resolver_t *resolver) {
+ char *path = ecex_path_join(root, name);
+ if (!path) return 0;
+
+ char *text = ecex_read_entire_file(path, NULL);
+ free(path);
+ if (!text) return 0;
+
+ int found = ecex_project_scan_include_flags(text, resolver);
+ free(text);
+ return found;
+}
+
+static char *ecex_project_resolve_file_path(const char *from_file, const char *path) {
+ if (!path || !path[0]) return NULL;
+
+ if (path[0] == '/' || path[0] == '~') {
+ return ecex_project_existing_candidate(NULL, path);
+ }
+
+ char *source_dir = ecex_project_source_dir(from_file);
+ char *root = ecex_project_root_for_file(from_file && from_file[0] ? from_file : source_dir);
+
+ char *target = NULL;
+ if (source_dir) target = ecex_project_existing_candidate(source_dir, path);
+
+ if (!target && root) {
+ ecex_project_file_resolver_t resolver = {
+ .root = root,
+ .path = path,
+ .found = NULL,
+ };
+
+ ecex_project_scan_include_file(root, ".ecex-project", &resolver);
+ if (!resolver.found) ecex_project_scan_include_file(root, "compile_flags.txt", &resolver);
+ if (!resolver.found) ecex_project_scan_include_file(root, "compile_commands.json", &resolver);
+ target = resolver.found;
+ }
+
+ if (!target && root && (!source_dir || strcmp(root, source_dir) != 0)) {
+ target = ecex_project_existing_candidate(root, path);
+ }
+
+ free(source_dir);
+ free(root);
+ return target;
+}
+
int ecex_find_file(ecex_t *ed, const char *path) {
if (!ed || !path || !path[0]) return ECEX_ERR;
@@ -3278,6 +3480,47 @@ int ecex_find_file(ecex_t *ed, const char *path) {
return switch_result;
}
+int ecex_find_project_file(ecex_t *ed, const char *from_file, const char *path) {
+ if (!ed || !path || !path[0]) return ECEX_ERR;
+
+ char *target = ecex_project_resolve_file_path(from_file, path);
+ if (!target) {
+ char message[1200];
+ snprintf(message, sizeof(message), "File not found in project: %s", path);
+ ecex_message(ed, message);
+ return ECEX_OK;
+ }
+
+ int result = ecex_find_file(ed, target);
+ free(target);
+ return result;
+}
+
+int ecex_find_file_at(ecex_t *ed, const char *path, size_t line, size_t column) {
+ if (!ed || !path || !path[0]) return ECEX_ERR;
+ if (line == 0) line = 1;
+ if (column == 0) column = 1;
+
+ if (ecex_find_file(ed, path) != ECEX_OK) return ECEX_ERR;
+
+ buffer_t *buf = ecex_current_buffer(ed);
+ if (!buf) return ECEX_ERR;
+
+ size_t pos = 0;
+ for (size_t current_line = 1; current_line < line && pos < buf->len; pos++) {
+ if (buf->data[pos] == '\n') current_line++;
+ }
+
+ size_t line_end = buffer_line_end_at(buf, pos);
+ size_t target = pos;
+ for (size_t current_col = 1; current_col < column && target < line_end; current_col++) {
+ target++;
+ }
+
+ buffer_set_point(buf, target);
+ return ECEX_OK;
+}
+
int ecex_save_current_buffer(ecex_t *ed) {
buffer_t *buf = ecex_current_buffer(ed);
if (!buf) return ECEX_ERR;
@@ -3905,6 +4148,34 @@ static int ecex_json_string_between(const char *start,
return 0;
}
+static int ecex_json_int_between(const char *start,
+ const char *end,
+ const char *key,
+ int *out) {
+ if (!start || !end || !key || !out || start >= end) return 0;
+
+ char pattern[64];
+ snprintf(pattern, sizeof(pattern), "\"%s\"", key);
+ const char *p = start;
+ while ((p = strstr(p, pattern)) != NULL && p < end) {
+ p += strlen(pattern);
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++;
+ if (p >= end || *p != ':') continue;
+ p++;
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) p++;
+ if (p >= end) continue;
+
+ char *next = NULL;
+ long value = strtol(p, &next, 10);
+ if (next != p) {
+ *out = (int)value;
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
static void ecex_trim_in_place(char *text) {
if (!text) return;
char *start = text;
@@ -4023,7 +4294,7 @@ static int ecex_clangd_collect_candidates(buffer_t *buffer,
if (!buffer || !buffer->path || !buffer->path[0] || !prefix || !items || !count) return ECEX_ERR;
char *uri = ecex_lsp_file_uri(buffer->path);
- char *root_path = ecex_path_dirname(buffer->path);
+ char *root_path = ecex_project_root_for_file(buffer->path);
char *root_uri = root_path ? ecex_lsp_file_uri(root_path) : NULL;
char *escaped_uri = ecex_json_escape(uri);
char *escaped_root_uri = ecex_json_escape(root_uri ? root_uri : uri);
@@ -4110,6 +4381,196 @@ static int ecex_clangd_collect_candidates(buffer_t *buffer,
return result;
}
+static char *ecex_lsp_file_path_from_uri(const char *uri) {
+ if (!uri || strncmp(uri, "file://", 7) != 0) return NULL;
+
+ const char *p = uri + 7;
+ size_t len = strlen(p);
+ char *out = malloc(len + 1);
+ if (!out) return NULL;
+
+ size_t used = 0;
+ while (*p) {
+ if (*p == '%' && p[1] && p[2]) {
+ int hi = ecex_json_hex_value(p[1]);
+ int lo = ecex_json_hex_value(p[2]);
+ if (hi >= 0 && lo >= 0) {
+ out[used++] = (char)((hi << 4) | lo);
+ p += 3;
+ continue;
+ }
+ }
+ out[used++] = *p++;
+ }
+
+ out[used] = '\0';
+ return out;
+}
+
+static int ecex_lsp_definition_location(const char *json,
+ char *uri,
+ size_t uri_size,
+ int *out_line,
+ int *out_character) {
+ if (uri && uri_size) uri[0] = '\0';
+ if (!json || !uri || uri_size == 0 || !out_line || !out_character) return ECEX_ERR;
+ if (strstr(json, "\"result\":null")) return ECEX_ERR;
+
+ const char *json_end = json + strlen(json);
+ const char *loc = strstr(json, "\"targetUri\"");
+ const char *uri_key = "targetUri";
+ if (!loc) {
+ loc = strstr(json, "\"uri\"");
+ uri_key = "uri";
+ }
+ if (!loc) return ECEX_ERR;
+
+ if (!ecex_json_string_between(loc, json_end, uri_key, uri, uri_size)) return ECEX_ERR;
+
+ const char *range = strstr(loc, "\"targetSelectionRange\"");
+ if (!range) range = strstr(loc, "\"targetRange\"");
+ if (!range) range = strstr(loc, "\"range\"");
+ if (!range) range = loc;
+
+ const char *start = strstr(range, "\"start\"");
+ if (!start) start = range;
+
+ int line = -1;
+ int character = 0;
+ if (!ecex_json_int_between(start, json_end, "line", &line) || line < 0) return ECEX_ERR;
+ if (!ecex_json_int_between(start, json_end, "character", &character) || character < 0) {
+ character = 0;
+ }
+
+ *out_line = line;
+ *out_character = character;
+ return ECEX_OK;
+}
+
+int ecex_clangd_jump_to_definition(ecex_t *ed) {
+ if (!ed) return ECEX_ERR;
+
+ buffer_t *buffer = ecex_current_buffer(ed);
+ if (!buffer || !buffer->path || !buffer->path[0]) {
+ ecex_message(ed, "Definition needs a file-backed buffer");
+ return ECEX_OK;
+ }
+
+ if (!ecex_dependency_available("clangd")) {
+ ecex_message(ed, "clangd not found");
+ return ECEX_OK;
+ }
+
+ char *uri = ecex_lsp_file_uri(buffer->path);
+ char *root_path = ecex_project_root_for_file(buffer->path);
+ char *root_uri = root_path ? ecex_lsp_file_uri(root_path) : NULL;
+ char *escaped_uri = ecex_json_escape(uri);
+ char *escaped_root_uri = ecex_json_escape(root_uri ? root_uri : uri);
+ char *escaped_text = ecex_json_escape_len(buffer->data ? buffer->data : "", buffer->len);
+ if (!uri || !escaped_uri || !escaped_root_uri || !escaped_text) {
+ free(uri);
+ free(root_path);
+ free(root_uri);
+ free(escaped_uri);
+ free(escaped_root_uri);
+ free(escaped_text);
+ ecex_message(ed, "Could not prepare definition request");
+ return ECEX_OK;
+ }
+
+ int line = 0;
+ int character = 0;
+ ecex_buffer_lsp_position(buffer, &line, &character);
+
+ const char *language_id = ecex_lsp_language_id(buffer->path);
+ char *initialize = ecex_lsp_format(
+ "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\","
+ "\"params\":{\"processId\":null,\"rootUri\":\"%s\","
+ "\"capabilities\":{\"textDocument\":{\"definition\":{\"dynamicRegistration\":false}}},"
+ "\"clientInfo\":{\"name\":\"ecex\",\"version\":\"0\"}}}",
+ escaped_root_uri);
+ char *initialized = ecex_lsp_format(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}");
+ char *did_open = ecex_lsp_format(
+ "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\","
+ "\"params\":{\"textDocument\":{\"uri\":\"%s\",\"languageId\":\"%s\","
+ "\"version\":1,\"text\":\"%s\"}}}",
+ escaped_uri,
+ language_id,
+ escaped_text);
+ char *definition = ecex_lsp_format(
+ "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"textDocument/definition\","
+ "\"params\":{\"textDocument\":{\"uri\":\"%s\"},"
+ "\"position\":{\"line\":%d,\"character\":%d}}}",
+ escaped_uri,
+ line,
+ character);
+
+ free(uri);
+ free(root_path);
+ free(root_uri);
+ free(escaped_uri);
+ free(escaped_root_uri);
+ free(escaped_text);
+
+ if (!initialize || !initialized || !did_open || !definition) {
+ free(initialize);
+ free(initialized);
+ free(did_open);
+ free(definition);
+ ecex_message(ed, "Could not build definition request");
+ return ECEX_OK;
+ }
+
+ int result = ECEX_ERR;
+ char *response = NULL;
+ ecex_lsp_process_t proc;
+ if (ecex_lsp_start_clangd(&proc) == ECEX_OK) {
+ if (ecex_lsp_send(proc.in_fd, initialize) == ECEX_OK &&
+ ecex_lsp_read_response(proc.out_fd, 1, &response) == ECEX_OK) {
+ free(response);
+ response = NULL;
+ if (ecex_lsp_send(proc.in_fd, initialized) == ECEX_OK &&
+ ecex_lsp_send(proc.in_fd, did_open) == ECEX_OK &&
+ ecex_lsp_send(proc.in_fd, definition) == ECEX_OK &&
+ ecex_lsp_read_response(proc.out_fd, 2, &response) == ECEX_OK) {
+ char location_uri[2048];
+ int target_line = 0;
+ int target_character = 0;
+ if (ecex_lsp_definition_location(response,
+ location_uri,
+ sizeof(location_uri),
+ &target_line,
+ &target_character) == ECEX_OK) {
+ char *target_path = ecex_lsp_file_path_from_uri(location_uri);
+ if (target_path) {
+ result = ecex_find_file_at(ed,
+ target_path,
+ (size_t)target_line + 1,
+ (size_t)target_character + 1);
+ free(target_path);
+ }
+ }
+ }
+ }
+ ecex_lsp_finish(&proc);
+ }
+
+ free(response);
+ free(initialize);
+ free(initialized);
+ free(did_open);
+ free(definition);
+
+ if (result != ECEX_OK) {
+ ecex_message(ed, "Definition not found");
+ return ECEX_OK;
+ }
+
+ ecex_message(ed, "Jumped to definition");
+ return ECEX_OK;
+}
+
static int ecex_clangd_completion_provider(ecex_t *ed,
buffer_t *buffer,
const char *prefix,
@@ -4466,15 +4927,7 @@ static int ecex_goto_file_line_action(ecex_t *ed, buffer_t *buffer, size_t line,
char path[1024];
size_t target_line = 1;
if (ecex_parse_file_line(payload, path, sizeof(path), &target_line) != ECEX_OK) return ECEX_ERR;
- if (ecex_find_file(ed, path) != ECEX_OK) return ECEX_ERR;
- buffer_t *buf = ecex_current_buffer(ed);
- if (!buf) return ECEX_ERR;
- size_t pos = 0;
- for (size_t l = 1; l < target_line && pos < buf->len; pos++) {
- if (buf->data[pos] == '\n') l++;
- }
- buffer_set_point(buf, pos);
- return ECEX_OK;
+ return ecex_find_file_at(ed, path, target_line, 1);
}
static int ecex_append_shell_output(ecex_t *ed, const char *name, const char *command) {
diff --git a/src/eval.c b/src/eval.c
index 34259eb..2bed8a8 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -446,6 +446,64 @@ static int remember_last_eval(ecex_t *ed,
return ECEX_OK;
}
+static char *eval_module_key(const char *filename, int wrap_as_statements) {
+ const char *name = (filename && filename[0]) ? filename : "<eval>";
+ int needed = snprintf(NULL, 0, "%d:%s", wrap_as_statements ? 1 : 0, name);
+ if (needed < 0) return NULL;
+
+ char *key = malloc((size_t)needed + 1);
+ if (!key) return NULL;
+
+ snprintf(key, (size_t)needed + 1, "%d:%s", wrap_as_statements ? 1 : 0, name);
+ return key;
+}
+
+static void eval_remove_kept_module_ref(ecex_t *ed, void *module) {
+ if (!ed || !module) return;
+
+ for (size_t i = 0; i < ed->jit_module_count; i++) {
+ if (ed->jit_modules[i] != module) continue;
+
+ if (i + 1 < ed->jit_module_count) {
+ memmove(&ed->jit_modules[i],
+ &ed->jit_modules[i + 1],
+ (ed->jit_module_count - i - 1) * sizeof(ed->jit_modules[i]));
+ }
+ ed->jit_module_count--;
+ return;
+ }
+}
+
+static int eval_remember_kept_module(ecex_t *ed, char *key, ccdjit_module *module) {
+ if (!ed || !key || !module) return ECEX_ERR;
+
+ for (size_t i = 0; i < ed->eval_module_count; i++) {
+ if (!ed->eval_modules[i].key || strcmp(ed->eval_modules[i].key, key) != 0) continue;
+
+ ccdjit_module *old = (ccdjit_module *)ed->eval_modules[i].module;
+ ed->eval_modules[i].module = module;
+ free(key);
+
+ if (old && old != module) {
+ eval_remove_kept_module_ref(ed, old);
+ ccdjit_module_free(old);
+ }
+ return ECEX_OK;
+ }
+
+ if (ECEX_GROW_ARRAY(ed->eval_modules,
+ ed->eval_module_count,
+ ed->eval_module_cap,
+ 8) != ECEX_OK) {
+ return ECEX_ERR;
+ }
+
+ ed->eval_modules[ed->eval_module_count].key = key;
+ ed->eval_modules[ed->eval_module_count].module = module;
+ ed->eval_module_count++;
+ return ECEX_OK;
+}
+
int ecex_eval_source(ecex_t *ed,
const char *source,
const char *filename,
@@ -573,12 +631,36 @@ int ecex_eval_source(ecex_t *ed,
buffer_append(out, line);
/*
- * Keep successful eval modules alive. This makes eval useful for live
- * customization: code evaluated from a buffer may register commands whose
- * function pointers remain valid after eval returns.
+ * Keep the latest successful eval module for each source key alive. A
+ * rerun of the same buffer replaces the old module after the new one has
+ * run, so normal eval-output `g` loops do not accumulate generated code.
*/
+ char *module_key = eval_module_key(filename, wrap_as_statements);
+ if (!module_key) {
+ buffer_append(out, "failed to allocate eval module key\n");
+ ccdjit_module_free(module);
+ g_eval_editor = NULL;
+ ccdjit_context_free(ctx);
+ free(eval_source);
+ ecex_switch_buffer(ed, "*eval-output*");
+ return ECEX_ERR;
+ }
+
if (ecex_keep_jit_module(ed, module) != ECEX_OK) {
buffer_append(out, "failed to keep eval module alive\n");
+ free(module_key);
+ ccdjit_module_free(module);
+ g_eval_editor = NULL;
+ ccdjit_context_free(ctx);
+ free(eval_source);
+ ecex_switch_buffer(ed, "*eval-output*");
+ return ECEX_ERR;
+ }
+
+ if (eval_remember_kept_module(ed, module_key, module) != ECEX_OK) {
+ buffer_append(out, "failed to track eval module\n");
+ free(module_key);
+ eval_remove_kept_module_ref(ed, module);
ccdjit_module_free(module);
g_eval_editor = NULL;
ccdjit_context_free(ctx);
diff --git a/src/path.c b/src/path.c
index be2b347..930fbb7 100644
--- a/src/path.c
+++ b/src/path.c
@@ -114,6 +114,92 @@ char *ecex_path_normalize(const char *path) {
return joined;
}
+static int ecex_project_root_has_marker(const char *dir) {
+ static const char *const markers[] = {
+ ".git",
+ "compile_commands.json",
+ "compile_flags.txt",
+ ".ecex-project",
+ };
+
+ for (size_t i = 0; i < sizeof(markers) / sizeof(markers[0]); i++) {
+ char *path = ecex_path_join(dir, markers[i]);
+ if (!path) continue;
+ int found = ecex_path_exists(path);
+ free(path);
+ if (found) return 1;
+ }
+
+ return 0;
+}
+
+static char *ecex_path_parent_dup(const char *path) {
+ if (!path || !path[0]) return NULL;
+
+ char *parent = ecex_strdup(path);
+ if (!parent) return NULL;
+
+ size_t len = strlen(parent);
+ while (len > 1 && parent[len - 1] == '/') parent[--len] = '\0';
+ if (strcmp(parent, "/") == 0) return parent;
+
+ char *slash = strrchr(parent, '/');
+ if (!slash) {
+ free(parent);
+ return NULL;
+ }
+
+ if (slash == parent) parent[1] = '\0';
+ else *slash = '\0';
+ return parent;
+}
+
+char *ecex_project_root_for_file(const char *path) {
+ char *start = NULL;
+
+ if (path && path[0]) {
+ if (ecex_path_is_dir(path)) {
+ start = ecex_path_normalize(path);
+ } else {
+ char *dir = ecex_path_dirname(path);
+ if (dir) {
+ start = ecex_path_normalize(dir);
+ free(dir);
+ }
+ }
+ } else {
+ char cwd[4096];
+ if (ecex_path_cwd(cwd, sizeof(cwd)) == 0) start = ecex_path_normalize(cwd);
+ }
+
+ if (!start) return NULL;
+ char *fallback = ecex_strdup(start);
+ if (!fallback) {
+ free(start);
+ return NULL;
+ }
+
+ char *dir = start;
+ while (dir && dir[0]) {
+ if (ecex_project_root_has_marker(dir)) {
+ free(fallback);
+ return dir;
+ }
+
+ char *parent = ecex_path_parent_dup(dir);
+ if (!parent || strcmp(parent, dir) == 0) {
+ free(parent);
+ break;
+ }
+
+ free(dir);
+ dir = parent;
+ }
+
+ free(dir);
+ return fallback;
+}
+
int ecex_path_exists(const char *path) {
if (!path) return 0;
char *expanded = ecex_path_expand_user(path);