summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore70
-rw-r--r--LICENSE28
-rw-r--r--Makefile50
-rw-r--r--README.md2
-rw-r--r--dged.nix25
-rw-r--r--flake.lock42
-rw-r--r--flake.nix16
-rw-r--r--src/binding.c57
-rw-r--r--src/binding.h43
-rw-r--r--src/buffer.c192
-rw-r--r--src/buffer.h68
-rw-r--r--src/command.c66
-rw-r--r--src/command.h34
-rw-r--r--src/display.c100
-rw-r--r--src/display.h32
-rw-r--r--src/keyboard.c56
-rw-r--r--src/keyboard.h28
-rw-r--r--src/main.c166
-rw-r--r--src/text.c299
-rw-r--r--src/text.h39
-rw-r--r--src/utf8.c42
-rw-r--r--src/utf8.h17
-rw-r--r--test/assert.c20
-rw-r--r--test/assert.h10
-rw-r--r--test/buffer.c46
-rw-r--r--test/main.c24
-rw-r--r--test/test.h11
-rw-r--r--test/text.c82
-rw-r--r--test/utf8.c13
30 files changed, 1679 insertions, 0 deletions
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 <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); }