diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | README.md | 15 | ||||
| -rw-r--r-- | config/c_mode_plugin.c | 170 | ||||
| -rw-r--r-- | config/ecex_api_completion_plugin.c | 30 | ||||
| -rw-r--r-- | config/ecexrc.c | 5 | ||||
| -rw-r--r-- | config/tetris.c | 9 | ||||
| -rw-r--r-- | docs/plugin-api.md | 1 | ||||
| -rw-r--r-- | guix.scm | 1 | ||||
| -rw-r--r-- | include/ecex.h | 5 | ||||
| -rw-r--r-- | include/path.h | 1 | ||||
| -rw-r--r-- | include/types.h | 8 | ||||
| -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 | ||||
| -rw-r--r-- | tests/test_completion_path.c | 35 |
17 files changed, 910 insertions, 31 deletions
@@ -1,6 +1,5 @@ bin/ crash.dump -compile_flags.txt *.o *.so *.a @@ -25,6 +25,7 @@ With Guix: ```sh guix shell -m manifest.scm -- make check guix build -f guix.scm +guix package -f guix.scm ``` Run: @@ -48,6 +49,8 @@ make install PREFIX=$HOME/.local `ECEX_FONT` can also point at a TTF font. `ECEX_INCLUDE` can point at the Ecex include directory when loading configs from another working directory. +If no `--config` is provided, Ecex looks for `ecexrc.c` in +`$XDG_CONFIG_HOME/ecex` and then `~/.config/ecex`. ## Useful Keys @@ -58,6 +61,8 @@ include directory when loading configs from another working directory. - `C-x k`: kill buffer - `C-x d`: file browser - `M-x messages`: open the read-only `*Messages*` buffer +- `C-g d` in C buffers: jump to definition from the project root +- `C-g f` in C buffers: open file at point using project include flags - `C-s` / `C-r`: incremental search - `C-/`: undo - `C-S-z`: redo @@ -77,6 +82,14 @@ follow-up work is tracked in `docs/ccdjit-improvements.md`. `guix.scm` defines a local package. It installs the binary, public headers, sample configs, and docs, and wraps `ecex` with `ECEX_INCLUDE` and a DejaVu font -path. +path. It also exports Guix profile search paths so `#include "ecex.h"` is +discoverable by Clang after the profile environment is sourced. + +In C-mode, `ENTER` auto-indents, `S-TAB` removes one indent level, and +`ecex_c_mode_set_tab_width(2)` changes the tab width from config. +For `C-g d`/`C-g f`, ecex finds the project root by walking up to `.git`, +`compile_commands.json`, `compile_flags.txt`, or `.ecex-project`. Clangd uses +standard `compile_commands.json`/`compile_flags.txt`; `C-g f` also reads include +flags such as `-Iinclude` from `.ecex-project`. Ecex is distributed under the BSD 2-Clause License. See `LICENSE`. diff --git a/config/c_mode_plugin.c b/config/c_mode_plugin.c index e707bca..410f029 100644 --- a/config/c_mode_plugin.c +++ b/config/c_mode_plugin.c @@ -1,10 +1,17 @@ #include "ecex.h" +#include <stdio.h> #include <string.h> #define C_MODE_PLUGIN_ID "c-mode" #define C_MODE_LSP_PROVIDER_NAME "c-mode-clangd" #define C_MODE_SLOT_CLANGD "clangd" +#define C_MODE_TAB_WIDTH_DEFAULT 4 + +static int c_mode_indent_width(void) { + int width = ecex_c_mode_tab_width(); + return width > 0 ? width : C_MODE_TAB_WIDTH_DEFAULT; +} static int c_mode_line_starts_with(buffer_t *buffer, size_t first, size_t line_end, const char *word) { size_t len = strlen(word); @@ -108,6 +115,24 @@ static int c_mode_brace_depth_before(buffer_t *buffer, size_t limit) { return depth; } +static int c_mode_line_indent_cols(buffer_t *buffer, size_t line_start, size_t line_end) { + int cols = 0; + if (!buffer) return 0; + + for (size_t i = line_start; i < line_end; i++) { + if (buffer->data[i] == ' ') { + cols++; + } else if (buffer->data[i] == '\t') { + int width = c_mode_indent_width(); + cols += width - (cols % width); + } else { + break; + } + } + + return cols; +} + static int cmd_c_indent_line(ecex_t *ed) { if (!ed) return -1; buffer_t *buffer = ecex_current_buffer(ed); @@ -120,10 +145,11 @@ static int cmd_c_indent_line(ecex_t *ed) { if (first < line_end && buffer->data[first] == '#') return ecex_indent_line_to(buffer, 0); + int width = c_mode_indent_width(); int depth = c_mode_brace_depth_before(buffer, line_start); - int target = depth * 4; + int target = depth * width; - if (first < line_end && buffer->data[first] == '}') target -= 4; + if (first < line_end && buffer->data[first] == '}') target -= width; size_t prev = c_mode_previous_nonblank_line(buffer, line_start); if (prev != (size_t)-1) { @@ -137,7 +163,7 @@ static int cmd_c_indent_line(ecex_t *ed) { !(c_mode_line_starts_with(buffer, first, line_end, "case") || c_mode_line_starts_with(buffer, first, line_end, "default") || (first < line_end && buffer->data[first] == '}'))) { - target += 4; + target += width; } } @@ -145,10 +171,137 @@ static int cmd_c_indent_line(ecex_t *ed) { return ecex_indent_line_to(buffer, target); } +static int cmd_c_newline_and_indent(ecex_t *ed) { + if (!ed) return -1; + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return -1; + if (buffer_insert_char(buffer, '\n') != 0) return -1; + return cmd_c_indent_line(ed); +} + +static int cmd_c_dedent_line(ecex_t *ed) { + if (!ed) return -1; + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer || buffer->read_only || buffer_is_interactive(buffer)) return -1; + + size_t line_start = buffer_current_line_start(buffer); + size_t line_end = buffer_current_line_end(buffer); + int current = c_mode_line_indent_cols(buffer, line_start, line_end); + int target = current - c_mode_indent_width(); + if (target < 0) target = 0; + return ecex_indent_line_to(buffer, target); +} + static int cmd_c_complete(ecex_t *ed) { return ecex_complete_at_point(ed); } +static int c_mode_set_tab_width_command(ecex_t *ed, int width) { + int actual = ecex_c_mode_set_tab_width(width); + char message[64]; + snprintf(message, sizeof(message), "c-mode tab width: %d", actual); + ecex_message(ed, message); + return 0; +} + +static int cmd_c_tab_width_2(ecex_t *ed) { return c_mode_set_tab_width_command(ed, 2); } +static int cmd_c_tab_width_4(ecex_t *ed) { return c_mode_set_tab_width_command(ed, 4); } +static int cmd_c_tab_width_8(ecex_t *ed) { return c_mode_set_tab_width_command(ed, 8); } + +static int c_mode_path_char(char c) { + return (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || + c == '_' || c == '-' || c == '.' || c == '/' || c == '~' || c == '+'; +} + +static int c_mode_copy_range(buffer_t *buffer, size_t start, size_t end, char *out, size_t out_size) { + if (!buffer || !out || out_size == 0 || start > end || end > buffer->len) return -1; + size_t len = end - start; + if (len == 0) return -1; + if (len >= out_size) len = out_size - 1; + memcpy(out, buffer->data + start, len); + out[len] = '\0'; + return 0; +} + +static int c_mode_line_is_include(buffer_t *buffer, size_t line_start, size_t line_end) { + if (!buffer) return 0; + size_t p = line_start; + while (p < line_end && (buffer->data[p] == ' ' || buffer->data[p] == '\t')) p++; + return p + 8 <= line_end && strncmp(buffer->data + p, "#include", 8) == 0; +} + +static int c_mode_quoted_path_at_point(buffer_t *buffer, + size_t line_start, + size_t line_end, + size_t point, + char *out, + size_t out_size) { + int include_line = c_mode_line_is_include(buffer, line_start, line_end); + + for (size_t p = line_start; p < line_end; p++) { + char open = buffer->data[p]; + char close = '\0'; + if (open == '"') close = '"'; + else if (include_line && open == '<') close = '>'; + else continue; + + size_t q = p + 1; + while (q < line_end && buffer->data[q] != close) q++; + if (q >= line_end) return -1; + if (point > p && point <= q) return c_mode_copy_range(buffer, p + 1, q, out, out_size); + p = q; + } + + return -1; +} + +static int c_mode_path_at_point(buffer_t *buffer, char *out, size_t out_size) { + if (out && out_size) out[0] = '\0'; + if (!buffer || !out || out_size == 0 || !buffer->data) return -1; + + size_t point = buffer->point > buffer->len ? buffer->len : buffer->point; + size_t line_start = buffer_line_start_at(buffer, point); + size_t line_end = buffer_line_end_at(buffer, point); + + if (c_mode_quoted_path_at_point(buffer, line_start, line_end, point, out, out_size) == 0) { + return 0; + } + + size_t pos = point; + if (pos == buffer->len || (pos < buffer->len && !c_mode_path_char(buffer->data[pos]))) { + if (pos == 0 || !c_mode_path_char(buffer->data[pos - 1])) return -1; + pos--; + } + + size_t start = pos; + while (start > line_start && c_mode_path_char(buffer->data[start - 1])) start--; + + size_t end = pos; + while (end < line_end && c_mode_path_char(buffer->data[end])) end++; + + return c_mode_copy_range(buffer, start, end, out, out_size); +} + +static int cmd_c_find_file_at_point(ecex_t *ed) { + if (!ed) return -1; + buffer_t *buffer = ecex_current_buffer(ed); + if (!buffer) return -1; + + char path[1024]; + if (c_mode_path_at_point(buffer, path, sizeof(path)) != 0) { + ecex_message(ed, "No file at point"); + return 0; + } + + return ecex_find_project_file(ed, buffer->path, path); +} + +static int cmd_c_jump_to_definition(ecex_t *ed) { + return ecex_clangd_jump_to_definition(ed); +} + static int c_mode_file_handler(ecex_t *ed, buffer_t *buffer) { if (!ed || !buffer) return -1; return ecex_buffer_set_major_mode_by_name(ed, buffer, "c-mode"); @@ -158,10 +311,21 @@ ECEX_PLUGIN_BEGIN(ecex_c_mode_plugin, C_MODE_PLUGIN_ID) if (!ecex_define_major_mode(ed, "c-mode")) return -1; ECEX_CONFIG_COMMAND("c-indent-line", cmd_c_indent_line); + ECEX_CONFIG_COMMAND("c-newline-and-indent", cmd_c_newline_and_indent); + ECEX_CONFIG_COMMAND("c-dedent-line", cmd_c_dedent_line); ECEX_CONFIG_COMMAND("c-complete", cmd_c_complete); + ECEX_CONFIG_COMMAND("c-tab-width-2", cmd_c_tab_width_2); + ECEX_CONFIG_COMMAND("c-tab-width-4", cmd_c_tab_width_4); + ECEX_CONFIG_COMMAND("c-tab-width-8", cmd_c_tab_width_8); + ECEX_CONFIG_COMMAND("c-jump-to-definition", cmd_c_jump_to_definition); + ECEX_CONFIG_COMMAND("c-find-file-at-point", cmd_c_find_file_at_point); ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "TAB", "c-indent-line")); + ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "S-TAB", "c-dedent-line")); + ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "ENTER", "c-newline-and-indent")); ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-TAB", "c-complete")); ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-S-TAB", "complete-at-point-previous")); + ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-g d", "c-jump-to-definition")); + ECEX_CONFIG_TRY(ecex_bind_mode_key(ed, "c-mode", "C-g f", "c-find-file-at-point")); if (ecex_dependency_available("clangd")) { ECEX_CONFIG_TRY(ecex_add_clangd_completion_provider(ed, diff --git a/config/ecex_api_completion_plugin.c b/config/ecex_api_completion_plugin.c index b806349..367ec09 100644 --- a/config/ecex_api_completion_plugin.c +++ b/config/ecex_api_completion_plugin.c @@ -202,6 +202,8 @@ "ecex_draw_stat_i\n" \ "ecex_draw_tetris_preview_i\n" \ "ecex_find_file\n" \ + "ecex_find_file_at\n" \ + "ecex_find_project_file\n" \ "ecex_save_current_buffer\n" \ "ecex_write_current_buffer\n" \ "ecex_compile\n" \ @@ -211,6 +213,9 @@ "ecex_next_interactive_action\n" \ "ecex_previous_interactive_action\n" \ "ecex_indent_line_to\n" \ + "ecex_clangd_jump_to_definition\n" \ + "ecex_c_mode_set_tab_width\n" \ + "ecex_c_mode_tab_width\n" \ "ecex_comment_region\n" \ "ecex_uncomment_region\n" \ "ecex_request_prompt\n" \ @@ -296,6 +301,7 @@ "ecex_path_dirname\n" \ "ecex_path_basename_dup\n" \ "ecex_path_normalize\n" \ + "ecex_project_root_for_file\n" \ "ecex_path_is_dir\n" \ "ecex_path_is_file\n" \ "ecex_path_exists\n" \ @@ -517,6 +523,8 @@ #define ECEX_API_SIGNATURE_ENTRIES_7 \ "ecex_find_file\tint ecex_find_file(ecex_t *ed, const char *path)\n" \ + "ecex_find_file_at\tint ecex_find_file_at(ecex_t *ed, const char *path, size_t line, size_t column)\n" \ + "ecex_find_project_file\tint ecex_find_project_file(ecex_t *ed, const char *from_file, const char *path)\n" \ "ecex_save_current_buffer\tint ecex_save_current_buffer(ecex_t *ed)\n" \ "ecex_write_current_buffer\tint ecex_write_current_buffer(ecex_t *ed, const char *path)\n" \ "ecex_compile\tint ecex_compile(ecex_t *ed, const char *command)\n" \ @@ -526,6 +534,9 @@ "ecex_next_interactive_action\tint ecex_next_interactive_action(ecex_t *ed)\n" \ "ecex_previous_interactive_action\tint ecex_previous_interactive_action(ecex_t *ed)\n" \ "ecex_indent_line_to\tint ecex_indent_line_to(buffer_t *buffer, int target_cols)\n" \ + "ecex_clangd_jump_to_definition\tint ecex_clangd_jump_to_definition(ecex_t *ed)\n" \ + "ecex_c_mode_set_tab_width\tint ecex_c_mode_set_tab_width(int spaces)\n" \ + "ecex_c_mode_tab_width\tint ecex_c_mode_tab_width(void)\n" \ "ecex_comment_region\tint ecex_comment_region(ecex_t *ed)\n" \ "ecex_uncomment_region\tint ecex_uncomment_region(ecex_t *ed)\n" \ "ecex_request_prompt\tvoid ecex_request_prompt(ecex_t *ed, ecex_prompt_request_t request, const char *message)\n" \ @@ -618,6 +629,7 @@ "ecex_path_dirname\tchar *ecex_path_dirname(const char *path)\n" \ "ecex_path_basename_dup\tchar *ecex_path_basename_dup(const char *path)\n" \ "ecex_path_normalize\tchar *ecex_path_normalize(const char *path)\n" \ + "ecex_project_root_for_file\tchar *ecex_project_root_for_file(const char *path)\n" \ "ecex_path_is_dir\tint ecex_path_is_dir(const char *path)\n" \ "ecex_path_is_file\tint ecex_path_is_file(const char *path)\n" \ "ecex_path_exists\tint ecex_path_exists(const char *path)\n" \ @@ -689,6 +701,9 @@ "last_eval_source\tchar *last_eval_source\n" \ "last_eval_filename\tchar *last_eval_filename\n" \ "last_eval_wrap_as_statements\tint last_eval_wrap_as_statements\n" \ + "eval_modules\tecex_eval_module_t *eval_modules\n" \ + "eval_module_cap\tsize_t eval_module_cap\n" \ + "eval_module_count\tsize_t eval_module_count\n" \ "last_compile_command\tchar *last_compile_command\n" \ "last_grep_command\tchar *last_grep_command\n" \ "prompt_request\tecex_prompt_request_t prompt_request\n" \ @@ -746,6 +761,10 @@ "plugins\n" \ "last_eval_source\n" \ "last_eval_filename\n" \ + "last_eval_wrap_as_statements\n" \ + "eval_modules\n" \ + "eval_module_cap\n" \ + "eval_module_count\n" \ "last_compile_command\n" \ "last_grep_command\n" \ "prompt_request\n" \ @@ -943,6 +962,8 @@ static const char *ecex_api_symbols[] ECEX_API_UNUSED = { "ecex_draw_stat_i", "ecex_draw_tetris_preview_i", "ecex_find_file", + "ecex_find_file_at", + "ecex_find_project_file", "ecex_save_current_buffer", "ecex_write_current_buffer", "ecex_compile", @@ -951,6 +972,10 @@ static const char *ecex_api_symbols[] ECEX_API_UNUSED = { "ecex_rerun_grep", "ecex_next_interactive_action", "ecex_previous_interactive_action", + "ecex_indent_line_to", + "ecex_clangd_jump_to_definition", + "ecex_c_mode_set_tab_width", + "ecex_c_mode_tab_width", "ecex_comment_region", "ecex_uncomment_region", "ecex_request_prompt", @@ -1036,6 +1061,7 @@ static const char *ecex_api_symbols[] ECEX_API_UNUSED = { "ecex_path_dirname", "ecex_path_basename_dup", "ecex_path_normalize", + "ecex_project_root_for_file", "ecex_path_is_dir", "ecex_path_is_file", "ecex_path_exists", @@ -1088,6 +1114,10 @@ static const char *ecex_ed_fields[] ECEX_API_UNUSED = { "plugins", "last_eval_source", "last_eval_filename", + "last_eval_wrap_as_statements", + "eval_modules", + "eval_module_cap", + "eval_module_count", "last_compile_command", "last_grep_command", "prompt_request", diff --git a/config/ecexrc.c b/config/ecexrc.c index 75f2470..dbce318 100644 --- a/config/ecexrc.c +++ b/config/ecexrc.c @@ -158,8 +158,9 @@ ECEX_CONFIG_BEGIN buffer_insert(scratch, "ecex config loaded !\n"); buffer_insert(scratch, "Try M-x, C-x C-f, C-x C-s, C-x d file browser, F2 commands, F3 buffers, F4 demo menu, C-x b.\n"); buffer_insert(scratch, "which-key is enabled: press C-x or M-g and the minibuffer will show continuations.\n"); - buffer_insert(scratch, "TAB indents; in C buffers it smart-indents. C-TAB completes and cycles candidates.\n"); - buffer_insert(scratch, "C completion asks clangd; c-mode highlights C syntax.\n"); + buffer_insert(scratch, "TAB indents; in C buffers ENTER auto-indents, S-TAB dedents, and C-TAB completes.\n"); + buffer_insert(scratch, "C-g d/f use the project root; clangd reads compile_flags.txt, C-g f also reads .ecex-project.\n"); + buffer_insert(scratch, "C completion asks clangd; c-mode highlights C syntax. Use ecex_c_mode_set_tab_width(2/4/8) in config.\n"); buffer_insert(scratch, "When c-tools is loaded, C-x c l checks, C-x c k lints, C-x c e lints with ./include.\n"); buffer_insert(scratch, "Run M-x render-demo for a custom renderer demo, or M-x tetris for Tetris.\n"); buffer_insert(scratch, "Example line to edit then C-x C-e: ecex_set_font_size(ed, 28.0f);\n"); diff --git a/config/tetris.c b/config/tetris.c index e86ab75..852b5ee 100644 --- a/config/tetris.c +++ b/config/tetris.c @@ -131,9 +131,6 @@ static void tetris_spawn(tetris_state_t *s) { ecex_log_ptr("tetris_spawn: state=", s); if (!s) return; - /* The sidebar must show the piece that will become active on the next - * spawn. Keep that queued value in a plugin slot, then consume - * it here and immediately replace it with a freshly-picked preview. */ queued = tetris_next_piece(s); if (queued < 0 || queued > 6) queued = tetris_pick(s); s->piece = queued; @@ -307,15 +304,9 @@ static int tetris_move_horizontal(tetris_state_t *s, int dx) { if (!tetris_collides(s, s->piece, s->rot, old_x + dx, old_y)) { s->x = old_x + dx; - /* Horizontal movement must never vertical-kick the active piece. - * Keep these assignments explicit because plugin/JIT callback bugs are - * otherwise very hard to distinguish from game movement. */ s->y = old_y; s->rot = old_rot; - /* Restart the gravity phase after manual lateral input. This avoids the - * visual "bop" where a key repeat lands on the same frame as gravity - * and appears to couple side movement with a vertical step. */ s->last_drop_ms = 0; ecex_log_int("tetris_move_horizontal: x=", s->x); ecex_log_int("tetris_move_horizontal: y=", s->y); diff --git a/docs/plugin-api.md b/docs/plugin-api.md index 76051f9..e520cfd 100644 --- a/docs/plugin-api.md +++ b/docs/plugin-api.md @@ -332,7 +332,6 @@ Plugin logging is optional. Normal runs are quiet. Enable logs with: ```sh ECEX_LOG=1 ECEX_TRACE_CALLBACKS=1 -ECEX_TRACE_TETRIS=1 ``` `ECEX_TRACE_CALLBACKS=1` also logs host callback entry/exit for renderer, @@ -18,6 +18,7 @@ (string-match "/Makefile$" file) (string-match "/LICENSE$" file) (string-match "/README\\.md$" file) + (string-match "/compile_flags\\.txt$" file) (string-match "/manifest\\.scm$" file) (string-match "/guix\\.scm$" file) (string-match "/\\.gitignore$" file))) diff --git a/include/ecex.h b/include/ecex.h index a7d4b24..ec08bd0 100644 --- a/include/ecex.h +++ b/include/ecex.h @@ -302,6 +302,8 @@ void ecex_draw_stat_i(ecex_draw_context_t *ctx, int x, int y, int label_id, int void ecex_draw_tetris_preview_i(ecex_draw_context_t *ctx, int piece, int x, int y, int cell, int alpha); int ecex_find_file(ecex_t *ed, const char *path); +int ecex_find_file_at(ecex_t *ed, const char *path, size_t line, size_t column); +int ecex_find_project_file(ecex_t *ed, const char *from_file, const char *path); int ecex_save_current_buffer(ecex_t *ed); int ecex_write_current_buffer(ecex_t *ed, const char *path); int ecex_compile(ecex_t *ed, const char *command); @@ -311,6 +313,9 @@ int ecex_rerun_grep(ecex_t *ed); int ecex_next_interactive_action(ecex_t *ed); int ecex_previous_interactive_action(ecex_t *ed); int ecex_indent_line_to(buffer_t *buffer, int target_cols); +int ecex_clangd_jump_to_definition(ecex_t *ed); +int ecex_c_mode_set_tab_width(int spaces); +int ecex_c_mode_tab_width(void); int ecex_comment_region(ecex_t *ed); int ecex_uncomment_region(ecex_t *ed); void ecex_request_prompt(ecex_t *ed, ecex_prompt_request_t request, const char *message); diff --git a/include/path.h b/include/path.h index e76a654..4debbad 100644 --- a/include/path.h +++ b/include/path.h @@ -9,6 +9,7 @@ char *ecex_path_join(const char *dir, const char *name); char *ecex_path_dirname(const char *path); char *ecex_path_basename_dup(const char *path); char *ecex_path_normalize(const char *path); +char *ecex_project_root_for_file(const char *path); int ecex_path_is_dir(const char *path); int ecex_path_is_file(const char *path); int ecex_path_exists(const char *path); diff --git a/include/types.h b/include/types.h index c950bbb..129acb5 100644 --- a/include/types.h +++ b/include/types.h @@ -271,6 +271,11 @@ typedef struct ecex_major_mode { char *name; } ecex_major_mode_t; +typedef struct ecex_eval_module { + char *key; + void *module; +} ecex_eval_module_t; + struct ecex_window { buffer_t *buffer; float x; @@ -343,6 +348,9 @@ struct ecex { char *last_eval_source; char *last_eval_filename; int last_eval_wrap_as_statements; + ecex_eval_module_t *eval_modules; + size_t eval_module_cap; + size_t eval_module_count; char *last_compile_command; char *last_grep_command; @@ -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); diff --git a/tests/test_completion_path.c b/tests/test_completion_path.c index 8170ee9..6615c38 100644 --- a/tests/test_completion_path.c +++ b/tests/test_completion_path.c @@ -1,11 +1,16 @@ +#define _POSIX_C_SOURCE 200809L + #include "test_core.h" #include "completion.h" #include "ecex.h" #include <assert.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> +#include <sys/stat.h> +#include <unistd.h> void test_completion_helpers(void) { assert(ecex_ascii_strncasecmp("Alpha", "alpha", 5) == 0); @@ -56,4 +61,34 @@ void test_path_helpers(void) { assert(ecex_path_is_image("photo.PNG") == 1); assert(ecex_path_is_video("clip.MKV") == 1); assert(ecex_path_is_media("notes.txt") == 0); + + char root_dir[256]; + snprintf(root_dir, sizeof(root_dir), "/tmp/ecex-project-test-%ld", (long)getpid()); + char src_dir[320]; + char nested_dir[384]; + char marker[384]; + char source_file[448]; + snprintf(src_dir, sizeof(src_dir), "%s/src", root_dir); + snprintf(nested_dir, sizeof(nested_dir), "%s/nested", src_dir); + snprintf(marker, sizeof(marker), "%s/.ecex-project", root_dir); + snprintf(source_file, sizeof(source_file), "%s/main.c", nested_dir); + + assert(mkdir(root_dir, 0700) == 0); + assert(mkdir(src_dir, 0700) == 0); + assert(mkdir(nested_dir, 0700) == 0); + FILE *file = fopen(marker, "wb"); + assert(file); + fclose(file); + + char *project_root = ecex_project_root_for_file(source_file); + char *normal_root = ecex_path_normalize(root_dir); + assert(project_root && normal_root); + assert(strcmp(project_root, normal_root) == 0); + free(project_root); + free(normal_root); + + unlink(marker); + rmdir(nested_dir); + rmdir(src_dir); + rmdir(root_dir); } |
