From 6aeaa171dc1ca43392f53cbd02097f76e1b1c5a0 Mon Sep 17 00:00:00 2001 From: David Moc Date: Sun, 31 May 2026 03:47:04 +0200 Subject: Hardened API, tetris, MD-View --- src/app.c | 511 ++++++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 346 insertions(+), 165 deletions(-) (limited to 'src/app.c') diff --git a/src/app.c b/src/app.c index 3ae368c..9a38e40 100644 --- a/src/app.c +++ b/src/app.c @@ -1,5 +1,7 @@ #include "app.h" #include "common.h" +#include "completion.h" +#include "path.h" #include #include @@ -60,140 +62,12 @@ static void app_set_prompt_input(app_t *app, const char *text) { app->dirty = 1; } -static char *app_expand_user_path(const char *path) { - if (!path) return NULL; - - if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - const char *home = getenv("HOME"); - if (!home || !home[0]) { - char *copy = malloc(strlen(path) + 1); - if (!copy) return NULL; - strcpy(copy, path); - return copy; - } - - size_t home_len = strlen(home); - size_t rest_len = strlen(path + 1); - - char *expanded = malloc(home_len + rest_len + 1); - if (!expanded) return NULL; - - memcpy(expanded, home, home_len); - memcpy(expanded + home_len, path + 1, rest_len + 1); - return expanded; -} - typedef struct command_candidate { const char *name; int score; size_t order; } command_candidate_t; -static int ascii_lower(int c) { - if (c >= 'A' && c <= 'Z') return c - 'A' + 'a'; - return c; -} - -static int ascii_strncasecmp_local(const char *a, const char *b, size_t n) { - for (size_t i = 0; i < n; i++) { - int ac = ascii_lower((unsigned char)a[i]); - int bc = ascii_lower((unsigned char)b[i]); - - if (ac != bc || ac == '\0' || bc == '\0') { - return ac - bc; - } - } - - return 0; -} - -static int ascii_contains_ci(const char *haystack, const char *needle) { - if (!haystack || !needle) return 0; - if (needle[0] == '\0') return 1; - - size_t needle_len = strlen(needle); - - for (size_t i = 0; haystack[i]; i++) { - if (ascii_strncasecmp_local(haystack + i, needle, needle_len) == 0) { - return 1; - } - } - - return 0; -} - -static int command_fuzzy_score(const char *candidate, const char *query) { - if (!candidate || !query) return -1; - - if (query[0] == '\0') { - return 0; - } - - int score = 0; - int consecutive = 0; - int last_match = -1; - - size_t ci = 0; - size_t qi = 0; - - while (candidate[ci] && query[qi]) { - char c = candidate[ci]; - char q = query[qi]; - - if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); - if (q >= 'A' && q <= 'Z') q = (char)(q - 'A' + 'a'); - - if (c == q) { - score += 10; - - if ((int)ci == last_match + 1) { - consecutive++; - score += 5 * consecutive; - } else { - consecutive = 0; - } - - if (ci == 0) { - score += 20; - } - - if (ci > 0 && - (candidate[ci - 1] == '-' || - candidate[ci - 1] == '_' || - candidate[ci - 1] == ' ')) { - score += 15; - } - - last_match = (int)ci; - qi++; - } - - ci++; - } - - if (query[qi] != '\0') { - return -1; - } - - score -= (int)strlen(candidate); - - if (strncmp(candidate, query, strlen(query)) == 0) { - score += 100; - } - - if (ascii_contains_ci(candidate, query)) { - score += 75; - } - - return score; -} - static int compare_command_candidates(const void *a, const void *b) { const command_candidate_t *ca = (const command_candidate_t *)a; const command_candidate_t *cb = (const command_candidate_t *)b; @@ -220,7 +94,7 @@ static command_candidate_t *collect_command_candidates(ecex_t *ed, for (size_t i = 0; i < ed->command_count; i++) { const char *name = ed->commands[i].name; - int score = command_fuzzy_score(name, query); + int score = ecex_fuzzy_score(name, query); if (score >= 0) { items[count].name = name; @@ -280,14 +154,14 @@ static int compare_path_candidates(const void *a, const void *b) { const path_candidate_t *pa = (const path_candidate_t *)a; const path_candidate_t *pb = (const path_candidate_t *)b; - if (pa->score != pb->score) { - return pb->score - pa->score; - } - if (pa->is_dir != pb->is_dir) { return pb->is_dir - pa->is_dir; } + if (pa->score != pb->score) { + return pb->score - pa->score; + } + int cmp = strcmp(pa->path, pb->path); if (cmp != 0) return cmp; @@ -323,7 +197,7 @@ static int split_path_query(const char *query, if (display_dir[0] == '\0') { snprintf(fs_dir, fs_dir_size, "."); } else { - char *expanded = app_expand_user_path(display_dir); + char *expanded = ecex_path_expand_user(display_dir); if (!expanded) return -1; snprintf(fs_dir, fs_dir_size, "%s", expanded); @@ -393,7 +267,7 @@ static path_candidate_t *collect_path_candidates(const char *query, continue; } - int score = command_fuzzy_score(name, needle); + int score = ecex_fuzzy_score(name, needle); if (score < 0) { order++; continue; @@ -446,35 +320,151 @@ static path_candidate_t *collect_path_candidates(const char *query, return items; } -static const char *path_candidate_at(const char *query, - size_t index, - size_t *out_count) { - static char candidate[1024]; +typedef ecex_completion_item_t prompt_candidate_t; + +static void free_prompt_candidates(prompt_candidate_t *items, size_t count) { + ecex_completion_items_free(items, count); +} + +static int compare_prompt_candidates(const void *a, const void *b) { + return ecex_completion_item_compare(a, b); +} + +static int prompt_kind_uses_path_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_FIND_FILE || + kind == ECEX_PROMPT_WRITE_FILE || + kind == ECEX_PROMPT_EVAL_FILE; +} + +static int prompt_kind_uses_buffer_completion(ecex_prompt_request_t kind) { + return kind == ECEX_PROMPT_SWITCH_BUFFER || + kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER; +} + +static prompt_candidate_t *collect_buffer_candidates(ecex_t *ed, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!ed || !query || ed->buffer_count == 0) return NULL; + + prompt_candidate_t *items = calloc(ed->buffer_count, sizeof(*items)); + if (!items) return NULL; size_t count = 0; - path_candidate_t *items = collect_path_candidates(query, &count); - if (!items || count == 0) { - free_path_candidates(items, count); + for (size_t i = 0; i < ed->buffer_count; i++) { + buffer_t *buf = ed->buffers[i]; + if (!buf || !buf->name) continue; + + int score = ecex_fuzzy_score(buf->name, query); + if (score < 0) continue; + + items[count].value = malloc(strlen(buf->name) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, buf->name); + items[count].score = score; + items[count].is_dir = 0; + items[count].order = i; + count++; + } + + if (count == 0) { + free(items); return NULL; } - snprintf(candidate, sizeof(candidate), "%s", items[index % count].path); + qsort(items, count, sizeof(*items), compare_prompt_candidates); + if (out_count) *out_count = count; + return items; +} - if (out_count) { - *out_count = count; +static prompt_candidate_t *collect_prompt_candidates(app_t *app, + const char *query, + size_t *out_count) { + if (out_count) *out_count = 0; + if (!app || !query) return NULL; + + if (prompt_kind_uses_path_completion(app->prompt_kind)) { + size_t path_count = 0; + path_candidate_t *paths = collect_path_candidates(query, &path_count); + if (!paths || path_count == 0) { + free_path_candidates(paths, path_count); + return NULL; + } + + prompt_candidate_t *items = calloc(path_count, sizeof(*items)); + if (!items) { + free_path_candidates(paths, path_count); + return NULL; + } + + size_t count = 0; + for (size_t i = 0; i < path_count; i++) { + items[count].value = malloc(strlen(paths[i].path) + 1); + if (!items[count].value) continue; + strcpy(items[count].value, paths[i].path); + items[count].score = paths[i].score; + items[count].is_dir = paths[i].is_dir; + items[count].order = paths[i].order; + count++; + } + + free_path_candidates(paths, path_count); + if (count == 0) { + free(items); + return NULL; + } + if (out_count) *out_count = count; + return items; + } + + if (prompt_kind_uses_buffer_completion(app->prompt_kind)) { + return collect_buffer_candidates(app->ed, query, out_count); + } + + return NULL; +} + +static const char *prompt_candidate_at(app_t *app, + const char *query, + size_t index, + size_t *out_count) { + static char candidate[1024]; + + size_t count = 0; + prompt_candidate_t *items = collect_prompt_candidates(app, query, &count); + if (!items || count == 0) { + free_prompt_candidates(items, count); + return NULL; } - free_path_candidates(items, count); + snprintf(candidate, sizeof(candidate), "%s", items[index % count].value); + if (out_count) *out_count = count; + + free_prompt_candidates(items, count); return candidate; } +static int app_prompt_input_is_directory(app_t *app) { + if (!app || !prompt_kind_uses_path_completion(app->prompt_kind)) return 0; + if (app->prompt_input_len == 0) return 0; + + char *expanded = ecex_path_expand_user(app->prompt_input); + if (!expanded) return 0; + + struct stat st; + int is_dir = stat(expanded, &st) == 0 && S_ISDIR(st.st_mode); + free(expanded); + return is_dir; +} + static void app_update_prompt_completion_preview(app_t *app) { if (!app || !app->prompt_completion_active) return; size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app->prompt_completion_preview_start = 0; app->prompt_completion_preview_count = 0; return; @@ -495,11 +485,11 @@ static void app_update_prompt_completion_preview(app_t *app) { snprintf(app->prompt_completion_preview[i], sizeof(app->prompt_completion_preview[i]), "%s", - items[start + i].path); + items[start + i].value); app->prompt_completion_preview_count++; } - free_path_candidates(items, count); + free_prompt_candidates(items, count); } static void app_begin_prompt_completion(app_t *app, int direction) { @@ -511,12 +501,12 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_input); size_t count = 0; - path_candidate_t *items = collect_path_candidates(app->prompt_completion_query, &count); + prompt_candidate_t *items = collect_prompt_candidates(app, app->prompt_completion_query, &count); if (!items || count == 0) { - free_path_candidates(items, count); + free_prompt_candidates(items, count); app_reset_prompt_completion(app); - app_message(app, "No file completions"); + app_message(app, prompt_kind_uses_buffer_completion(app->prompt_kind) ? "No buffer completions" : "No file completions"); app->mode = APP_MODE_PROMPT; return; } @@ -525,8 +515,8 @@ static void app_begin_prompt_completion(app_t *app, int direction) { app->prompt_completion_count = count; app->prompt_completion_index = direction < 0 ? count - 1 : 0; - app_set_prompt_input(app, items[app->prompt_completion_index].path); - free_path_candidates(items, count); + app_set_prompt_input(app, items[app->prompt_completion_index].value); + free_prompt_candidates(items, count); app_update_prompt_completion_preview(app); } @@ -548,9 +538,10 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { } size_t count = 0; - const char *candidate = path_candidate_at(app->prompt_completion_query, - app->prompt_completion_index, - &count); + const char *candidate = prompt_candidate_at(app, + app->prompt_completion_query, + app->prompt_completion_index, + &count); if (!candidate || count == 0) { app_reset_prompt_completion(app); @@ -562,6 +553,18 @@ static void app_cycle_prompt_completion(app_t *app, int delta) { app_update_prompt_completion_preview(app); } +static void app_complete_prompt(app_t *app) { + if (!app) return; + + if (app->prompt_completion_active && app_prompt_input_is_directory(app)) { + app_reset_prompt_completion(app); + app_begin_prompt_completion(app, 1); + return; + } + + app_cycle_prompt_completion(app, 1); +} + static void app_begin_completion(app_t *app, int direction) { if (!app) return; @@ -819,25 +822,47 @@ static void app_cancel_prompt(app_t *app) { app_message(app, "Prompt cancelled"); } +static const char *app_default_prompt_input(app_t *app, ecex_prompt_request_t kind) { + if (!app || !app->ed) return NULL; + + if (kind == ECEX_PROMPT_SWITCH_BUFFER) { + buffer_t *other = ecex_other_buffer(app->ed); + return other ? other->name : NULL; + } + + if (kind == ECEX_PROMPT_KILL_BUFFER || + kind == ECEX_PROMPT_FORCE_KILL_BUFFER) { + buffer_t *current = ecex_current_buffer(app->ed); + return current ? current->name : NULL; + } + + return NULL; +} + static void app_submit_prompt(app_t *app) { if (!app || !app->ed) return; - if (app->prompt_input_len == 0 && app->prompt_kind != ECEX_PROMPT_COMPILE) { + ecex_prompt_request_t kind = app->prompt_kind; + const char *default_input = app_default_prompt_input(app, kind); + + if (app->prompt_input_len == 0 && + kind != ECEX_PROMPT_COMPILE && + (!default_input || default_input[0] == '\0')) { app_cancel_prompt(app); return; } char input[sizeof(app->prompt_input)]; - snprintf(input, sizeof(input), "%s", app->prompt_input); + snprintf(input, sizeof(input), "%s", + app->prompt_input_len > 0 ? app->prompt_input : (default_input ? default_input : "")); - ecex_prompt_request_t kind = app->prompt_kind; app->mode = APP_MODE_EDIT; app->prompt_kind = ECEX_PROMPT_NONE; app->prompt_label[0] = '\0'; app->prompt_input[0] = '\0'; app->prompt_input_len = 0; - char *expanded_input = app_expand_user_path(input); + char *expanded_input = ecex_path_expand_user(input); const char *path = expanded_input ? expanded_input : input; int result = -1; @@ -869,6 +894,11 @@ static void app_submit_prompt(app_t *app) { verb = "Killed"; break; + case ECEX_PROMPT_FORCE_KILL_BUFFER: + result = ecex_kill_buffer_force(app->ed, input); + verb = "Force killed"; + break; + case ECEX_PROMPT_COMPILE: result = ecex_compile(app->ed, input[0] ? input : "make"); verb = "Compiled"; @@ -1049,6 +1079,11 @@ static float app_mono_cell_width(app_t *app) { return w > 1.0f ? w : app->font.size_px * 0.6f; } +static int app_trace_callbacks_enabled(void) { + const char *v = getenv("ECEX_TRACE_CALLBACKS"); + return v && v[0] && v[0] != '0'; +} + static float app_status_height(app_t *app) { float line_h = app->font.line_height > 1.0f ? app->font.line_height : app->font.size_px * 1.2f; float pad_y = app->font.size_px * 0.35f; @@ -1117,15 +1152,151 @@ static void app_move_point_to_pixel(app_t *app, double px, double py) { app->dirty = 1; } +static int app_window_rect_at(app_t *app, + double px, + double py, + size_t *out_index, + float *out_x, + float *out_y, + float *out_w, + float *out_h) { + if (!app || !app->ed || app->ed->window_count == 0) return 0; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + for (size_t i = 0; i < app->ed->window_count; i++) { + ecex_window_t *w = &app->ed->windows[i]; + float x = w->x * (float)app->width; + float y = w->y * editor_h; + float ww = w->w * (float)app->width; + float hh = w->h * editor_h; + if (px >= x && px < x + ww && py >= y && py < y + hh) { + if (out_index) *out_index = i; + if (out_x) *out_x = x; + if (out_y) *out_y = y; + if (out_w) *out_w = ww; + if (out_h) *out_h = hh; + return 1; + } + } + return 0; +} + +static int app_dispatch_buffer_mouse(app_t *app, int event, double px, double py, int button) { + if (!app || !app->ed) return 0; + + buffer_t *buf = NULL; + float wx = 0.0f; + float wy = 0.0f; + float ww = 0.0f; + float wh = 0.0f; + + if (app->mouse_capture_active && app->mouse_capture_buffer) { + buf = app->mouse_capture_buffer; + for (size_t i = 0; i < app->ed->window_count; i++) { + if (app->ed->windows[i].buffer != buf) continue; + float editor_h = (float)app->height - app_status_height(app); + if (app_minibuffer_visible(app)) editor_h -= app_minibuffer_height(app); + if (editor_h < 1.0f) editor_h = 1.0f; + wx = app->ed->windows[i].x * (float)app->width; + wy = app->ed->windows[i].y * editor_h; + ww = app->ed->windows[i].w * (float)app->width; + wh = app->ed->windows[i].h * editor_h; + break; + } + } else { + size_t wi = 0; + if (!app_window_rect_at(app, px, py, &wi, &wx, &wy, &ww, &wh)) return 0; + if (wi >= app->ed->window_count) return 0; + buf = app->ed->windows[wi].buffer; + } + + (void)ww; + (void)wh; + if (!buf || !buf->mouse_fn) return 0; + + int local_x = (int)(px - (double)wx); + int local_y = (int)(py - (double)wy); + if (local_x < -32768) local_x = -32768; + if (local_y < -32768) local_y = -32768; + + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_callback buffer=%p fn=%p event=%d x=%d y=%d button=%d userdata=%p\n", + (void *)buf, (void *)buf->mouse_fn, event, local_x, local_y, button, buf->mouse_userdata); + fflush(stderr); + } + + int result = buf->mouse_fn(app->ed, buf, event, local_x, local_y, button, buf->mouse_userdata); + if (result) app->dirty = 1; + return result != 0; +} + +static int app_mouse_button_id(int glfw_button) { + if (glfw_button == GLFW_MOUSE_BUTTON_LEFT) return ECEX_MOUSE_BUTTON_LEFT; + if (glfw_button == GLFW_MOUSE_BUTTON_RIGHT) return ECEX_MOUSE_BUTTON_RIGHT; + if (glfw_button == GLFW_MOUSE_BUTTON_MIDDLE) return ECEX_MOUSE_BUTTON_MIDDLE; + return glfw_button; +} + static void mouse_button_callback(GLFWwindow *window, int button, int action, int mods) { (void)mods; - if (button != GLFW_MOUSE_BUTTON_LEFT || action != GLFW_PRESS) return; app_t *app = glfwGetWindowUserPointer(window); if (!app) return; glfwGetFramebufferSize(window, &app->width, &app->height); double x = 0.0, y = 0.0; glfwGetCursorPos(window, &x, &y); - app_move_point_to_pixel(app, x, y); + int ebutton = app_mouse_button_id(button); + + if (action == GLFW_PRESS) { + size_t capture_wi = 0; + float capture_wx = 0.0f, capture_wy = 0.0f, capture_ww = 0.0f, capture_wh = 0.0f; + int have_capture_window = app_window_rect_at(app, x, y, &capture_wi, + &capture_wx, &capture_wy, &capture_ww, &capture_wh); + + if (app_dispatch_buffer_mouse(app, ECEX_MOUSE_PRESS, x, y, ebutton)) { + /* Start capture from the window resolved before dispatch. Do not + * re-hit-test after the plugin callback; the callback may mark the + * app dirty or mutate buffer/window state, and a second lookup can + * fail or select a different target. Without capture, GLFW cursor + * motion is delivered as plain ECEX_MOUSE_MOVE events, so rendered + * widgets never receive ECEX_MOUSE_DRAG. */ + if (have_capture_window && capture_wi < app->ed->window_count) { + app->mouse_capture_buffer = app->ed->windows[capture_wi].buffer; + app->mouse_capture_button = ebutton; + app->mouse_capture_active = 1; + if (app_trace_callbacks_enabled()) { + fprintf(stderr, "ecex-log: mouse_capture_start buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + } + return; + } + if (button == GLFW_MOUSE_BUTTON_LEFT) app_move_point_to_pixel(app, x, y); + } else if (action == GLFW_RELEASE) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_RELEASE, x, y, ebutton); + if (!app->mouse_capture_active || app->mouse_capture_button == ebutton) { + if (app_trace_callbacks_enabled() && app->mouse_capture_active) { + fprintf(stderr, "ecex-log: mouse_capture_end buffer=%p button=%d\n", + (void *)app->mouse_capture_buffer, ebutton); + fflush(stderr); + } + app->mouse_capture_active = 0; + app->mouse_capture_buffer = NULL; + app->mouse_capture_button = 0; + } + } +} + +static void cursor_pos_callback(GLFWwindow *window, double x, double y) { + app_t *app = glfwGetWindowUserPointer(window); + if (!app) return; + glfwGetFramebufferSize(window, &app->width, &app->height); + if (app->mouse_capture_active) { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_DRAG, x, y, app->mouse_capture_button); + } else { + app_dispatch_buffer_mouse(app, ECEX_MOUSE_MOVE, x, y, ECEX_MOUSE_BUTTON_LEFT); + } } static void scroll_callback(GLFWwindow *window, double xoffset, double yoffset) { @@ -1329,6 +1500,7 @@ static void char_callback(GLFWwindow *window, unsigned int codepoint) { if (app->mode == APP_MODE_PROMPT) { if (codepoint >= 32 && codepoint <= 126) { if (app->prompt_input_len + 1 < sizeof(app->prompt_input)) { + app_reset_prompt_completion(app); app->prompt_input[app->prompt_input_len++] = (char)codepoint; app->prompt_input[app->prompt_input_len] = '\0'; app->dirty = 1; @@ -1470,7 +1642,7 @@ static void key_callback(GLFWwindow *window, return; case GLFW_KEY_TAB: - app_cycle_prompt_completion(app, 1); + app_complete_prompt(app); return; case GLFW_KEY_DOWN: @@ -1531,6 +1703,14 @@ static void key_callback(GLFWwindow *window, try_execute_keybind(app, key_name); return; } + + if (key_sequence_has_prefix(app, key_name)) { + if (key_produces_text(key, scancode)) { + app->suppress_next_char = 1; + } + app_enter_prefix(app, key_name); + return; + } } if (key == GLFW_KEY_ESCAPE) { @@ -1574,5 +1754,6 @@ void app_install_callbacks(app_t *app) { glfwSetFramebufferSizeCallback(app->window, framebuffer_size_callback); glfwSetWindowRefreshCallback(app->window, window_refresh_callback); glfwSetMouseButtonCallback(app->window, mouse_button_callback); + glfwSetCursorPosCallback(app->window, cursor_pos_callback); glfwSetScrollCallback(app->window, scroll_callback); } -- cgit v1.2.3