aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Moc <personal@cdatgoose.org>2026-06-03 04:14:59 +0200
committerDavid Moc <personal@cdatgoose.org>2026-06-03 04:14:59 +0200
commitaba45a64364457f20e84de4189500f4426e11d53 (patch)
treefe42d47f01decd53e28bb187cd3d85de78035a46
parentc1ccd38b31d722c843ab311338e2b8d1905eb8f8 (diff)
Added ffap and fixed an eval memory leakmain
-rw-r--r--.gitignore1
-rw-r--r--README.md15
-rw-r--r--config/c_mode_plugin.c170
-rw-r--r--config/ecex_api_completion_plugin.c30
-rw-r--r--config/ecexrc.c5
-rw-r--r--config/tetris.c9
-rw-r--r--docs/plugin-api.md1
-rw-r--r--guix.scm1
-rw-r--r--include/ecex.h5
-rw-r--r--include/path.h1
-rw-r--r--include/types.h8
-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
-rw-r--r--tests/test_completion_path.c35
17 files changed, 910 insertions, 31 deletions
diff --git a/.gitignore b/.gitignore
index 87e1836..dd96ece 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
bin/
crash.dump
-compile_flags.txt
*.o
*.so
*.a
diff --git a/README.md b/README.md
index 7454862..fb532d5 100644
--- a/README.md
+++ b/README.md
@@ -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,
diff --git a/guix.scm b/guix.scm
index 6b312f7..12f4dc7 100644
--- a/guix.scm
+++ b/guix.scm
@@ -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;
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);
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);
}