From c6d44836fd8ed1442e01825cb0f9f97e7bf11515 Mon Sep 17 00:00:00 2001 From: David Moc Date: Wed, 3 Jun 2026 02:26:11 +0200 Subject: Harden editor logging and packaging --- .gitignore | 7 +++ LICENSE | 24 ++++++++++ Makefile | 67 +++++++++++++++++++++++---- README.md | 82 +++++++++++++++++++++++++++++++++ guix.scm | 75 ++++++++++++++++++++++++++++++ include/ecex.h | 2 + include/types.h | 3 ++ src/buffers.c | 18 ++++++-- src/config.c | 13 +++++- src/ecex.c | 106 +++++++++++++++++++++++++++++++++++++++++++ src/log.c | 22 +++++++++ src/path.c | 22 +++------ tests/test_buffers.c | 87 +++++++++++++++++++++++++++++++++++ tests/test_completion_path.c | 59 ++++++++++++++++++++++++ tests/test_core.c | 88 ----------------------------------- tests/test_core.h | 13 ++++++ tests/test_main.c | 13 ++++++ tests/test_plugin.c | 83 +++++++++++++++++++++++++++++++++ 18 files changed, 667 insertions(+), 117 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 guix.scm create mode 100644 tests/test_buffers.c create mode 100644 tests/test_completion_path.c delete mode 100644 tests/test_core.c create mode 100644 tests/test_core.h create mode 100644 tests/test_main.c create mode 100644 tests/test_plugin.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..87e1836 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +crash.dump +compile_flags.txt +*.o +*.so +*.a +*.dSYM/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efd398a --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +BSD 2-Clause License + +Copyright (c) 2026, Ecex contributors + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Makefile b/Makefile index 3a23f4f..79aecf7 100644 --- a/Makefile +++ b/Makefile @@ -1,28 +1,62 @@ CC = clang +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +DATADIR ?= $(PREFIX)/share +DOCDIR ?= $(DATADIR)/doc/ecex +ECEX_DATADIR ?= $(DATADIR)/ecex +ECEX_INCLUDEDIR ?= $(PREFIX)/include/ecex STATIC_LIB = include/libccdjit.a SHARED_LIB = include/libccdjit.so -SRC = src/main.c src/app.c src/completion.c src/path.c src/media.c src/render.c src/font.c src/util.c src/log.c src/buffers.c src/ecex.c src/plugin.c src/config.c src/eval.c +SRC = \ + src/main.c \ + src/app.c \ + src/completion.c \ + src/path.c \ + src/media.c \ + src/render.c \ + src/font.c \ + src/util.c \ + src/log.c \ + src/buffers.c \ + src/ecex.c \ + src/plugin.c \ + src/config.c \ + src/eval.c + +TEST_SRC = \ + tests/test_main.c \ + tests/test_plugin.c \ + tests/test_buffers.c \ + tests/test_completion_path.c \ + src/buffers.c \ + src/completion.c \ + src/path.c \ + src/util.c \ + src/log.c \ + src/plugin.c + BIN = bin/ecex PKG_CFLAGS = $(shell pkg-config --cflags glfw3 2>/dev/null) PKG_LIBS = $(shell pkg-config --libs glfw3 2>/dev/null || echo -lglfw) -CFLAGS ?= -std=c11 -Wall -Wextra -pedantic -Iinclude $(PKG_CFLAGS) +CPPFLAGS ?= -Iinclude $(PKG_CFLAGS) -DECEX_SYSTEM_INCLUDE_DIR=\"$(ECEX_INCLUDEDIR)\" +CFLAGS ?= -std=c11 -Wall -Wextra -pedantic LDLIBS = $(PKG_LIBS) -lGL -lm -.PHONY: all static shared clean run check sanitize debug release +.PHONY: all static shared clean run check sanitize debug release install uninstall all: static static: $(SRC) $(STATIC_LIB) @mkdir -p bin - $(CC) $(CFLAGS) $(SRC) $(STATIC_LIB) -o $(BIN) $(LDLIBS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(SRC) $(STATIC_LIB) -o $(BIN) $(LDLIBS) shared: $(SRC) $(SHARED_LIB) @mkdir -p bin - $(CC) $(CFLAGS) $(SRC) -Linclude -lccdjit -o $(BIN) $(LDLIBS) + $(CC) $(CPPFLAGS) $(CFLAGS) $(SRC) -Linclude -lccdjit -o $(BIN) $(LDLIBS) run: static LD_LIBRARY_PATH=include ./$(BIN) @@ -35,12 +69,29 @@ release: static sanitize: CFLAGS += -O1 -g -fsanitize=address,undefined -fno-omit-frame-pointer sanitize: LDLIBS += -fsanitize=address,undefined -sanitize: static +sanitize: SANITIZER_TEST_ENV = ASAN_OPTIONS=detect_leaks=0 +sanitize: static check check: @mkdir -p bin - $(CC) -std=c11 -Wall -Wextra -pedantic -Iinclude tests/test_core.c src/buffers.c src/completion.c src/path.c src/util.c src/log.c src/plugin.c -o bin/ecex-tests - ./bin/ecex-tests + $(CC) $(CPPFLAGS) $(CFLAGS) $(TEST_SRC) -o bin/ecex-tests + $(SANITIZER_TEST_ENV) ./bin/ecex-tests + +install: static + install -d "$(DESTDIR)$(BINDIR)" + install -d "$(DESTDIR)$(ECEX_INCLUDEDIR)" + install -d "$(DESTDIR)$(ECEX_DATADIR)/config" + install -d "$(DESTDIR)$(DOCDIR)" + install -m 755 "$(BIN)" "$(DESTDIR)$(BINDIR)/ecex" + install -m 644 include/*.h "$(DESTDIR)$(ECEX_INCLUDEDIR)/" + install -m 644 config/*.c "$(DESTDIR)$(ECEX_DATADIR)/config/" + install -m 644 README.md LICENSE docs/*.md "$(DESTDIR)$(DOCDIR)/" + +uninstall: + rm -f "$(DESTDIR)$(BINDIR)/ecex" + rm -rf "$(DESTDIR)$(ECEX_INCLUDEDIR)" + rm -rf "$(DESTDIR)$(ECEX_DATADIR)" + rm -rf "$(DESTDIR)$(DOCDIR)" clean: rm -f $(BIN) bin/ecex-tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..7454862 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Ecex + +Ecex is a small C editor with GLFW/OpenGL rendering and C config/plugins loaded +through CCDJIT. + +## Build + +Dependencies: + +- `clang` +- `make` +- `pkg-config` +- GLFW 3 +- OpenGL + +Build and test: + +```sh +make +make check +``` + +With Guix: + +```sh +guix shell -m manifest.scm -- make check +guix build -f guix.scm +``` + +Run: + +```sh +make run +``` + +Use a config or font explicitly: + +```sh +./bin/ecex --config config/ecexrc.c +./bin/ecex --font /path/to/font.ttf +``` + +Install locally: + +```sh +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. + +## Useful Keys + +- `M-x`: command prompt +- `C-x C-f`: find file +- `C-x C-s`: save buffer +- `C-x b`: switch buffer +- `C-x k`: kill buffer +- `C-x d`: file browser +- `M-x messages`: open the read-only `*Messages*` buffer +- `C-s` / `C-r`: incremental search +- `C-/`: undo +- `C-S-z`: redo +- `C-x C-r`: reload config +- `C-x C-c`: quit + +The sample config in `config/ecexrc.c` enables demo plugins including Markdown, +which-key, C mode, API completion, render demo, and Tetris. + +## Development + +`make check` runs the core C tests. `make sanitize` builds the app and runs the +tests with AddressSanitizer/UBSan flags. + +Plugin API conventions are documented in `docs/plugin-api.md`. Current CCDJIT +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. + +Ecex is distributed under the BSD 2-Clause License. See `LICENSE`. diff --git a/guix.scm b/guix.scm new file mode 100644 index 0000000..bd2d244 --- /dev/null +++ b/guix.scm @@ -0,0 +1,75 @@ +(use-modules (guix packages) + (guix build-system gnu) + (guix gexp) + ((guix licenses) #:prefix license:) + (gnu packages fonts) + (gnu packages gl) + (gnu packages llvm) + (gnu packages pkg-config) + (ice-9 regex)) + +(define (ecex-source-file? file stat) + (or (string-match "/src($|/)" file) + (string-match "/include($|/)" file) + (string-match "/config($|/)" file) + (string-match "/docs($|/)" file) + (string-match "/tests($|/)" file) + (string-match "/Makefile$" file) + (string-match "/LICENSE$" file) + (string-match "/README\\.md$" file) + (string-match "/manifest\\.scm$" file) + (string-match "/guix\\.scm$" file) + (string-match "/\\.gitignore$" file))) + +(define-public ecex + (package + (name "ecex") + (version "0.1.0") + (source + (local-file "." "ecex-checkout" + #:recursive? #t + #:select? ecex-source-file?)) + (build-system gnu-build-system) + (arguments + (list + #:make-flags + #~(list "CC=clang" + (string-append "PREFIX=" #$output)) + #:phases + #~(modify-phases %standard-phases + (delete 'configure) + (replace 'build + (lambda* (#:key make-flags #:allow-other-keys) + (apply invoke "make" "static" make-flags))) + (replace 'check + (lambda* (#:key tests? make-flags #:allow-other-keys) + (when tests? + (apply invoke "make" "check" make-flags)))) + (replace 'install + (lambda* (#:key make-flags #:allow-other-keys) + (apply invoke "make" "install" make-flags))) + (add-after 'install 'wrap-ecex + (lambda* (#:key inputs outputs #:allow-other-keys) + (let* ((out (assoc-ref outputs "out")) + (font (assoc-ref inputs "font-dejavu")) + (font-path + (string-append font "/share/fonts/truetype/DejaVuSansMono.ttf"))) + (wrap-program (string-append out "/bin/ecex") + `("ECEX_INCLUDE" ":" prefix + (,(string-append out "/include/ecex"))) + `("ECEX_FONT" ":" = (,font-path))))))))) + (native-inputs + `(("clang-toolchain" ,clang-toolchain) + ("pkg-config" ,pkg-config))) + (inputs + `(("font-dejavu" ,font-dejavu) + ("glfw" ,glfw) + ("mesa" ,mesa))) + (home-page "git.cdatgoose.org/ecex.git") + (synopsis "Small C editor with CCDJIT-loaded C config and plugins") + (description + "Ecex is a small editor written in C with GLFW/OpenGL rendering and a +C-based config/plugin system loaded through CCDJIT.") + (license license:bsd-2))) + +ecex diff --git a/include/ecex.h b/include/ecex.h index 3e0048e..a7d4b24 100644 --- a/include/ecex.h +++ b/include/ecex.h @@ -46,6 +46,8 @@ void ecex_log_int(const char *message, int value); void ecex_log_double(const char *message, double value); void ecex_log_ptr(const char *message, const void *ptr); void ecex_log_flush(void); +void ecex_log_set_sink(ecex_log_sink_fn fn, void *userdata); +void ecex_log_clear_sink(void *userdata); void ecex_mem_zero(void *ptr, size_t size); int ecex_i32_get(const int *items, size_t index); void ecex_i32_set(int *items, size_t index, int value); diff --git a/include/types.h b/include/types.h index 008bd98..c950bbb 100644 --- a/include/types.h +++ b/include/types.h @@ -23,6 +23,7 @@ typedef void (*ecex_hook_free_fn)(void *userdata); typedef void (*ecex_command_hook_fn)(ecex_t *ed, const char *command, int event, int result, void *userdata); typedef void (*ecex_prefix_hook_fn)(ecex_t *ed, const char *prefix, int event, void *userdata); typedef void (*ecex_buffer_hook_fn)(ecex_t *ed, buffer_t *buffer, int event, void *userdata); +typedef void (*ecex_log_sink_fn)(const char *line, int depth, void *userdata); typedef int (*ecex_completion_provider_fn)(ecex_t *ed, buffer_t *buffer, const char *prefix, @@ -336,6 +337,8 @@ struct ecex { int next_major_mode_id; ecex_plugin_runtime_t *plugins; + buffer_t *messages_buffer; + int messages_append_active; char *last_eval_source; char *last_eval_filename; diff --git a/src/buffers.c b/src/buffers.c index 1e42bfc..c2e94e2 100644 --- a/src/buffers.c +++ b/src/buffers.c @@ -302,15 +302,25 @@ int buffer_delete_selection(buffer_t *buffer) { int buffer_replace_selection(buffer_t *buffer, const char *text) { if (!buffer || !text) return ECEX_ERR; if (!buffer_has_selection(buffer)) return buffer_insert(buffer, text); + if (buffer_record_undo(buffer) != ECEX_OK) return ECEX_ERR; size_t start = 0; size_t end = 0; buffer_selection_range(buffer, &start, &end); - if (buffer_delete_range(buffer, start, end) != ECEX_OK) return ECEX_ERR; - buffer->point = start; - buffer_clear_mark(buffer); - return buffer_insert(buffer, text); + int undo_disabled = buffer->undo_disabled; + int result = ECEX_OK; + buffer->undo_disabled = 1; + + result = buffer_delete_range(buffer, start, end); + if (result == ECEX_OK) { + buffer->point = start; + buffer_clear_mark(buffer); + result = buffer_insert(buffer, text); + } + + buffer->undo_disabled = undo_disabled; + return result; } int buffer_backspace(buffer_t *buffer) { diff --git a/src/config.c b/src/config.c index 9c885ff..6e4b84f 100644 --- a/src/config.c +++ b/src/config.c @@ -339,9 +339,18 @@ static int add_include_path(ccdjit_context *ctx, const char *path) { return ECEX_ERR; } +static int add_existing_include_path(ccdjit_context *ctx, const char *path) { + if (!path || !path[0] || !ecex_path_is_dir(path)) return ECEX_OK; + return add_include_path(ctx, path); +} + int ecex_add_ccdjit_include_paths(ccdjit_context *ctx) { - if (add_include_path(ctx, "include") != ECEX_OK) return ECEX_ERR; - if (add_include_path(ctx, "../include") != ECEX_OK) return ECEX_ERR; + if (add_existing_include_path(ctx, "include") != ECEX_OK) return ECEX_ERR; + if (add_existing_include_path(ctx, "../include") != ECEX_OK) return ECEX_ERR; + +#ifdef ECEX_SYSTEM_INCLUDE_DIR + if (add_existing_include_path(ctx, ECEX_SYSTEM_INCLUDE_DIR) != ECEX_OK) return ECEX_ERR; +#endif const char *env_include = getenv("ECEX_INCLUDE"); if (env_include && env_include[0]) { diff --git a/src/ecex.c b/src/ecex.c index a40a792..f3b4980 100644 --- a/src/ecex.c +++ b/src/ecex.c @@ -33,6 +33,9 @@ extern int kill(pid_t pid, int sig); #define ECEX_INITIAL_MODE_CAP 8 #define ECEX_INITIAL_MODE_KEYBIND_CAP 16 #define ECEX_INITIAL_HOOK_CAP 8 +#define ECEX_MESSAGES_BUFFER_NAME "*Messages*" +#define ECEX_MESSAGES_MAX_BYTES (1024u * 1024u) +#define ECEX_MESSAGES_TRIM_BYTES (256u * 1024u) ecex_window_t *ecex_current_window(ecex_t *ed); static void ecex_clear_command_hooks(ecex_t *ed); @@ -41,6 +44,10 @@ static void ecex_clear_buffer_hooks(ecex_t *ed); static void ecex_clear_completion_providers(ecex_t *ed); static int ecex_complete_at_point_direction(ecex_t *ed, int direction); static int ecex_indent_for_tab(ecex_t *ed); +static int ecex_buffer_index_of(ecex_t *ed, buffer_t *buffer, size_t *out_index); +static buffer_t *ecex_ensure_messages_buffer(ecex_t *ed); +static int ecex_messages_append(ecex_t *ed, const char *prefix, const char *message); +static void ecex_log_messages_sink(const char *line, int depth, void *userdata); void *ecex_config_alloc(size_t size) { @@ -769,10 +776,16 @@ static int cmd_file_browser_history_forward(ecex_t *ed) { } static int cmd_media_play_pause(ecex_t *ed) { return ecex_media_toggle_playback(ed); } +static int cmd_messages(ecex_t *ed) { + buffer_t *buffer = ecex_ensure_messages_buffer(ed); + return buffer ? ecex_switch_buffer(ed, buffer->name) : ECEX_ERR; +} static int ecex_register_builtins(ecex_t *ed) { ECEX_COMMAND("quit", cmd_quit); ECEX_COMMAND("force-quit", cmd_force_quit); + ECEX_COMMAND("messages", cmd_messages); + ECEX_COMMAND("view-messages", cmd_messages); ECEX_COMMAND("find-file", cmd_find_file); ECEX_COMMAND("file-browser", cmd_file_browser); ECEX_COMMAND("file-browser-here", cmd_file_browser_here); @@ -1011,12 +1024,18 @@ ecex_t *ecex_new(void) { } ecex_buffer_set_major_mode_by_name(ed, scratch, "fundamental-mode"); + if (!ecex_ensure_messages_buffer(ed)) { + ecex_free(ed); + return NULL; + } + ecex_log_set_sink(ecex_log_messages_sink, ed); return ed; } void ecex_free(ecex_t *ed) { if (!ed) return; + ecex_log_clear_sink(ed); /* Buffers may hold renderer/animation callbacks and userdata destructors * compiled by CCDJIT config modules. Run those destructors while their JIT @@ -1109,6 +1128,91 @@ int ecex_add_buffer(ecex_t *ed, buffer_t *buffer) { return ECEX_OK; } +static void ecex_messages_trim(buffer_t *buffer) { + if (!buffer || buffer->len <= ECEX_MESSAGES_MAX_BYTES) return; + + size_t trim = buffer->len - ECEX_MESSAGES_MAX_BYTES + ECEX_MESSAGES_TRIM_BYTES; + if (trim > buffer->len) trim = buffer->len; + while (trim < buffer->len && buffer->data[trim] != '\n') trim++; + if (trim < buffer->len) trim++; + + memmove(buffer->data, buffer->data + trim, buffer->len - trim + 1); + buffer->len -= trim; + buffer->point = buffer->len; + buffer->mark = 0; + buffer->mark_active = 0; + buffer->scroll_line = 0; + buffer->scroll_col = 0; +} + +static buffer_t *ecex_ensure_messages_buffer(ecex_t *ed) { + if (!ed) return NULL; + + buffer_t *buffer = ed->messages_buffer; + if (!buffer || ecex_buffer_index_of(ed, buffer, NULL) != ECEX_OK) { + buffer = ecex_find_buffer(ed, ECEX_MESSAGES_BUFFER_NAME); + } + + if (!buffer) { + buffer = ecex_create_buffer(ed, ECEX_MESSAGES_BUFFER_NAME, NULL, 0); + if (!buffer) return NULL; + } + + ed->messages_buffer = buffer; + buffer_set_interactive(buffer, 1); + if (buffer->major_mode == 0) { + ecex_buffer_set_major_mode_by_name(ed, buffer, "special-mode"); + } + buffer->read_only = 1; + buffer->modified = 0; + return buffer; +} + +static int ecex_messages_append(ecex_t *ed, const char *prefix, const char *message) { + if (!ed || ed->messages_append_active) return ECEX_ERR; + + buffer_t *buffer = ecex_ensure_messages_buffer(ed); + if (!buffer) return ECEX_ERR; + + int old_read_only = buffer->read_only; + int old_undo_disabled = buffer->undo_disabled; + ed->messages_append_active = 1; + buffer->read_only = 0; + buffer->undo_disabled = 1; + + int result = ECEX_OK; + if (prefix && prefix[0]) result = buffer_append(buffer, prefix); + if (result == ECEX_OK) result = buffer_append(buffer, message ? message : ""); + if (result == ECEX_OK) result = buffer_append(buffer, "\n"); + if (result == ECEX_OK) ecex_messages_trim(buffer); + + buffer->read_only = old_read_only; + buffer->undo_disabled = old_undo_disabled; + buffer->modified = 0; + buffer_clear_undo(buffer); + ed->messages_append_active = 0; + ecex_mark_ui_changed(ed); + return result; +} + +static void ecex_log_messages_sink(const char *line, int depth, void *userdata) { + ecex_t *ed = (ecex_t *)userdata; + char prefix[64]; + size_t pos = 0; + + if (!ed) return; + int written = snprintf(prefix, sizeof(prefix), "ecex-log: "); + if (written < 0) return; + pos = (size_t)written; + if (pos >= sizeof(prefix)) pos = sizeof(prefix) - 1; + for (int i = 0; i < depth && pos + 2 < sizeof(prefix); i++) { + prefix[pos++] = ' '; + prefix[pos++] = ' '; + } + prefix[pos < sizeof(prefix) ? pos : sizeof(prefix) - 1] = '\0'; + ecex_messages_append(ed, prefix, line); +} + buffer_t *ecex_create_buffer(ecex_t *ed, const char *name, const char *path, @@ -1382,6 +1486,7 @@ static int ecex_kill_buffer_impl(ecex_t *ed, const char *name, int force) { buffer_t *victim = ed->buffers[index]; if (ed->previous_buffer == victim) ed->previous_buffer = NULL; + if (ed->messages_buffer == victim) ed->messages_buffer = NULL; if (!force && victim->modified && !victim->read_only) { fprintf(stderr, "ecex: refusing to kill modified buffer '%s'; save it or use force-kill-buffer.\n", @@ -2100,6 +2205,7 @@ void ecex_message(ecex_t *ed, const char *message) { if (!ed) return; snprintf(ed->message, sizeof(ed->message), "%s", message ? message : ""); ed->message_revision++; + ecex_messages_append(ed, "message: ", message ? message : ""); ecex_mark_ui_changed(ed); } diff --git a/src/log.c b/src/log.c index 7dec534..78c384a 100644 --- a/src/log.c +++ b/src/log.c @@ -31,6 +31,22 @@ static char ecex_frame_group_start[1024]; static char ecex_frame_repeat_start[1024]; static char ecex_frame_repeat_end[1024]; static unsigned long ecex_frame_repeat_count = 0; +static ecex_log_sink_fn ecex_log_sink = NULL; +static void *ecex_log_sink_userdata = NULL; +static int ecex_log_sink_active = 0; + +void ecex_log_set_sink(ecex_log_sink_fn fn, void *userdata) { + ecex_log_sink = fn; + ecex_log_sink_userdata = userdata; +} + +void ecex_log_clear_sink(void *userdata) { + if (!userdata || userdata == ecex_log_sink_userdata) { + ecex_log_sink = NULL; + ecex_log_sink_userdata = NULL; + ecex_log_sink_active = 0; + } +} static size_t ecex_strn_copy(char *out, size_t out_cap, const char *in) { size_t i = 0; @@ -157,6 +173,12 @@ static void ecex_log_emit_raw_depth(const char *line, int depth) { } ecex_write_all(line ? line : "(null)", line ? len : 6); ecex_write_all("\n", 1); + + if (ecex_log_sink && !ecex_log_sink_active) { + ecex_log_sink_active = 1; + ecex_log_sink(line ? line : "(null)", depth, ecex_log_sink_userdata); + ecex_log_sink_active = 0; + } } static void ecex_log_emit_raw_counted(const char *line, int depth, unsigned long count) { diff --git a/src/path.c b/src/path.c index 1e002d7..be2b347 100644 --- a/src/path.c +++ b/src/path.c @@ -1,5 +1,7 @@ #include "path.h" +#include "util.h" + #include #include #include @@ -25,16 +27,12 @@ int ecex_path_copy(char *out, size_t out_size, const char *text) { char *ecex_path_expand_user(const char *path) { if (!path) return NULL; if (path[0] != '~' || (path[1] != '\0' && path[1] != '/')) { - char *copy = malloc(strlen(path) + 1); - if (copy) strcpy(copy, path); - return copy; + return ecex_strdup(path); } const char *home = getenv("HOME"); if (!home || !home[0]) { - char *copy = malloc(strlen(path) + 1); - if (copy) strcpy(copy, path); - return copy; + return ecex_strdup(path); } size_t home_len = strlen(home); @@ -49,9 +47,7 @@ char *ecex_path_expand_user(const char *path) { char *ecex_path_join(const char *dir, const char *name) { if (!name) return NULL; if (name[0] == '/') { - char *copy = malloc(strlen(name) + 1); - if (copy) strcpy(copy, name); - return copy; + return ecex_strdup(name); } if (!dir || !dir[0]) dir = "."; @@ -68,9 +64,7 @@ char *ecex_path_join(const char *dir, const char *name) { char *ecex_path_dirname(const char *path) { if (!path || !path[0]) { - char *dot = malloc(2); - if (dot) strcpy(dot, "."); - return dot; + return ecex_strdup("."); } char *expanded = ecex_path_expand_user(path); @@ -98,9 +92,7 @@ char *ecex_path_basename_dup(const char *path) { else if (slash && slash == path) base = "/"; else base = path; } - char *out = malloc(strlen(base) + 1); - if (out) strcpy(out, base); - return out; + return ecex_strdup(base); } char *ecex_path_normalize(const char *path) { diff --git a/tests/test_buffers.c b/tests/test_buffers.c new file mode 100644 index 0000000..7b073f4 --- /dev/null +++ b/tests/test_buffers.c @@ -0,0 +1,87 @@ +#include "test_core.h" + +#include "ecex.h" + +#include +#include +#include +#include + +void test_buffer_editing_and_undo(void) { + buffer_t *buffer = buffer_new("edit", NULL, 0); + assert(buffer); + + assert(buffer_insert(buffer, "hello") == 0); + assert(buffer_insert_char(buffer, '\n') == 0); + assert(buffer_insert(buffer, "world") == 0); + assert(strcmp(buffer->data, "hello\nworld") == 0); + assert(buffer->len == strlen("hello\nworld")); + assert(buffer_line_count(buffer) == 2); + + buffer_set_point(buffer, 5); + assert(buffer_insert(buffer, ",") == 0); + assert(strcmp(buffer->data, "hello,\nworld") == 0); + + assert(buffer_undo(buffer) == 0); + assert(strcmp(buffer->data, "hello\nworld") == 0); + assert(buffer_redo(buffer) == 0); + assert(strcmp(buffer->data, "hello,\nworld") == 0); + + buffer_free(buffer); +} + +void test_buffer_selection_and_search(void) { + buffer_t *buffer = buffer_new("select", NULL, 0); + assert(buffer); + + assert(buffer_set_text(buffer, "alpha beta gamma beta") == 0); + buffer_set_mark(buffer, 6); + buffer_set_point(buffer, 10); + assert(buffer_has_selection(buffer) == 1); + + size_t start = 0; + size_t end = 0; + buffer_selection_range(buffer, &start, &end); + assert(start == 6); + assert(end == 10); + + char *selection = buffer_substring(buffer, start, end); + assert(selection); + assert(strcmp(selection, "beta") == 0); + free(selection); + + assert(buffer_replace_selection(buffer, "delta") == 0); + assert(strcmp(buffer->data, "alpha delta gamma beta") == 0); + + size_t pos = 0; + assert(buffer_search_forward(buffer, "gamma", 0, &pos) == 0); + assert(pos == 12); + assert(buffer_search_backward(buffer, "alpha", buffer->len, &pos) == 0); + assert(pos == 0); + assert(buffer_search_forward(buffer, "missing", 0, &pos) != 0); + assert(buffer_undo(buffer) == 0); + assert(strcmp(buffer->data, "alpha beta gamma beta") == 0); + + buffer_free(buffer); +} + +void test_buffer_file_round_trip(void) { + const char *path = "/tmp/ecex-test-core-buffer.txt"; + remove(path); + + buffer_t *writer = buffer_new("writer", NULL, 0); + assert(writer); + assert(buffer_set_text(writer, "one\ntwo\n") == 0); + assert(buffer_save_as(writer, path) == 0); + assert(writer->modified == 0); + buffer_free(writer); + + buffer_t *reader = buffer_new("reader", path, 0); + assert(reader); + assert(buffer_load_file(reader, path) == 0); + assert(strcmp(reader->data, "one\ntwo\n") == 0); + assert(reader->modified == 0); + buffer_free(reader); + + remove(path); +} diff --git a/tests/test_completion_path.c b/tests/test_completion_path.c new file mode 100644 index 0000000..8170ee9 --- /dev/null +++ b/tests/test_completion_path.c @@ -0,0 +1,59 @@ +#include "test_core.h" + +#include "completion.h" +#include "ecex.h" + +#include +#include +#include + +void test_completion_helpers(void) { + assert(ecex_ascii_strncasecmp("Alpha", "alpha", 5) == 0); + assert(ecex_ascii_strncasecmp("Alpha", "Alpine", 4) != 0); + assert(ecex_ascii_contains_ci("compile-buffer", "PILE") == 1); + assert(ecex_ascii_contains_ci("compile-buffer", "missing") == 0); + + assert(ecex_fuzzy_score("find-file", "ff") > 0); + assert(ecex_fuzzy_score("find-file", "zz") < 0); + assert(ecex_fuzzy_score("find-file", "find") > ecex_fuzzy_score("other-find", "find")); + + ecex_completion_item_t items[] = { + {"z-file", 10, 0, 0}, + {"a-dir", 10, 1, 1}, + {"b-file", 30, 0, 2}, + }; + qsort(items, 3, sizeof(items[0]), ecex_completion_item_compare); + assert(strcmp(items[0].value, "b-file") == 0); + assert(strcmp(items[1].value, "a-dir") == 0); + assert(strcmp(items[2].value, "z-file") == 0); +} + +void test_path_helpers(void) { + char small[4]; + assert(ecex_path_copy(small, sizeof(small), "abcd") != 0); + assert(strcmp(small, "abc") == 0); + + char *joined = ecex_path_join("/tmp", "ecex-path-test.txt"); + assert(joined); + assert(strcmp(joined, "/tmp/ecex-path-test.txt") == 0); + free(joined); + + char *absolute = ecex_path_join("/tmp", "/var/log"); + assert(absolute); + assert(strcmp(absolute, "/var/log") == 0); + free(absolute); + + char *dir = ecex_path_dirname("/tmp/ecex/file.txt"); + assert(dir); + assert(strcmp(dir, "/tmp/ecex") == 0); + free(dir); + + char *base = ecex_path_basename_dup("/tmp/ecex/file.txt"); + assert(base); + assert(strcmp(base, "file.txt") == 0); + free(base); + + assert(ecex_path_is_image("photo.PNG") == 1); + assert(ecex_path_is_video("clip.MKV") == 1); + assert(ecex_path_is_media("notes.txt") == 0); +} diff --git a/tests/test_core.c b/tests/test_core.c deleted file mode 100644 index 42dfd99..0000000 --- a/tests/test_core.c +++ /dev/null @@ -1,88 +0,0 @@ -#include "ecex.h" - -#include -#include -#include - -static void test_plugin_identity(void) { - ecex_t ed; - memset(&ed, 0, sizeof(ed)); - ed.plugins = ecex_plugin_runtime_new(); - assert(ed.plugins); - - ecex_plugin_t *a = ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION); - assert(a); - assert(ecex_plugin_find(&ed, "alpha") == a); - assert(ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == 0); - assert(ecex_plugin_require(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == a); - assert(ecex_plugin_register(&ed, "Bad ID", ECEX_PLUGIN_API_VERSION) == 0); - assert(ecex_plugin_register(&ed, "beta", ECEX_PLUGIN_API_VERSION + 1) == 0); - - ecex_plugin_runtime_free(ed.plugins); -} - -static void test_plugin_slots_and_exports(void) { - ecex_t ed; - memset(&ed, 0, sizeof(ed)); - ed.plugins = ecex_plugin_runtime_new(); - assert(ed.plugins); - - ecex_plugin_t *owner = ecex_plugin_register(&ed, "owner", ECEX_PLUGIN_API_VERSION); - ecex_plugin_t *reader = ecex_plugin_register(&ed, "reader", ECEX_PLUGIN_API_VERSION); - assert(owner); - assert(reader); - - assert(ecex_plugin_slot_i32_set(owner, "score", 0, 42) == 0); - assert(ecex_plugin_slot_i32_get_scalar(owner, "score", -1) == 42); - assert(ecex_plugin_slot_i32_set_2d(owner, "board", 10, 3, 4, 99) == 0); - assert(ecex_plugin_slot_i32_get_2d(owner, "board", 10, 3, 4, -1) == 99); - assert(ecex_plugin_slot_i32_get_2d(owner, "board", 0, 3, 4, -7) == -7); - - int copied = 0; - size_t copied_len = 0; - assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) != 0); - assert(ecex_plugin_slot_set_export_flags(owner, "score", ECEX_PLUGIN_I32, ECEX_PLUGIN_EXPORT_READ) == 0); - assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) == 0); - assert(copied == 42); - assert(copied_len == sizeof(copied)); - - copied = 7; - assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, 0, &copied_len) != 0); - assert(copied == 7); - assert(copied_len == sizeof(copied)); - - ecex_plugin_runtime_free(ed.plugins); -} - -static void test_plugin_objects_and_text(void) { - ecex_t ed; - memset(&ed, 0, sizeof(ed)); - ed.plugins = ecex_plugin_runtime_new(); - assert(ed.plugins); - - ecex_plugin_t *plugin = ecex_plugin_register(&ed, "objects", ECEX_PLUGIN_API_VERSION); - assert(plugin); - - unsigned char *object = ecex_plugin_object_calloc(plugin, "state", 1, 16); - assert(object); - assert(ecex_plugin_object_i32_set(plugin, object, 4, 1234) == 0); - assert(ecex_plugin_object_i32_get(plugin, object, 4, -1) == 1234); - assert(ecex_plugin_object_i32_set(plugin, object, 14, 1) != 0); - assert(ecex_plugin_object_valid(plugin, object) == 1); - assert(ecex_plugin_object_free(plugin, object) == 0); - assert(ecex_plugin_object_valid(plugin, object) == 0); - - assert(ecex_plugin_text_set(plugin, 1, "hello", -1) == 0); - assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "hello") == 0); - assert(ecex_plugin_text_free_all(plugin) == 0); - assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "") == 0); - - ecex_plugin_runtime_free(ed.plugins); -} - -int main(void) { - test_plugin_identity(); - test_plugin_slots_and_exports(); - test_plugin_objects_and_text(); - return 0; -} diff --git a/tests/test_core.h b/tests/test_core.h new file mode 100644 index 0000000..a9dd52f --- /dev/null +++ b/tests/test_core.h @@ -0,0 +1,13 @@ +#ifndef ECEX_TEST_CORE_H +#define ECEX_TEST_CORE_H + +void test_plugin_identity(void); +void test_plugin_slots_and_exports(void); +void test_plugin_objects_and_text(void); +void test_buffer_editing_and_undo(void); +void test_buffer_selection_and_search(void); +void test_buffer_file_round_trip(void); +void test_completion_helpers(void); +void test_path_helpers(void); + +#endif diff --git a/tests/test_main.c b/tests/test_main.c new file mode 100644 index 0000000..6a8dc5f --- /dev/null +++ b/tests/test_main.c @@ -0,0 +1,13 @@ +#include "test_core.h" + +int main(void) { + test_plugin_identity(); + test_plugin_slots_and_exports(); + test_plugin_objects_and_text(); + test_buffer_editing_and_undo(); + test_buffer_selection_and_search(); + test_buffer_file_round_trip(); + test_completion_helpers(); + test_path_helpers(); + return 0; +} diff --git a/tests/test_plugin.c b/tests/test_plugin.c new file mode 100644 index 0000000..505d363 --- /dev/null +++ b/tests/test_plugin.c @@ -0,0 +1,83 @@ +#include "test_core.h" + +#include "ecex.h" + +#include +#include +#include + +void test_plugin_identity(void) { + ecex_t ed; + memset(&ed, 0, sizeof(ed)); + ed.plugins = ecex_plugin_runtime_new(); + assert(ed.plugins); + + ecex_plugin_t *a = ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION); + assert(a); + assert(ecex_plugin_find(&ed, "alpha") == a); + assert(ecex_plugin_register(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == 0); + assert(ecex_plugin_require(&ed, "alpha", ECEX_PLUGIN_API_VERSION) == a); + assert(ecex_plugin_register(&ed, "Bad ID", ECEX_PLUGIN_API_VERSION) == 0); + assert(ecex_plugin_register(&ed, "beta", ECEX_PLUGIN_API_VERSION + 1) == 0); + + ecex_plugin_runtime_free(ed.plugins); +} + +void test_plugin_slots_and_exports(void) { + ecex_t ed; + memset(&ed, 0, sizeof(ed)); + ed.plugins = ecex_plugin_runtime_new(); + assert(ed.plugins); + + ecex_plugin_t *owner = ecex_plugin_register(&ed, "owner", ECEX_PLUGIN_API_VERSION); + ecex_plugin_t *reader = ecex_plugin_register(&ed, "reader", ECEX_PLUGIN_API_VERSION); + assert(owner); + assert(reader); + + assert(ecex_plugin_slot_i32_set(owner, "score", 0, 42) == 0); + assert(ecex_plugin_slot_i32_get_scalar(owner, "score", -1) == 42); + assert(ecex_plugin_slot_i32_set_2d(owner, "board", 10, 3, 4, 99) == 0); + assert(ecex_plugin_slot_i32_get_2d(owner, "board", 10, 3, 4, -1) == 99); + assert(ecex_plugin_slot_i32_get_2d(owner, "board", 0, 3, 4, -7) == -7); + + int copied = 0; + size_t copied_len = 0; + assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) != 0); + assert(ecex_plugin_slot_set_export_flags(owner, "score", ECEX_PLUGIN_I32, ECEX_PLUGIN_EXPORT_READ) == 0); + assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, sizeof(copied), &copied_len) == 0); + assert(copied == 42); + assert(copied_len == sizeof(copied)); + + copied = 7; + assert(ecex_plugin_slot_read_exported(&ed, "owner", "score", &copied, 0, &copied_len) != 0); + assert(copied == 7); + assert(copied_len == sizeof(copied)); + + ecex_plugin_runtime_free(ed.plugins); +} + +void test_plugin_objects_and_text(void) { + ecex_t ed; + memset(&ed, 0, sizeof(ed)); + ed.plugins = ecex_plugin_runtime_new(); + assert(ed.plugins); + + ecex_plugin_t *plugin = ecex_plugin_register(&ed, "objects", ECEX_PLUGIN_API_VERSION); + assert(plugin); + + unsigned char *object = ecex_plugin_object_calloc(plugin, "state", 1, 16); + assert(object); + assert(ecex_plugin_object_i32_set(plugin, object, 4, 1234) == 0); + assert(ecex_plugin_object_i32_get(plugin, object, 4, -1) == 1234); + assert(ecex_plugin_object_i32_set(plugin, object, 14, 1) != 0); + assert(ecex_plugin_object_valid(plugin, object) == 1); + assert(ecex_plugin_object_free(plugin, object) == 0); + assert(ecex_plugin_object_valid(plugin, object) == 0); + + assert(ecex_plugin_text_set(plugin, 1, "hello", -1) == 0); + assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "hello") == 0); + assert(ecex_plugin_text_free_all(plugin) == 0); + assert(strcmp(ecex_plugin_text_get_drawable(&ed, plugin, 1), "") == 0); + + ecex_plugin_runtime_free(ed.plugins); +} -- cgit v1.2.3