diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 7 | ||||
| -rw-r--r-- | src/config.c | 6 | ||||
| -rw-r--r-- | src/ecex.c | 473 | ||||
| -rw-r--r-- | src/eval.c | 88 | ||||
| -rw-r--r-- | src/path.c | 86 |
5 files changed, 646 insertions, 14 deletions
@@ -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), @@ -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) { @@ -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); @@ -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); |
