From 2f4cb88d5c60f725323739300bb49dfa8923e7d5 Mon Sep 17 00:00:00 2001 From: Albert Cervin Date: Wed, 2 Nov 2022 22:20:04 +0100 Subject: =?UTF-8?q?=F0=9F=8E=89=20And=20so=20it=20begins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .envrc | 1 + .gitignore | 70 ++++++++++++++ LICENSE | 28 ++++++ Makefile | 50 ++++++++++ README.md | 2 + dged.nix | 25 +++++ flake.lock | 42 ++++++++ flake.nix | 16 +++ src/binding.c | 57 +++++++++++ src/binding.h | 43 +++++++++ src/buffer.c | 192 ++++++++++++++++++++++++++++++++++++ src/buffer.h | 68 +++++++++++++ src/command.c | 66 +++++++++++++ src/command.h | 34 +++++++ src/display.c | 100 +++++++++++++++++++ src/display.h | 32 ++++++ src/keyboard.c | 56 +++++++++++ src/keyboard.h | 28 ++++++ src/main.c | 166 ++++++++++++++++++++++++++++++++ src/text.c | 299 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/text.h | 39 ++++++++ src/utf8.c | 42 ++++++++ src/utf8.h | 17 ++++ test/assert.c | 20 ++++ test/assert.h | 10 ++ test/buffer.c | 46 +++++++++ test/main.c | 24 +++++ test/test.h | 11 +++ test/text.c | 82 ++++++++++++++++ test/utf8.c | 13 +++ 30 files changed, 1679 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 dged.nix create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/binding.c create mode 100644 src/binding.h create mode 100644 src/buffer.c create mode 100644 src/buffer.h create mode 100644 src/command.c create mode 100644 src/command.h create mode 100644 src/display.c create mode 100644 src/display.h create mode 100644 src/keyboard.c create mode 100644 src/keyboard.h create mode 100644 src/main.c create mode 100644 src/text.c create mode 100644 src/text.h create mode 100644 src/utf8.c create mode 100644 src/utf8.h create mode 100644 test/assert.c create mode 100644 test/assert.h create mode 100644 test/buffer.c create mode 100644 test/main.c create mode 100644 test/test.h create mode 100644 test/text.c create mode 100644 test/utf8.c diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..a5dbbcb --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..840af7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# obj file dir +/objs/ + +# exe +/dged +/run-tests + +# clangd things +/compile_commands.json + +# gdb +/.gdb_history + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +/result* + +.direnv/ +.cache/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2e90c37 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2022, Albert Cervin + +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. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +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 new file mode 100644 index 0000000..dfc2a3c --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +.PHONY: default clean check run debug debug-tests + +default: dged + +headers != find src/ -type f -name '*.h' +srcs != find src/ -type f -name '*.c' ! -name 'main.c' + +test_headers != find test/ -type f -name '*.h' +test_srcs != find test/ -type f -name '*.c' + +objs-path = objs +objs = $(patsubst %.c,$(objs-path)/%.o, $(srcs)) +test_objs = $(patsubst %.c,$(objs-path)/test/%.o, $(test_srcs)) + +UNAME_S != uname -s + +CFLAGS = -Werror -g -std=c99 + +ifeq ($(UNAME_S),Linux) + DEFINES += -DLINUX +endif + +$(objs-path)/test/%.o: %.c $(headers) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(DEFINES) -I ./src -I ./test -c $< -o $@ + +$(objs-path)/%.o: %.c $(headers) + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) $(DEFINES) -I ./src -c $< -o $@ + +dged: $(objs) $(objs-path)/src/main.o + $(CC) $(LDFLAGS) $(objs) $(objs-path)/src/main.o -o dged + +run-tests: $(test_objs) $(objs) + $(CC) $(LDFLAGS) $(test_objs) $(objs) -o run-tests + +check: run-tests + ./run-tests + +run: dged + ./dged + +debug: dged + gdb ./dged + +debug-tests: run-tests + gdb ./run-tests + +clean: + rm -rf $(objs-path) dged run-tests diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b5846f --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# DGED +A text editor for datagubbar! diff --git a/dged.nix b/dged.nix new file mode 100644 index 0000000..e443d63 --- /dev/null +++ b/dged.nix @@ -0,0 +1,25 @@ +{ stdenv +, clang-tools +, gnumake +, pkg-config +, tree-sitter +, bear +, lib +}: +stdenv.mkDerivation { + name = "dged"; + src = ./.; + + nativeBuildInputs = [ + gnumake + pkg-config + clang-tools + bear + ]; + + buildInputs = [ + tree-sitter + ]; + + hardeningDisable = [ "fortify" ]; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8c664f7 --- /dev/null +++ b/flake.lock @@ -0,0 +1,42 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1667318659, + "narHash": "sha256-mRXqCdlnxPgm3Wk7mNAOanl7B3Q3U5scYTEiyYmNEOE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b3a8f7ed267e0a7ed100eb7d716c9137ff120fe3", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.05", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e8486e0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,16 @@ +{ + description = "An editor for datagubbar"; + + inputs.nixpkgs.url = "nixpkgs/nixos-22.05"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages."${system}"; + in + { + packages.default = pkgs.callPackage ./dged.nix { }; + } + ); +} diff --git a/src/binding.c b/src/binding.c new file mode 100644 index 0000000..953c0d8 --- /dev/null +++ b/src/binding.c @@ -0,0 +1,57 @@ +#include "binding.h" +#include "command.h" + +#include +#include + +struct keymap keymap_create(const char *name, uint32_t capacity) { + return (struct keymap){ + .name = name, + .bindings = calloc(capacity, sizeof(struct binding)), + .nbindings = 0, + .capacity = capacity, + }; +} + +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings) { + if (keymap->nbindings + nbindings >= keymap->capacity) { + keymap->capacity = + nbindings > keymap->capacity * 2 ? nbindings * 2 : keymap->capacity * 2; + keymap->bindings = + realloc(keymap->bindings, sizeof(struct binding) * keymap->capacity); + } + memcpy(keymap->bindings + keymap->nbindings, bindings, + sizeof(struct binding) * nbindings); + + keymap->nbindings += nbindings; +} + +void keymap_destroy(struct keymap *keymap) { + free(keymap->bindings); + keymap->bindings = 0; + keymap->capacity = 0; + keymap->nbindings = 0; +} + +struct command *lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands) { + // lookup in order in the keymaps + for (uint32_t kmi = 0; kmi < nkeymaps; ++kmi) { + struct keymap *keymap = &keymaps[kmi]; + + for (uint32_t bi = 0; bi < keymap->nbindings; ++bi) { + struct binding *binding = &keymap->bindings[bi]; + if (key->c == binding->key.c && key->mod == binding->key.mod) { + if (binding->type == BindingType_Command) { + return lookup_command_by_hash(commands, binding->command); + } else if (binding->type == BindingType_Keymap) { + // TODO + return NULL; + } + } + } + } + + return NULL; +} diff --git a/src/binding.h b/src/binding.h new file mode 100644 index 0000000..260a463 --- /dev/null +++ b/src/binding.h @@ -0,0 +1,43 @@ +#include "keyboard.h" + +struct keymap { + const char *name; + struct binding *bindings; + uint32_t nbindings; + uint32_t capacity; +}; + +enum binding_type { BindingType_Command, BindingType_Keymap }; + +#define BINDING(mod_, c_, command_) \ + (struct binding) { \ + .key = {.mod = mod_, .c = c_}, .type = BindingType_Command, \ + .command = hash_command_name(command_) \ + } + +#define PREFIX(mod_, c_, keymap_) \ + (struct binding) { \ + .key = {.mod = mod_, .c = c_}, .type = BindingType_Keymap, \ + .keymap = keymap_ \ + } + +struct binding { + struct key key; + + uint8_t type; + + union { + uint32_t command; + struct keymap *keymap; + }; +}; + +struct commands; + +struct keymap keymap_create(const char *name, uint32_t capacity); +void keymap_bind_keys(struct keymap *keymap, struct binding *bindings, + uint32_t nbindings); +void keymap_destroy(struct keymap *keymap); + +struct command *lookup_key(struct keymap *keymaps, uint32_t nkeymaps, + struct key *key, struct commands *commands); diff --git a/src/buffer.c b/src/buffer.c new file mode 100644 index 0000000..e08bca5 --- /dev/null +++ b/src/buffer.c @@ -0,0 +1,192 @@ +#include "buffer.h" +#include "binding.h" +#include "display.h" + +#include +#include +#include +#include +#include + +struct buffer buffer_create(const char *name) { + struct buffer b = + (struct buffer){.filename = NULL, + .name = name, + .text = text_create(10), + .dot_col = 0, + .dot_line = 0, + .modeline_buf = (uint8_t *)malloc(1024), + .keymaps = calloc(10, sizeof(struct keymap)), + .nkeymaps = 1, + .lines_rendered = -1, + .nkeymaps_max = 10}; + + b.keymaps[0] = keymap_create("buffer-default", 128); + struct binding bindings[] = { + BINDING(Ctrl, 'B', "backward-char"), + BINDING(Ctrl, 'F', "forward-char"), + + BINDING(Ctrl, 'P', "backward-line"), + BINDING(Ctrl, 'N', "forward-line"), + + BINDING(Ctrl, 'A', "beginning-of-line"), + BINDING(Ctrl, 'E', "end-of-line"), + + BINDING(Ctrl, 'M', "newline"), + + BINDING(Ctrl, '?', "backward-delete-char"), + }; + keymap_bind_keys(&b.keymaps[0], bindings, + sizeof(bindings) / sizeof(bindings[0])); + + return b; +} + +void buffer_destroy(struct buffer *buffer) { + free(buffer->modeline_buf); + text_destroy(buffer->text); + free(buffer->text); +} + +uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out) { + *keymaps_out = buffer->keymaps; + return buffer->nkeymaps; +} + +void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap) { + if (buffer->nkeymaps == buffer->nkeymaps_max) { + // TODO: better + return; + } + buffer->keymaps[buffer->nkeymaps] = *keymap; + ++buffer->nkeymaps; +} + +bool movev(struct buffer *buffer, int rowdelta) { + int64_t new_line = (int64_t)buffer->dot_line + rowdelta; + + if (new_line < 0) { + buffer->dot_line = 0; + return false; + } else if (new_line > text_num_lines(buffer->text) - 1) { + buffer->dot_line = text_num_lines(buffer->text) - 1; + return false; + } else { + buffer->dot_line = (uint32_t)new_line; + + // make sure column stays on the line + uint32_t linelen = text_line_length(buffer->text, buffer->dot_line); + buffer->dot_col = buffer->dot_col > linelen ? linelen : buffer->dot_col; + return true; + } +} + +// move dot `coldelta` chars +void moveh(struct buffer *buffer, int coldelta) { + int64_t new_col = (int64_t)buffer->dot_col + coldelta; + + if (new_col > (int64_t)text_line_length(buffer->text, buffer->dot_line)) { + if (movev(buffer, 1)) { + buffer->dot_col = 0; + } + } else if (new_col < 0) { + if (movev(buffer, -1)) { + buffer->dot_col = text_line_length(buffer->text, buffer->dot_line); + } + } else { + buffer->dot_col = new_col; + } +} + +void buffer_backward_delete_char(struct buffer *buffer) { + // TODO: merge lines + if (text_line_length(buffer->text, buffer->dot_line) == 0) { + text_delete_line(buffer->text, buffer->dot_line); + } else if (buffer->dot_col > 0) { + text_delete(buffer->text, buffer->dot_line, buffer->dot_col - 1, 1); + } + moveh(buffer, -1); +} + +void buffer_backward_char(struct buffer *buffer) { moveh(buffer, -1); } +void buffer_forward_char(struct buffer *buffer) { moveh(buffer, 1); } + +void buffer_backward_line(struct buffer *buffer) { movev(buffer, -1); } +void buffer_forward_line(struct buffer *buffer) { movev(buffer, 1); } + +void buffer_end_of_line(struct buffer *buffer) { + buffer->dot_col = text_line_length(buffer->text, buffer->dot_line); +} + +void buffer_beginning_of_line(struct buffer *buffer) { buffer->dot_col = 0; } + +struct buffer buffer_from_file(const char *filename) { + // TODO: create a reader for the file that calls add_text + return (struct buffer){.filename = filename, .name = filename}; +} + +int buffer_to_file(struct buffer *buffer) { return 0; } + +int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes) { + uint32_t lines_added, cols_added; + text_append(buffer->text, buffer->dot_line, buffer->dot_col, text, nbytes, + &lines_added, &cols_added); + movev(buffer, lines_added); + moveh(buffer, cols_added); + + return lines_added; +} + +void buffer_newline(struct buffer *buffer) { + buffer_add_text(buffer, (uint8_t *)"\n", 1); +} + +bool modeline_update(struct buffer *buffer, uint32_t width) { + char buf[width * 4]; + + time_t now = time(NULL); + struct tm *lt = localtime(&now); + char left[128], right[128]; + snprintf(left, 128, "--- %-16s (%d, %d)", buffer->name, buffer->dot_line + 1, + buffer->dot_col); + snprintf(right, 128, "%02d:%02d", lt->tm_hour, lt->tm_min); + + snprintf(buf, width * 4, "\x1b[100m%s%*s%s\x1b[0m", left, + (int)(width - (strlen(left) + strlen(right))), "", right); + if (strcmp(buf, (char *)buffer->modeline_buf) != 0) { + buffer->modeline_buf = realloc(buffer->modeline_buf, width * 4); + strcpy((char *)buffer->modeline_buf, buf); + return true; + } else { + return false; + } +} + +struct buffer_update buffer_begin_frame(struct buffer *buffer, uint32_t width, + uint32_t height, alloc_fn frame_alloc) { + // reserve space for modeline + uint32_t bufheight = height - 1; + uint32_t nlines = + buffer->lines_rendered > bufheight ? bufheight : buffer->lines_rendered; + + struct render_cmd *cmds = + (struct render_cmd *)frame_alloc(sizeof(struct render_cmd) * (height)); + + uint32_t ncmds = text_render(buffer->text, 0, nlines, cmds, nlines); + + buffer->lines_rendered = text_num_lines(buffer->text); + + if (modeline_update(buffer, width)) { + cmds[ncmds] = (struct render_cmd){ + .col = 0, + .row = height - 1, + .data = buffer->modeline_buf, + .len = strlen((char *)buffer->modeline_buf), + }; + ++ncmds; + } + + return (struct buffer_update){.cmds = cmds, .ncmds = ncmds}; +} + +void buffer_end_frame(struct buffer *buffer, struct buffer_update *upd) {} diff --git a/src/buffer.h b/src/buffer.h new file mode 100644 index 0000000..1b73505 --- /dev/null +++ b/src/buffer.h @@ -0,0 +1,68 @@ +#include +#include + +#include "command.h" +#include "text.h" + +struct keymap; + +struct buffer { + const char *name; + const char *filename; + + struct text *text; + + uint32_t dot_line; + uint32_t dot_col; + + uint8_t *modeline_buf; + + // local keymaps + struct keymap *keymaps; + uint32_t nkeymaps; + uint32_t nkeymaps_max; + + uint32_t lines_rendered; +}; + +struct buffer_update { + struct render_cmd *cmds; + uint64_t ncmds; +}; + +typedef void *(alloc_fn)(size_t); + +struct buffer buffer_create(const char *name); +void buffer_destroy(struct buffer *buffer); + +uint32_t buffer_keymaps(struct buffer *buffer, struct keymap **keymaps_out); +void buffer_add_keymap(struct buffer *buffer, struct keymap *keymap); + +int buffer_add_text(struct buffer *buffer, uint8_t *text, uint32_t nbytes); + +void buffer_backward_delete_char(struct buffer *buffer); +void buffer_backward_char(struct buffer *buffer); +void buffer_forward_char(struct buffer *buffer); +void buffer_backward_line(struct buffer *buffer); +void buffer_forward_line(struct buffer *buffer); +void buffer_end_of_line(struct buffer *buffer); +void buffer_beginning_of_line(struct buffer *buffer); +void buffer_newline(struct buffer *buffer); + +struct buffer buffer_from_file(const char *filename); +int buffer_to_file(struct buffer *buffer); + +struct buffer_update buffer_begin_frame(struct buffer *buffer, uint32_t width, + uint32_t height, alloc_fn frame_alloc); +void buffer_end_frame(struct buffer *buffer, struct buffer_update *upd); + +static struct command BUFFER_COMMANDS[] = { + {.name = "backward-delete-char", .fn = buffer_backward_delete_char}, + {.name = "backward-char", .fn = buffer_backward_char}, + {.name = "forward-char", .fn = buffer_forward_char}, + {.name = "backward-line", .fn = buffer_backward_line}, + {.name = "forward-line", .fn = buffer_forward_line}, + {.name = "end-of-line", .fn = buffer_end_of_line}, + {.name = "beginning-of-line", .fn = buffer_beginning_of_line}, + {.name = "newline", .fn = buffer_newline}, +}; diff --git a/src/command.c b/src/command.c new file mode 100644 index 0000000..4b233b2 --- /dev/null +++ b/src/command.c @@ -0,0 +1,66 @@ +#include "command.h" + +#include + +struct commands command_list_create(uint32_t capacity) { + return (struct commands){ + .commands = calloc(capacity, sizeof(struct hashed_command)), + .ncommands = 0, + .capacity = capacity, + }; +} + +void command_list_destroy(struct commands *commands) { + free(commands->commands); + commands->ncommands = 0; + commands->capacity = 0; +} + +uint32_t hash_command_name(const char *name) { + unsigned long hash = 5381; + int c; + + while ((c = *name++)) + hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ + + return hash; +} + +uint32_t register_command(struct commands *commands, struct command *command) { + if (commands->ncommands == commands->capacity) { + commands->capacity *= 2; + commands->commands = realloc( + commands->commands, sizeof(struct hashed_command) * commands->capacity); + } + + uint32_t hash = hash_command_name(command->name); + commands->commands[commands->ncommands] = + (struct hashed_command){.command = command, .hash = hash}; + + ++commands->ncommands; + return hash; +} + +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands) { + for (uint32_t ci = 0; ci < ncommands; ++ci) { + register_command(command_list, &commands[ci]); + } +} + +struct command *lookup_command(struct commands *command_list, + const char *name) { + uint32_t needle = hash_command_name(name); + return lookup_command_by_hash(command_list, needle); +} + +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash) { + for (uint32_t ci = 0; ci < commands->ncommands; ++ci) { + if (commands->commands[ci].hash == hash) { + return commands->commands[ci].command; + } + } + + return NULL; +} diff --git a/src/command.h b/src/command.h new file mode 100644 index 0000000..9515282 --- /dev/null +++ b/src/command.h @@ -0,0 +1,34 @@ +#include + +struct buffer; + +typedef void (*command_fn)(struct buffer *buffer); + +struct command { + const char *name; + command_fn fn; +}; + +struct hashed_command { + uint32_t hash; + struct command *command; +}; + +struct commands { + struct hashed_command *commands; + uint32_t ncommands; + uint32_t capacity; +}; + +struct commands command_list_create(uint32_t capacity); +void command_list_destroy(struct commands *commands); + +uint32_t register_command(struct commands *commands, struct command *command); +void register_commands(struct commands *command_list, struct command *commands, + uint32_t ncommands); + +uint32_t hash_command_name(const char *name); + +struct command *lookup_command(struct commands *commands, const char *name); +struct command *lookup_command_by_hash(struct commands *commands, + uint32_t hash); diff --git a/src/display.c b/src/display.c new file mode 100644 index 0000000..b34cbf1 --- /dev/null +++ b/src/display.c @@ -0,0 +1,100 @@ +#define _DEFAULT_SOURCE +#include "display.h" + +#include "buffer.h" + +#include +#include +#include + +#define ESC 0x1b + +struct display display_create() { + + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) { + // TODO: if it fails to fetch, do something? + return (struct display){ + .height = 0, + .width = 0, + }; + } + + // save old settings + struct termios orig_term; + tcgetattr(0, &orig_term); + + // set terminal to raw mode + struct termios term; + cfmakeraw(&term); + // non-blocking input + // TODO: move to kbd? + term.c_cc[VMIN] = 0; + term.c_cc[VTIME] = 0; + + tcsetattr(0, TCSADRAIN, &term); + + return (struct display){ + .orig_term = orig_term, + .term = term, + .height = ws.ws_row, + .width = ws.ws_col, + }; +} + +void display_destroy(struct display *display) { + // reset old terminal mode + tcsetattr(0, TCSADRAIN, &display->orig_term); +} + +void putbytes(uint8_t *line_bytes, uint32_t line_length) { + fwrite(line_bytes, 1, line_length, stdout); +} + +void putbyte(uint8_t c) { putc(c, stdout); } + +void put_ansiparm(int n) { + int q = n / 10; + if (q != 0) { + int r = q / 10; + if (r != 0) { + putbyte((r % 10) + '0'); + } + putbyte((q % 10) + '0'); + } + putbyte((n % 10) + '0'); +} + +void display_move_cursor(struct display *display, uint32_t row, uint32_t col) { + putbyte(ESC); + putbyte('['); + put_ansiparm(row + 1); + putbyte(';'); + put_ansiparm(col + 1); + putbyte('H'); +} + +void display_clear(struct display *display) { + display_move_cursor(display, 0, 0); + uint8_t bytes[] = {ESC, '[', 'J'}; + putbytes(bytes, 3); +} + +void delete_to_eol() { + uint8_t bytes[] = {ESC, '[', 'K'}; + putbytes(bytes, 3); +} + +void display_update(struct display *display, struct render_cmd *cmds, + uint32_t ncmds, uint32_t currow, uint32_t curcol) { + for (uint64_t cmdi = 0; cmdi < ncmds; ++cmdi) { + struct render_cmd *cmd = &cmds[cmdi]; + display_move_cursor(display, cmd->row, cmd->col); + putbytes(cmd->data, cmd->len); + delete_to_eol(); + } + + display_move_cursor(display, currow, curcol); + + fflush(stdout); +} diff --git a/src/display.h b/src/display.h new file mode 100644 index 0000000..18200d7 --- /dev/null +++ b/src/display.h @@ -0,0 +1,32 @@ +#include + +#include + +struct display { + struct termios term; + struct termios orig_term; + uint32_t width; + uint32_t height; +}; + +struct render_cmd { + uint32_t col; + uint32_t row; + + uint8_t *data; + uint32_t len; +}; + +struct render_cmd_buf { + char source[16]; + struct render_cmd *cmds; + uint64_t ncmds; +}; + +struct display display_create(); +void display_destroy(struct display *display); + +void display_clear(struct display *display); +void display_move_cursor(struct display *display, uint32_t row, uint32_t col); +void display_update(struct display *display, struct render_cmd *cmds, + uint32_t ncmds, uint32_t currow, uint32_t curcol); diff --git a/src/keyboard.c b/src/keyboard.c new file mode 100644 index 0000000..2cf9e8c --- /dev/null +++ b/src/keyboard.c @@ -0,0 +1,56 @@ +#include "keyboard.h" + +#include +#include + +struct keyboard keyboard_create() { + // TODO: should input term stuff be set here? + return (struct keyboard){}; +} + +void parse_keys(uint8_t *bytes, uint32_t nbytes, struct key *out_keys, + uint32_t *out_nkeys) { + uint32_t nkps = 0; + for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { + uint8_t b = bytes[bytei]; + + struct key *kp = &out_keys[nkps]; + + if (b == 0x1b) { // meta + kp->mod |= Meta; + } else if (b >= 0x00 && b <= 0x1f) { // ctrl char + kp->mod |= Ctrl; + kp->c = b | 0x40; + + } else if (b == 0x7f) { // ^? + kp->mod |= Ctrl; + kp->c = '?'; + } else { // normal char (or part of char) + kp->c = b; + } + + ++nkps; + } + + *out_nkeys = nkps; +} + +struct keyboard_update keyboard_begin_frame(struct keyboard *kbd) { + uint8_t bytes[32] = {0}; + int nbytes = read(STDIN_FILENO, bytes, 32); + + struct keyboard_update upd = + (struct keyboard_update){.keys = {0}, .nkeys = 0}; + + if (nbytes > 0) { + parse_keys(bytes, nbytes, upd.keys, &upd.nkeys); + } + + return upd; +} + +void keyboard_end_frame(struct keyboard *kbd) {} + +bool key_equal(struct key *key, uint8_t mod, uint8_t c) { + return key->c == c && key->mod == mod; +} diff --git a/src/keyboard.h b/src/keyboard.h new file mode 100644 index 0000000..439e60d --- /dev/null +++ b/src/keyboard.h @@ -0,0 +1,28 @@ +#include +#include + +enum modifiers { + Ctrl = 1 << 0, + Meta = 1 << 1, +}; + +// note that unicode chars are split over multiple keypresses +// TODO: make unicode chars nicer to deal with +struct key { + uint8_t c; + uint8_t mod; +}; + +struct keyboard {}; + +struct keyboard_update { + struct key keys[32]; + uint32_t nkeys; +}; + +struct keyboard keyboard_create(); + +struct keyboard_update keyboard_begin_frame(struct keyboard *kbd); +void keyboard_end_frame(struct keyboard *kbd); + +bool key_equal(struct key *key, uint8_t mod, uint8_t c); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..140b281 --- /dev/null +++ b/src/main.c @@ -0,0 +1,166 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "binding.h" +#include "buffer.h" +#include "display.h" + +struct reactor { + int epoll_fd; +}; + +struct reactor reactor_create(); +int reactor_register_interest(struct reactor *reactor, int fd); + +struct frame_allocator { + uint8_t *buf; + size_t offset; + size_t capacity; +}; + +struct frame_allocator frame_allocator_create(size_t capacity) { + return (struct frame_allocator){ + .capacity = capacity, .offset = 0, .buf = (uint8_t *)malloc(capacity)}; +} + +void *frame_allocator_alloc(struct frame_allocator *alloc, size_t sz) { + if (alloc->offset + sz > alloc->capacity) { + return NULL; + } + + void *mem = alloc->buf + alloc->offset; + alloc->offset += sz; + + return mem; +} + +void frame_allocator_clear(struct frame_allocator *alloc) { alloc->offset = 0; } + +struct frame_allocator frame_allocator; + +void *frame_alloc(size_t sz) { + return frame_allocator_alloc(&frame_allocator, sz); +} + +bool running = true; + +void terminate() { running = false; } + +void unimplemented_command(struct buffer *buffer) {} +void exit_editor(struct buffer *buffer) { terminate(); } + +static struct command GLOBAL_COMMANDS[] = { + {.name = "find-file", .fn = unimplemented_command}, + {.name = "exit", .fn = exit_editor}}; + +int main(int argc, char *argv[]) { + const char *filename = NULL; + if (argc >= 1) { + filename = argv[1]; + } + + setlocale(LC_ALL, ""); + signal(SIGTERM, terminate); + + frame_allocator = frame_allocator_create(1024 * 1024); + + // create reactor + struct reactor reactor = reactor_create(); + + // initialize display + struct display display = display_create(); + display_clear(&display); + + // init keyboard + struct keyboard kbd = keyboard_create(); + + // commands + struct commands commands = command_list_create(32); + register_commands(&commands, GLOBAL_COMMANDS, + sizeof(GLOBAL_COMMANDS) / sizeof(GLOBAL_COMMANDS[0])); + register_commands(&commands, BUFFER_COMMANDS, + sizeof(BUFFER_COMMANDS) / sizeof(BUFFER_COMMANDS[0])); + + // keymaps + struct keymap global_keymap = keymap_create("global", 32); + struct binding global_binds[] = { + BINDING(Ctrl, 'X', "exit"), + }; + keymap_bind_keys(&global_keymap, global_binds, + sizeof(global_binds) / sizeof(global_binds[0])); + + // TODO: load initial buffer + struct buffer curbuf = buffer_create("welcome"); + const char *welcome_txt = "Welcome to the editor for datagubbar šŸ‘“\n"; + buffer_add_text(&curbuf, (uint8_t *)welcome_txt, strlen(welcome_txt)); + + while (running) { + // update keyboard + struct keymap *local_keymaps = NULL; + uint32_t nbuffer_keymaps = buffer_keymaps(&curbuf, &local_keymaps); + struct keyboard_update kbd_upd = keyboard_begin_frame(&kbd); + for (uint32_t ki = 0; ki < kbd_upd.nkeys; ++ki) { + struct key *k = &kbd_upd.keys[ki]; + + // check first the global keymap, then the buffer ones + struct command *cmd = lookup_key(&global_keymap, 1, k, &commands); + if (cmd == NULL) { + cmd = lookup_key(local_keymaps, nbuffer_keymaps, k, &commands); + } + + if (cmd != NULL) { + cmd->fn(&curbuf); + } else { + buffer_add_text(&curbuf, &k->c, 1); + } + } + + // update current buffer + struct buffer_update buf_upd = buffer_begin_frame( + &curbuf, display.width, display.height - 1, frame_alloc); + + // update screen + if (buf_upd.ncmds > 0) { + display_update(&display, buf_upd.cmds, buf_upd.ncmds, curbuf.dot_line, + curbuf.dot_col); + } + + buffer_end_frame(&curbuf, &buf_upd); + frame_allocator_clear(&frame_allocator); + } + + display_clear(&display); + display_destroy(&display); + keymap_destroy(&global_keymap); + command_list_destroy(&commands); + + return 0; +} + +struct reactor reactor_create() { + int epollfd = epoll_create1(0); + if (epollfd == -1) { + perror("epoll_create1"); + } + + return (struct reactor){.epoll_fd = epollfd}; +} + +int reactor_register_interest(struct reactor *reactor, int fd) { + struct epoll_event ev; + ev.events = EPOLLIN; + ev.data.fd = fd; + if (epoll_ctl(reactor->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + perror("epoll_ctl"); + return -1; + } + + return fd; +} diff --git a/src/text.c b/src/text.c new file mode 100644 index 0000000..4162e94 --- /dev/null +++ b/src/text.c @@ -0,0 +1,299 @@ +#include "text.h" + +#include +#include +#include + +#include "display.h" +#include "utf8.h" + +enum flags { + LineChanged = 1 << 0, +}; + +struct line { + uint8_t *data; + uint8_t flags; + uint32_t nbytes; + uint32_t nchars; +}; + +struct text { + // raw bytes without any null terminators + struct line *lines; + uint32_t nlines; + uint32_t capacity; +}; + +struct text *text_create(uint32_t initial_capacity) { + struct text *txt = calloc(1, sizeof(struct text)); + txt->lines = calloc(initial_capacity, sizeof(struct line)); + txt->capacity = initial_capacity; + + // we always have one line, since add line adds a second one + txt->nlines = 1; + + return txt; +} + +void text_destroy(struct text *text) { + for (uint32_t li = 0; li < text->nlines; ++li) { + free(text->lines[li].data); + text->lines[li].data = NULL; + text->lines[li].flags = 0; + text->lines[li].nbytes = 0; + text->lines[li].nchars = 0; + } + + free(text->lines); +} + +void append_to_line(struct line *line, uint32_t col, uint8_t *text, + uint32_t len, uint32_t nchars) { + + if (len == 0) { + return; + } + + line->nbytes += len; + line->nchars += nchars; + line->flags = LineChanged; + line->data = realloc(line->data, line->nbytes + len); + + // move chars out of the way + memmove(line->data + col + 1, line->data + col, line->nbytes - (col + 1)); + + // insert new chars + memcpy(line->data + col, text, len); +} + +uint32_t text_line_length(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nchars; +} + +uint32_t text_line_size(struct text *text, uint32_t lineidx) { + return text->lines[lineidx].nbytes; +} + +uint32_t text_num_lines(struct text *text) { return text->nlines; } + +// given `char_idx` as a character index, return the byte index +uint32_t charidx_to_byteidx(struct line *line, uint32_t char_idx) { + if (char_idx > line->nchars) { + return line->nchars; + } + return utf8_nbytes(line->data, char_idx); +} + +// TODO: grapheme clusters +// given `byte_idx` as a byte index, return the character index +uint32_t byteidx_to_charidx(struct line *line, uint32_t byte_idx) { + if (byte_idx > line->nbytes) { + return line->nchars; + } + + return utf8_nchars(line->data, byte_idx); +} + +uint32_t char_byte_size(struct line *line, uint32_t byte_idx) { + return utf8_nbytes(line->data + byte_idx, 1); +} + +void split_line(uint32_t col, struct line *line, struct line *next) { + uint8_t *data = line->data; + uint32_t nbytes = line->nbytes; + uint32_t nchars = line->nchars; + + uint32_t chari = col; + uint32_t bytei = charidx_to_byteidx(line, chari); + + line->nbytes = bytei; + line->nchars = chari; + next->nbytes = nbytes - bytei; + next->nchars = nchars - chari; + line->flags = next->flags = line->flags; + + // first, handle some cases where the new line or the pre-existing one is + // empty + if (next->nbytes == 0) { + line->data = data; + } else if (line->nbytes == 0) { + next->data = data; + } else { + // actually split the line + next->data = (uint8_t *)malloc(next->nbytes); + memcpy(next->data, data + bytei, next->nbytes); + + line->data = (uint8_t *)malloc(line->nbytes); + memcpy(line->data, data, line->nbytes); + + free(data); + } +} + +void mark_lines_changed(struct text *text, uint32_t line, uint32_t nlines) { + for (uint32_t linei = line; linei < (line + nlines); ++linei) { + text->lines[linei].flags |= LineChanged; + } +} + +void shift_lines(struct text *text, uint32_t start, int32_t direction) { + struct line *dest = text->lines + ((int64_t)start + direction); + struct line *src = text->lines + start; + uint32_t nlines = text->nlines - (dest > src ? (start + direction) : start); + memmove(dest, src, nlines * sizeof(struct line)); +} + +void new_line_at(struct text *text, uint32_t line, uint32_t col) { + if (text->nlines == text->capacity) { + text->capacity *= 2; + text->lines = realloc(text->lines, sizeof(struct line) * text->capacity); + } + + struct line *nline = &text->lines[text->nlines]; + nline->data = NULL; + nline->nbytes = 0; + nline->nchars = 0; + nline->flags = 0; + + ++text->nlines; + + mark_lines_changed(text, line, text->nlines - line); + + // move following lines out of the way + shift_lines(text, line + 1, 1); + + // split line if needed + struct line *pl = &text->lines[line]; + struct line *cl = &text->lines[line + 1]; + split_line(col, pl, cl); +} + +void text_delete_line(struct text *text, uint32_t line) { + // always keep a single line + if (text->nlines == 1) { + return; + } + + mark_lines_changed(text, line, text->nlines - line); + free(text->lines[line].data); + text->lines[line].data = NULL; + + shift_lines(text, line + 1, -1); + + if (text->nlines > 0) { + --text->nlines; + text->lines[text->nlines].data = NULL; + text->lines[text->nlines].nbytes = 0; + text->lines[text->nlines].nchars = 0; + } +} + +void text_append(struct text *text, uint32_t line, uint32_t col, uint8_t *bytes, + uint32_t nbytes, uint32_t *lines_added, uint32_t *cols_added) { + uint32_t linelen = 0; + uint32_t nchars_counted = 0; + uint32_t nlines_added = 0; + uint32_t ncols_added = 0; + for (uint32_t bytei = 0; bytei < nbytes; ++bytei) { + uint8_t byte = bytes[bytei]; + if (byte == '\n') { + append_to_line(&text->lines[line], col, bytes + (bytei - linelen), + linelen, nchars_counted); + + col += nchars_counted; + new_line_at(text, line, col); + ++line; + ++nlines_added; + + col = text_line_length(text, line); + linelen = 0; + nchars_counted = 0; + } else { + if (utf8_byte_is_ascii(byte) || utf8_byte_is_unicode_start(byte)) { + ++nchars_counted; + } + ++linelen; + } + } + + // handle remaining + if (linelen > 0) { + append_to_line(&text->lines[line], col, bytes + (nbytes - linelen), linelen, + nchars_counted); + ncols_added = nchars_counted; + } + + *lines_added = nlines_added; + *cols_added = ncols_added; +} + +void text_delete(struct text *text, uint32_t line, uint32_t col, + uint32_t nchars) { + struct line *lp = &text->lines[line]; + uint32_t max_chars = nchars > lp->nchars ? lp->nchars : nchars; + if (lp->nchars > 0) { + + // get byte index and size for char to remove + uint32_t bytei = charidx_to_byteidx(lp, col); + uint32_t nbytes = utf8_nbytes(lp->data + bytei, max_chars); + + memcpy(lp->data + bytei, lp->data + bytei + nbytes, + lp->nbytes - (bytei + nbytes)); + + lp->nbytes -= nbytes; + lp->nchars -= max_chars; + lp->flags |= LineChanged; + } +} + +uint32_t text_render(struct text *text, uint32_t line, uint32_t nlines, + struct render_cmd *cmds, uint32_t max_ncmds) { + uint32_t nlines_max = nlines > text->capacity ? text->capacity : nlines; + + uint32_t ncmds = 0; + for (uint32_t lineidx = line; lineidx < nlines_max; ++lineidx) { + struct line *lp = &text->lines[lineidx]; + if (lp->flags & LineChanged) { + + cmds[ncmds] = (struct render_cmd){ + .row = lineidx, + .col = 0, // TODO: do not redraw full line + .data = lp->data, + .len = lp->nbytes, + }; + + lp->flags &= ~(LineChanged); + + ++ncmds; + } + } + + return ncmds; +} + +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + line_cb callback) { + for (uint32_t li = line; li < (line + nlines); ++li) { + struct line *src_line = &text->lines[li]; + struct txt_line line = (struct txt_line){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + }; + callback(&line); + } +} + +struct txt_line text_get_line(struct text *text, uint32_t line) { + struct line *src_line = &text->lines[line]; + return (struct txt_line){ + .text = src_line->data, + .nbytes = src_line->nbytes, + .nchars = src_line->nchars, + }; +} + +bool text_line_contains_unicode(struct text *text, uint32_t line) { + return text->lines[line].nbytes != text->lines[line].nchars; +} diff --git a/src/text.h b/src/text.h new file mode 100644 index 0000000..21de899 --- /dev/null +++ b/src/text.h @@ -0,0 +1,39 @@ +#include +#include +#include + +// opaque so it is easier to change representation to gap, rope etc. +struct text; + +struct render_cmd; + +struct text *text_create(uint32_t initial_capacity); +void text_destroy(struct text *text); + +void text_append(struct text *text, uint32_t line, uint32_t col, uint8_t *bytes, + uint32_t nbytes, uint32_t *lines_added, uint32_t *cols_added); + +void text_delete(struct text *text, uint32_t line, uint32_t col, + uint32_t nchars); +void text_delete_line(struct text *text, uint32_t line); + +uint32_t text_render(struct text *text, uint32_t line, uint32_t nlines, + struct render_cmd *cmds, uint32_t max_ncmds); + +uint32_t text_num_lines(struct text *text); +uint32_t text_line_length(struct text *text, uint32_t lineidx); +uint32_t text_line_size(struct text *text, uint32_t lineidx); + +struct txt_line { + uint8_t *text; + uint32_t nbytes; + uint32_t nchars; +}; + +typedef void (*line_cb)(struct txt_line *line); +void text_for_each_line(struct text *text, uint32_t line, uint32_t nlines, + line_cb callback); + +struct txt_line text_get_line(struct text *text, uint32_t line); + +bool text_line_contains_unicode(struct text *text, uint32_t line); diff --git a/src/utf8.c b/src/utf8.c new file mode 100644 index 0000000..3afef40 --- /dev/null +++ b/src/utf8.c @@ -0,0 +1,42 @@ +#include "utf8.h" + +#include + +bool utf8_byte_is_unicode_start(uint8_t byte) { return (byte & 0xc0) == 0xc0; } +bool utf8_byte_is_unicode_continuation(uint8_t byte) { + return utf8_byte_is_unicode(byte) && !utf8_byte_is_unicode_start(byte); +} +bool utf8_byte_is_unicode(uint8_t byte) { return (byte & 0x80) != 0x0; } +bool utf8_byte_is_ascii(uint8_t byte) { return !utf8_byte_is_unicode(byte); } + +// TODO: grapheme clusters, this returns the number of unicode code points +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes) { + uint32_t nchars = 0; + for (uint32_t bi = 0; bi < nbytes; ++bi) { + if (utf8_byte_is_ascii(bytes[bi]) || utf8_byte_is_unicode_start(bytes[bi])) + ++nchars; + } + return nchars; +} + +// TODO: grapheme clusters, this uses the number of unicode code points +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nchars) { + uint32_t bi = 0; + uint32_t chars = 0; + while (chars < nchars) { + uint8_t byte = bytes[bi]; + if (utf8_byte_is_unicode_start(byte)) { + ++chars; + + // length of char is the number of leading ones + // flip it and count number of leading zeros + uint8_t invb = ~byte; + bi += __builtin_clz((uint32_t)invb) - 24; + } else { + ++chars; + ++bi; + } + } + + return bi; +} diff --git a/src/utf8.h b/src/utf8.h new file mode 100644 index 0000000..901b1af --- /dev/null +++ b/src/utf8.h @@ -0,0 +1,17 @@ +#include +#include + +/*! + * \brief Return the number of chars the utf-8 sequence pointed at by `bytes` of + * length `nbytes`, represents + */ +uint32_t utf8_nchars(uint8_t *bytes, uint32_t nbytes); + +/* Return the number of bytes used to make up the next `nchars` characters */ +uint32_t utf8_nbytes(uint8_t *bytes, uint32_t nchars); + +/* true if `byte` is a unicode byte sequence start byte */ +bool utf8_byte_is_unicode_start(uint8_t byte); +bool utf8_byte_is_unicode_continuation(uint8_t byte); +bool utf8_byte_is_ascii(uint8_t byte); +bool utf8_byte_is_unicode(uint8_t byte); diff --git a/test/assert.c b/test/assert.c new file mode 100644 index 0000000..b252d36 --- /dev/null +++ b/test/assert.c @@ -0,0 +1,20 @@ +#include "assert.h" + +#include +#include +#include +#include + +void assert(bool cond, const char *cond_str, const char *file, int line, + const char *msg) { + if (!cond) { + printf("\n%s:%d: assert failed (%s): %s\n", file, line, cond_str, msg); + raise(SIGABRT); + } +} + +void assert_streq(const char *left, const char *right, const char *file, + int line, const char *msg) { + assert(strcmp(left, right) == 0, " == ", file, + line, msg); +} diff --git a/test/assert.h b/test/assert.h new file mode 100644 index 0000000..8b730b2 --- /dev/null +++ b/test/assert.h @@ -0,0 +1,10 @@ +#include + +#define ASSERT(cond, msg) assert(cond, #cond, __FILE__, __LINE__, msg) +#define ASSERT_STR_EQ(left, right, msg) \ + assert_streq(left, right, __FILE__, __LINE__, msg) + +void assert(bool cond, const char *cond_str, const char *file, int line, + const char *msg); +void assert_streq(const char *left, const char *right, const char *file, + int line, const char *msg); diff --git a/test/buffer.c b/test/buffer.c new file mode 100644 index 0000000..d7d9b0b --- /dev/null +++ b/test/buffer.c @@ -0,0 +1,46 @@ +#include "assert.h" +#include "test.h" + +#include "buffer.h" + +#include + +void test_move() { + struct buffer b = buffer_create("test-buffer"); + ASSERT(b.dot_col == 0 && b.dot_line == 0, + "Expected dot to be at buffer start"); + + // make sure we cannot move now + buffer_backward_char(&b); + buffer_backward_line(&b); + ASSERT(b.dot_col == 0 && b.dot_line == 0, + "Expected to not be able to move backward in empty buffer"); + + buffer_forward_char(&b); + buffer_forward_line(&b); + ASSERT(b.dot_col == 0 && b.dot_line == 0, + "Expected to not be able to move forward in empty buffer"); + + // add some text and try again + const char *txt = "testing movement"; + int lineindex = buffer_add_text(&b, (uint8_t *)txt, strlen(txt)); + ASSERT(lineindex + 1 == 1, "Expected buffer to have one line"); + + buffer_beginning_of_line(&b); + buffer_forward_char(&b); + ASSERT(b.dot_col == 1 && b.dot_line == 0, + "Expected to be able to move forward by one char"); + + // now we have two lines + const char *txt2 = "\n"; + int lineindex2 = buffer_add_text(&b, (uint8_t *)txt2, strlen(txt2)); + ASSERT(lineindex2 + 1 == 2, "Expected buffer to have two lines"); + buffer_backward_line(&b); + buffer_beginning_of_line(&b); + buffer_backward_char(&b); + ASSERT( + b.dot_col == 0 && b.dot_line == 0, + "Expected to not be able to move backwards when at beginning of buffer"); +} + +void run_buffer_tests() { run_test(test_move); } diff --git a/test/main.c b/test/main.c new file mode 100644 index 0000000..f124f0c --- /dev/null +++ b/test/main.c @@ -0,0 +1,24 @@ +#include +#include +#include + +#include "test.h" + +void handle_abort() { exit(1); } + +int main() { + setlocale(LC_ALL, ""); + signal(SIGABRT, handle_abort); + + printf("\nšŸŒ \x1b[1;36mRunning utf8 tests...\x1b[0m\n"); + run_utf8_tests(); + + printf("\nšŸ“œ \x1b[1;36mRunning text tests...\x1b[0m\n"); + run_text_tests(); + + printf("\nšŸ•“ļø \x1b[1;36mRunning buffer tests...\x1b[0m\n"); + run_buffer_tests(); + + printf("\nšŸŽ‰ \x1b[1;32mDone! All tests successful!\x1b[0m\n"); + return 0; +} diff --git a/test/test.h b/test/test.h new file mode 100644 index 0000000..ae6f22d --- /dev/null +++ b/test/test.h @@ -0,0 +1,11 @@ +#include + +#define run_test(fn) \ + printf(" 🧜 running \x1b[1;36m" #fn "\033[0m... "); \ + fflush(stdout); \ + fn(); \ + printf("\033[32mok!\033[0m\n"); + +void run_buffer_tests(); +void run_utf8_tests(); +void run_text_tests(); diff --git a/test/text.c b/test/text.c new file mode 100644 index 0000000..ec99890 --- /dev/null +++ b/test/text.c @@ -0,0 +1,82 @@ +#include "assert.h" +#include "test.h" + +#include "text.h" + +#include +#include +#include + +void assert_line_equal(struct txt_line *line) {} + +void test_add_text() { + uint32_t lines_added, cols_added; + struct text *t = text_create(10); + const char *txt = "This is line 1\n"; + text_append(t, 0, 0, (uint8_t *)txt, strlen(txt), &lines_added, &cols_added); + ASSERT(text_num_lines(t) == 2, + "Expected text to have two lines after insertion"); + + ASSERT(text_line_size(t, 0) == 14 && text_line_length(t, 0) == 14, + "Expected line 1 to have 14 chars and 14 bytes"); + ASSERT_STR_EQ((const char *)text_get_line(t, 0).text, "This is line 1", + "Expected line 1 to be line 1"); + + const char *txt2 = "This is line 2\n"; + text_append(t, 1, 0, (uint8_t *)txt2, strlen(txt2), &lines_added, + &cols_added); + ASSERT_STR_EQ((const char *)text_get_line(t, 1).text, "This is line 2", + "Expected line 2 to be line 2"); +} + +void test_delete_text() { + uint32_t lines_added, cols_added; + struct text *t = text_create(10); + const char *txt = "This is line 1"; + text_append(t, 0, 0, (uint8_t *)txt, strlen(txt), &lines_added, &cols_added); + + text_delete(t, 0, 12, 2); + ASSERT(text_line_length(t, 0) == 12, + "Expected line to be 12 chars after deleting two"); + ASSERT(strncmp((const char *)text_get_line(t, 0).text, "This is line", + text_line_size(t, 0)) == 0, + "Expected two chars to be deleted"); + + text_delete(t, 0, 0, 25); + ASSERT(text_get_line(t, 0).nbytes == 0, + "Expected line to be empty after many chars removed"); + + const char *txt2 = "This is line 1\nThis is line 2\nThis is line 3"; + text_append(t, 0, 0, (uint8_t *)txt2, strlen(txt2), &lines_added, + &cols_added); + text_delete(t, 1, 11, 3); + ASSERT(text_line_length(t, 1) == 11, + "Expected line to contain 11 chars after deletion"); + struct txt_line line = text_get_line(t, 1); + ASSERT(strncmp((const char *)line.text, "This is lin", line.nbytes) == 0, + "Expected deleted characters to be gone in the second line"); + + // test utf-8 + struct text *t2 = text_create(10); + const char *txt3 = "Emojis: šŸ‡«šŸ‡® 🐮\n"; + text_append(t2, 0, 0, (uint8_t *)txt3, strlen(txt3), &lines_added, + &cols_added); + + // TODO: Fix when graphemes are implemented, should be 11, right now it counts + // the two unicode code points šŸ‡« and šŸ‡® as two chars. + ASSERT(text_line_length(t2, 0) == 12, + "Line length should be 12 (even though there " + "are more bytes in the line)."); + + text_delete(t2, 0, 10, 2); + ASSERT(text_line_length(t2, 0) == 10, + "Line length should be 10 after deleting the cow emoji and a space"); + struct txt_line line2 = text_get_line(t2, 0); + ASSERT(strncmp((const char *)line2.text, "Emojis: šŸ‡«šŸ‡®", line2.nbytes) == 0, + "Expected cow emoji plus space to be deleted"); +} + +void run_text_tests() { + run_test(test_add_text); + run_test(test_delete_text); +} diff --git a/test/utf8.c b/test/utf8.c new file mode 100644 index 0000000..5b020c3 --- /dev/null +++ b/test/utf8.c @@ -0,0 +1,13 @@ +#include "utf8.h" +#include "assert.h" +#include "test.h" +#include "wchar.h" + +void test_nchars_nbytes() { + ASSERT(utf8_nchars((uint8_t *)"šŸ‘“", 2) == 1, + "Expected old man emoji to be 1 char"); + ASSERT(utf8_nbytes((uint8_t *)"šŸ‘“", 1) == 4, + "Expected old man emoji to be 4 bytes"); +} + +void run_utf8_tests() { run_test(test_nchars_nbytes); } -- cgit v1.2.3