diff options
| -rw-r--r-- | .envrc | 1 | ||||
| -rw-r--r-- | .gitignore | 70 | ||||
| -rw-r--r-- | LICENSE | 28 | ||||
| -rw-r--r-- | Makefile | 50 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | dged.nix | 25 | ||||
| -rw-r--r-- | flake.lock | 42 | ||||
| -rw-r--r-- | flake.nix | 16 | ||||
| -rw-r--r-- | src/binding.c | 57 | ||||
| -rw-r--r-- | src/binding.h | 43 | ||||
| -rw-r--r-- | src/buffer.c | 192 | ||||
| -rw-r--r-- | src/buffer.h | 68 | ||||
| -rw-r--r-- | src/command.c | 66 | ||||
| -rw-r--r-- | src/command.h | 34 | ||||
| -rw-r--r-- | src/display.c | 100 | ||||
| -rw-r--r-- | src/display.h | 32 | ||||
| -rw-r--r-- | src/keyboard.c | 56 | ||||
| -rw-r--r-- | src/keyboard.h | 28 | ||||
| -rw-r--r-- | src/main.c | 166 | ||||
| -rw-r--r-- | src/text.c | 299 | ||||
| -rw-r--r-- | src/text.h | 39 | ||||
| -rw-r--r-- | src/utf8.c | 42 | ||||
| -rw-r--r-- | src/utf8.h | 17 | ||||
| -rw-r--r-- | test/assert.c | 20 | ||||
| -rw-r--r-- | test/assert.h | 10 | ||||
| -rw-r--r-- | test/buffer.c | 46 | ||||
| -rw-r--r-- | test/main.c | 24 | ||||
| -rw-r--r-- | test/test.h | 11 | ||||
| -rw-r--r-- | test/text.c | 82 | ||||
| -rw-r--r-- | test/utf8.c | 13 |
30 files changed, 1679 insertions, 0 deletions
@@ -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/ @@ -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 <stdlib.h> +#include <string.h> + +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 <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +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 <stddef.h> +#include <stdint.h> + +#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 <stdlib.h> + +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 <stdint.h> + +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 <stdio.h> +#include <sys/ioctl.h> +#include <unistd.h> + +#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 <stdint.h> + +#include <termios.h> + +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 <string.h> +#include <unistd.h> + +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 <stdbool.h> +#include <stdint.h> + +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 <locale.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <assert.h> +#include <signal.h> +#include <sys/epoll.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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 <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +// 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 <stdio.h> + +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 <stdbool.h> +#include <stdint.h> + +/*! + * \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 <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +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, "<left string> == <right string>", 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 <stdbool.h> + +#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 <string.h> + +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 <locale.h> +#include <signal.h> +#include <stdlib.h> + +#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 <stdio.h> + +#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 <stdlib.h> +#include <string.h> +#include <wchar.h> + +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); } |
