summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--compile_flags.txt1
-rw-r--r--examples/dte/README.md19
-rw-r--r--examples/dte/bind.c115
-rw-r--r--examples/dte/bind.h20
-rw-r--r--examples/dte/block-iter.c343
-rw-r--r--examples/dte/block-iter.h78
-rw-r--r--examples/dte/block.c19
-rw-r--r--examples/dte/block.h39
-rw-r--r--examples/dte/bookmark.c103
-rw-r--r--examples/dte/bookmark.h24
-rw-r--r--examples/dte/buffer.c480
-rw-r--r--examples/dte/buffer.h103
-rw-r--r--examples/dte/change.c417
-rw-r--r--examples/dte/change.h39
-rw-r--r--examples/dte/cmdline.c540
-rw-r--r--examples/dte/cmdline.h40
-rw-r--r--examples/dte/commands.c2594
-rw-r--r--examples/dte/commands.h22
-rw-r--r--examples/dte/compat.c35
-rw-r--r--examples/dte/compat.h8
-rw-r--r--examples/dte/compiler.c151
-rw-r--r--examples/dte/compiler.h49
-rw-r--r--examples/dte/completion.c879
-rw-r--r--examples/dte/completion.h21
-rw-r--r--examples/dte/config.c185
-rw-r--r--examples/dte/config.h42
-rw-r--r--examples/dte/convert.c581
-rw-r--r--examples/dte/convert.h24
-rw-r--r--examples/dte/copy.c74
-rw-r--r--examples/dte/copy.h26
-rw-r--r--examples/dte/ctags.c157
-rw-r--r--examples/dte/ctags.h31
-rw-r--r--examples/dte/edit.c394
-rw-r--r--examples/dte/edit.h13
-rw-r--r--examples/dte/editor.c321
-rw-r--r--examples/dte/editor.h121
-rw-r--r--examples/dte/encoding.c132
-rw-r--r--examples/dte/encoding.h46
-rw-r--r--examples/dte/error.c95
-rw-r--r--examples/dte/error.h15
-rw-r--r--examples/dte/exec.c366
-rw-r--r--examples/dte/exec.h37
-rw-r--r--examples/dte/file-history.c153
-rw-r--r--examples/dte/file-history.h29
-rw-r--r--examples/dte/file-option.c193
-rw-r--r--examples/dte/file-option.h32
-rw-r--r--examples/dte/filetype.c333
-rw-r--r--examples/dte/filetype.h35
-rw-r--r--examples/dte/frame.c496
-rw-r--r--examples/dte/frame.h48
-rw-r--r--examples/dte/history.c146
-rw-r--r--examples/dte/history.h35
-rw-r--r--examples/dte/indent.c193
-rw-r--r--examples/dte/indent.h58
-rw-r--r--examples/dte/load-save.c505
-rw-r--r--examples/dte/load-save.h14
-rw-r--r--examples/dte/lock.c201
-rw-r--r--examples/dte/lock.h12
-rw-r--r--examples/dte/main.c575
-rw-r--r--examples/dte/misc.c764
-rw-r--r--examples/dte/misc.h22
-rw-r--r--examples/dte/mode.c74
-rw-r--r--examples/dte/mode.h11
-rw-r--r--examples/dte/move.c311
-rw-r--r--examples/dte/move.h31
-rw-r--r--examples/dte/msg.c139
-rw-r--r--examples/dte/msg.h30
-rw-r--r--examples/dte/options.c987
-rw-r--r--examples/dte/options.h114
-rw-r--r--examples/dte/regexp.c151
-rw-r--r--examples/dte/regexp.h85
-rw-r--r--examples/dte/replace.c256
-rw-r--r--examples/dte/replace.h18
-rw-r--r--examples/dte/screen-cmdline.c91
-rw-r--r--examples/dte/screen-prompt.c134
-rw-r--r--examples/dte/screen-status.c46
-rw-r--r--examples/dte/screen-tabbar.c176
-rw-r--r--examples/dte/screen-view.c427
-rw-r--r--examples/dte/screen-window.c130
-rw-r--r--examples/dte/screen.c211
-rw-r--r--examples/dte/screen.h59
-rw-r--r--examples/dte/search.c244
-rw-r--r--examples/dte/search.h34
-rw-r--r--examples/dte/selection.c110
-rw-r--r--examples/dte/selection.h22
-rw-r--r--examples/dte/shift.c147
-rw-r--r--examples/dte/shift.h8
-rw-r--r--examples/dte/show.c558
-rw-r--r--examples/dte/show.h27
-rw-r--r--examples/dte/signals.c169
-rw-r--r--examples/dte/signals.h11
-rw-r--r--examples/dte/spawn.c396
-rw-r--r--examples/dte/spawn.h37
-rw-r--r--examples/dte/status.c337
-rw-r--r--examples/dte/status.h20
-rw-r--r--examples/dte/tag.c322
-rw-r--r--examples/dte/tag.h23
-rw-r--r--examples/dte/vars.c113
-rw-r--r--examples/dte/vars.h11
-rw-r--r--examples/dte/view.c178
-rw-r--r--examples/dte/view.h62
-rw-r--r--examples/dte/window.c507
-rw-r--r--examples/dte/window.h68
-rw-r--r--list.c64
-rw-r--r--list.h14
-rw-r--r--main.c139
107 files changed, 19711 insertions, 36 deletions
diff --git a/Makefile b/Makefile
index 20cfbdc..4e0f696 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ SOURCES = $(wildcard *.c)
TS_ALIBS = $(shell find vendor -name "*.a" -print)
VENDOR_DIRS = $(wildcard vendor/*)
CFLAGS = $(EXTRA_FLAGS) -Wall -Wextra -std=gnu99 -pedantic -ggdb -O3
-LIBS = -I./vendor/tree-sitter/lib/include
+LIBS = -I./vendor/tree-sitter/lib/include -lpthread
$(info VENDOR_DIRS: $(VENDOR_DIRS))
$(info SOURCES: $(SOURCES))
diff --git a/compile_flags.txt b/compile_flags.txt
index f6f7057..534180f 100644
--- a/compile_flags.txt
+++ b/compile_flags.txt
@@ -1,2 +1 @@
--std=c99
-I./vendor/tree-sitter/lib/include
diff --git a/examples/dte/README.md b/examples/dte/README.md
new file mode 100644
index 0000000..6f74c23
--- /dev/null
+++ b/examples/dte/README.md
@@ -0,0 +1,19 @@
+dte source code
+===============
+
+This directory contains the `dte` source code. It makes liberal use
+of ISO C99 features and POSIX 2008 APIs, but generally requires very
+little else.
+
+The main editor code is in the base directory and various other
+(somewhat reusable) parts are in sub-directories:
+
+* `command/` - command language parsing and execution
+* `editorconfig/` - [EditorConfig] implementation
+* `filetype/` - filetype detection
+* `syntax/` - syntax highlighting
+* `terminal/` - terminal input/output handling
+* `util/` - data structures, string utilities, etc.
+
+
+[EditorConfig]: https://editorconfig.org/
diff --git a/examples/dte/bind.c b/examples/dte/bind.c
new file mode 100644
index 0000000..222ee27
--- /dev/null
+++ b/examples/dte/bind.c
@@ -0,0 +1,115 @@
+#include <limits.h>
+#include <stdlib.h>
+#include "bind.h"
+#include "change.h"
+#include "command/macro.h"
+#include "command/run.h"
+#include "command/serialize.h"
+#include "util/debug.h"
+#include "util/xmalloc.h"
+
+void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc)
+{
+ cached_command_free(intmap_insert_or_replace(bindings, key, cc));
+}
+
+void remove_binding(IntMap *bindings, KeyCode key)
+{
+ cached_command_free(intmap_remove(bindings, key));
+}
+
+const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key)
+{
+ return intmap_get(bindings, key);
+}
+
+void free_bindings(IntMap *bindings)
+{
+ intmap_free(bindings, (FreeFunction)cached_command_free);
+}
+
+bool handle_binding(EditorState *e, InputMode mode, KeyCode key)
+{
+ const IntMap *bindings = &e->modes[mode].key_bindings;
+ const CachedCommand *binding = lookup_binding(bindings, key);
+ if (!binding) {
+ return false;
+ }
+
+ // If the command isn't cached or a macro is being recorded
+ const CommandSet *cmds = e->modes[mode].cmds;
+ if (!binding->cmd || (cmds->macro_record && macro_is_recording(&e->macro))) {
+ // Parse and run command string
+ CommandRunner runner = cmdrunner_for_mode(e, mode, true);
+ return handle_command(&runner, binding->cmd_str);
+ }
+
+ // Command is cached; call it directly
+ begin_change(CHANGE_MERGE_NONE);
+ current_command = binding->cmd;
+ bool r = binding->cmd->cmd(e, &binding->a);
+ current_command = NULL;
+ end_change();
+ return r;
+}
+
+typedef struct {
+ KeyCode key;
+ const char *cmd;
+} KeyBinding;
+
+static int binding_cmp(const void *ap, const void *bp)
+{
+ static_assert((MOD_MASK | KEY_SPECIAL_MAX) <= INT_MAX);
+ const KeyBinding *a = ap;
+ const KeyBinding *b = bp;
+ return (int)a->key - (int)b->key;
+}
+
+UNITTEST {
+ KeyBinding a = {.key = KEY_F5};
+ KeyBinding b = {.key = KEY_F5};
+ BUG_ON(binding_cmp(&a, &b) != 0);
+ b.key = KEY_F3;
+ BUG_ON(binding_cmp(&a, &b) <= 0);
+ b.key = KEY_F12;
+ BUG_ON(binding_cmp(&a, &b) >= 0);
+}
+
+bool dump_bindings(const IntMap *bindings, const char *flag, String *buf)
+{
+ const size_t count = bindings->count;
+ if (unlikely(count == 0)) {
+ return false;
+ }
+
+ // Clone the contents of the map as an array of key/command pairs
+ KeyBinding *array = xnew(*array, count);
+ size_t n = 0;
+ for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) {
+ const CachedCommand *cc = it.entry->value;
+ array[n++] = (KeyBinding) {
+ .key = it.entry->key,
+ .cmd = cc->cmd_str,
+ };
+ }
+
+ // Sort the array
+ BUG_ON(n != count);
+ qsort(array, count, sizeof(array[0]), binding_cmp);
+
+ // Serialize the bindings in sorted order
+ char keystr[KEYCODE_STR_MAX];
+ for (size_t i = 0; i < count; i++) {
+ string_append_literal(buf, "bind ");
+ string_append_cstring(buf, flag);
+ size_t keylen = keycode_to_string(array[i].key, keystr);
+ string_append_escaped_arg_sv(buf, string_view(keystr, keylen), true);
+ string_append_byte(buf, ' ');
+ string_append_escaped_arg(buf, array[i].cmd, true);
+ string_append_byte(buf, '\n');
+ }
+
+ free(array);
+ return true;
+}
diff --git a/examples/dte/bind.h b/examples/dte/bind.h
new file mode 100644
index 0000000..22d89bd
--- /dev/null
+++ b/examples/dte/bind.h
@@ -0,0 +1,20 @@
+#ifndef BIND_H
+#define BIND_H
+
+#include <stdbool.h>
+#include "command/cache.h"
+#include "editor.h"
+#include "terminal/key.h"
+#include "util/intmap.h"
+#include "util/macros.h"
+#include "util/string.h"
+
+void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc) NONNULL_ARGS;
+void remove_binding(IntMap *bindings, KeyCode key) NONNULL_ARGS;
+const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key) NONNULL_ARGS;
+bool handle_binding(EditorState *e, InputMode mode, KeyCode key) NONNULL_ARGS WARN_UNUSED_RESULT;
+void free_bindings(IntMap *bindings) NONNULL_ARGS;
+bool dump_bindings(const IntMap *bindings, const char *flag, String *buf) NONNULL_ARGS WARN_UNUSED_RESULT;
+
+#endif
+
diff --git a/examples/dte/block-iter.c b/examples/dte/block-iter.c
new file mode 100644
index 0000000..73fc2ec
--- /dev/null
+++ b/examples/dte/block-iter.c
@@ -0,0 +1,343 @@
+#include <string.h>
+#include "block-iter.h"
+#include "util/debug.h"
+#include "util/utf8.h"
+#include "util/xmalloc.h"
+
+void block_iter_normalize(BlockIter *bi)
+{
+ const Block *blk = bi->blk;
+ if (bi->offset == blk->size && blk->node.next != bi->head) {
+ bi->blk = BLOCK(blk->node.next);
+ bi->offset = 0;
+ }
+}
+
+/*
+ * Move after next newline (beginning of next line or end of file).
+ * Returns number of bytes iterator advanced.
+ */
+size_t block_iter_eat_line(BlockIter *bi)
+{
+ block_iter_normalize(bi);
+ const size_t offset = bi->offset;
+ if (unlikely(offset == bi->blk->size)) {
+ return 0;
+ }
+
+ // There must be at least one newline
+ if (bi->blk->nl == 1) {
+ bi->offset = bi->blk->size;
+ } else {
+ const unsigned char *end;
+ end = memchr(bi->blk->data + offset, '\n', bi->blk->size - offset);
+ BUG_ON(!end);
+ bi->offset = (size_t)(end + 1 - bi->blk->data);
+ }
+
+ return bi->offset - offset;
+}
+
+/*
+ * Move to beginning of next line.
+ * If there is no next line, iterator is not advanced.
+ * Returns number of bytes iterator advanced.
+ */
+size_t block_iter_next_line(BlockIter *bi)
+{
+ block_iter_normalize(bi);
+ const size_t offset = bi->offset;
+ if (unlikely(offset == bi->blk->size)) {
+ return 0;
+ }
+
+ // There must be at least one newline
+ size_t new_offset;
+ if (bi->blk->nl == 1) {
+ new_offset = bi->blk->size;
+ } else {
+ const unsigned char *end;
+ end = memchr(bi->blk->data + offset, '\n', bi->blk->size - offset);
+ BUG_ON(!end);
+ new_offset = (size_t)(end + 1 - bi->blk->data);
+ }
+ if (new_offset == bi->blk->size && bi->blk->node.next == bi->head) {
+ return 0;
+ }
+
+ bi->offset = new_offset;
+ return bi->offset - offset;
+}
+
+/*
+ * Move to beginning of previous line.
+ * Returns number of bytes moved, which is zero if there's no previous line.
+ */
+size_t block_iter_prev_line(BlockIter *bi)
+{
+ Block *blk = bi->blk;
+ size_t offset = bi->offset;
+ size_t start = offset;
+
+ while (offset && blk->data[offset - 1] != '\n') {
+ offset--;
+ }
+
+ if (!offset) {
+ if (blk->node.prev == bi->head) {
+ return 0;
+ }
+ bi->blk = blk = BLOCK(blk->node.prev);
+ offset = blk->size;
+ start += offset;
+ }
+
+ offset--;
+ while (offset && blk->data[offset - 1] != '\n') {
+ offset--;
+ }
+ bi->offset = offset;
+ return start - offset;
+}
+
+size_t block_iter_get_char(const BlockIter *bi, CodePoint *up)
+{
+ BlockIter tmp = *bi;
+ return block_iter_next_char(&tmp, up);
+}
+
+size_t block_iter_next_char(BlockIter *bi, CodePoint *up)
+{
+ size_t offset = bi->offset;
+ if (unlikely(offset == bi->blk->size)) {
+ if (unlikely(bi->blk->node.next == bi->head)) {
+ return 0;
+ }
+ bi->blk = BLOCK(bi->blk->node.next);
+ bi->offset = offset = 0;
+ }
+
+ // Note: this block can't be empty
+ *up = bi->blk->data[offset];
+ if (likely(*up < 0x80)) {
+ bi->offset++;
+ return 1;
+ }
+
+ *up = u_get_nonascii(bi->blk->data, bi->blk->size, &bi->offset);
+ return bi->offset - offset;
+}
+
+size_t block_iter_prev_char(BlockIter *bi, CodePoint *up)
+{
+ size_t offset = bi->offset;
+ if (unlikely(offset == 0)) {
+ if (unlikely(bi->blk->node.prev == bi->head)) {
+ return 0;
+ }
+ bi->blk = BLOCK(bi->blk->node.prev);
+ bi->offset = offset = bi->blk->size;
+ }
+
+ // Note: this block can't be empty
+ *up = bi->blk->data[offset - 1];
+ if (likely(*up < 0x80)) {
+ bi->offset--;
+ return 1;
+ }
+
+ *up = u_prev_char(bi->blk->data, &bi->offset);
+ return offset - bi->offset;
+}
+
+size_t block_iter_next_column(BlockIter *bi)
+{
+ CodePoint u;
+ size_t size = block_iter_next_char(bi, &u);
+ while (block_iter_get_char(bi, &u) && u_is_zero_width(u)) {
+ size += block_iter_next_char(bi, &u);
+ }
+ return size;
+}
+
+size_t block_iter_prev_column(BlockIter *bi)
+{
+ CodePoint u;
+ size_t skip, total = 0;
+ do {
+ skip = block_iter_prev_char(bi, &u);
+ total += skip;
+ } while (skip && u_is_zero_width(u));
+ return total;
+}
+
+size_t block_iter_bol(BlockIter *bi)
+{
+ block_iter_normalize(bi);
+ size_t offset = bi->offset;
+ if (offset == 0 || offset == bi->blk->size) {
+ return 0;
+ }
+
+ if (bi->blk->nl == 1) {
+ offset = 0;
+ } else {
+ while (offset && bi->blk->data[offset - 1] != '\n') {
+ offset--;
+ }
+ }
+
+ const size_t ret = bi->offset - offset;
+ bi->offset = offset;
+ return ret;
+}
+
+size_t block_iter_eol(BlockIter *bi)
+{
+ block_iter_normalize(bi);
+ const Block *blk = bi->blk;
+ const size_t offset = bi->offset;
+ if (unlikely(offset == blk->size)) {
+ // Cursor at end of last block
+ return 0;
+ }
+ if (blk->nl == 1) {
+ bi->offset = blk->size - 1;
+ return bi->offset - offset;
+ }
+ const unsigned char *end = memchr(blk->data + offset, '\n', blk->size - offset);
+ BUG_ON(!end);
+ bi->offset = (size_t)(end - blk->data);
+ return bi->offset - offset;
+}
+
+void block_iter_back_bytes(BlockIter *bi, size_t count)
+{
+ while (count > bi->offset) {
+ count -= bi->offset;
+ bi->blk = BLOCK(bi->blk->node.prev);
+ bi->offset = bi->blk->size;
+ }
+ bi->offset -= count;
+}
+
+void block_iter_skip_bytes(BlockIter *bi, size_t count)
+{
+ size_t avail = bi->blk->size - bi->offset;
+ while (count > avail) {
+ count -= avail;
+ bi->blk = BLOCK(bi->blk->node.next);
+ bi->offset = 0;
+ avail = bi->blk->size;
+ }
+ bi->offset += count;
+}
+
+void block_iter_goto_offset(BlockIter *bi, size_t offset)
+{
+ Block *blk;
+ block_for_each(blk, bi->head) {
+ if (offset <= blk->size) {
+ bi->blk = blk;
+ bi->offset = offset;
+ return;
+ }
+ offset -= blk->size;
+ }
+}
+
+void block_iter_goto_line(BlockIter *bi, size_t line)
+{
+ Block *blk = BLOCK(bi->head->next);
+ size_t nl = 0;
+ while (blk->node.next != bi->head && nl + blk->nl < line) {
+ nl += blk->nl;
+ blk = BLOCK(blk->node.next);
+ }
+
+ bi->blk = blk;
+ bi->offset = 0;
+ while (nl < line) {
+ if (!block_iter_eat_line(bi)) {
+ break;
+ }
+ nl++;
+ }
+}
+
+size_t block_iter_get_offset(const BlockIter *bi)
+{
+ const Block *blk;
+ size_t offset = 0;
+ block_for_each(blk, bi->head) {
+ if (blk == bi->blk) {
+ break;
+ }
+ offset += blk->size;
+ }
+ return offset + bi->offset;
+}
+
+char *block_iter_get_bytes(const BlockIter *bi, size_t len)
+{
+ if (len == 0) {
+ return NULL;
+ }
+
+ const Block *blk = bi->blk;
+ size_t offset = bi->offset;
+ size_t pos = 0;
+ char *buf = xmalloc(len);
+
+ while (pos < len) {
+ const size_t avail = blk->size - offset;
+ size_t count = MIN(len - pos, avail);
+ memcpy(buf + pos, blk->data + offset, count);
+ pos += count;
+ BUG_ON(pos < len && blk->node.next == bi->head);
+ blk = BLOCK(blk->node.next);
+ offset = 0;
+ }
+
+ return buf;
+}
+
+// bi should be at bol
+void fill_line_nl_ref(BlockIter *bi, StringView *line)
+{
+ block_iter_normalize(bi);
+ line->data = bi->blk->data + bi->offset;
+ const size_t max = bi->blk->size - bi->offset;
+ if (unlikely(max == 0)) {
+ // Cursor at end of last block
+ line->length = 0;
+ return;
+ }
+ if (bi->blk->nl == 1) {
+ BUG_ON(line->data[max - 1] != '\n');
+ line->length = max;
+ return;
+ }
+ const unsigned char *nl = memchr(line->data, '\n', max);
+ BUG_ON(!nl);
+ line->length = (size_t)(nl - line->data + 1);
+ BUG_ON(line->length == 0);
+}
+
+void fill_line_ref(BlockIter *bi, StringView *line)
+{
+ fill_line_nl_ref(bi, line);
+ // Trim the newline
+ line->length -= (line->length > 0);
+}
+
+// Set the `line` argument to point to the current line and return
+// the offset of the cursor, relative to the start of the line
+// (zero means cursor is at bol)
+size_t fetch_this_line(const BlockIter *bi, StringView *line)
+{
+ BlockIter tmp = *bi;
+ size_t count = block_iter_bol(&tmp);
+ fill_line_ref(&tmp, line);
+ return count;
+}
diff --git a/examples/dte/block-iter.h b/examples/dte/block-iter.h
new file mode 100644
index 0000000..aaef8cf
--- /dev/null
+++ b/examples/dte/block-iter.h
@@ -0,0 +1,78 @@
+#ifndef BLOCK_ITER_H
+#define BLOCK_ITER_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "block.h"
+#include "util/list.h"
+#include "util/macros.h"
+#include "util/string-view.h"
+#include "util/unicode.h"
+
+typedef struct {
+ Block *blk;
+ const ListHead *head;
+ size_t offset;
+} BlockIter;
+
+static inline void block_iter_bof(BlockIter *bi)
+{
+ bi->blk = BLOCK(bi->head->next);
+ bi->offset = 0;
+}
+
+static inline void block_iter_eof(BlockIter *bi)
+{
+ bi->blk = BLOCK(bi->head->prev);
+ bi->offset = bi->blk->size;
+}
+
+static inline bool block_iter_is_eof(const BlockIter *bi)
+{
+ return bi->offset == bi->blk->size && bi->blk->node.next == bi->head;
+}
+
+static inline bool block_iter_is_bol(const BlockIter *bi)
+{
+ return bi->offset == 0 || bi->blk->data[bi->offset - 1] == '\n';
+}
+
+static inline bool block_iter_is_eol(const BlockIter *bi)
+{
+ const Block *blk = bi->blk;
+ size_t offset = bi->offset;
+ if (offset == blk->size) {
+ if (blk->node.next == bi->head) {
+ // EOF
+ return true;
+ }
+ // Normalize
+ blk = BLOCK(blk->node.next);
+ offset = 0;
+ }
+ return blk->data[offset] == '\n';
+}
+
+void block_iter_normalize(BlockIter *bi);
+size_t block_iter_eat_line(BlockIter *bi);
+size_t block_iter_next_line(BlockIter *bi);
+size_t block_iter_prev_line(BlockIter *bi);
+size_t block_iter_next_char(BlockIter *bi, CodePoint *up);
+size_t block_iter_prev_char(BlockIter *bi, CodePoint *up);
+size_t block_iter_next_column(BlockIter *bi);
+size_t block_iter_prev_column(BlockIter *bi);
+size_t block_iter_bol(BlockIter *bi);
+size_t block_iter_eol(BlockIter *bi);
+void block_iter_back_bytes(BlockIter *bi, size_t count);
+void block_iter_skip_bytes(BlockIter *bi, size_t count);
+void block_iter_goto_offset(BlockIter *bi, size_t offset);
+void block_iter_goto_line(BlockIter *bi, size_t line);
+size_t block_iter_get_offset(const BlockIter *bi) WARN_UNUSED_RESULT;
+size_t block_iter_get_char(const BlockIter *bi, CodePoint *up) WARN_UNUSED_RESULT;
+char *block_iter_get_bytes(const BlockIter *bi, size_t len) WARN_UNUSED_RESULT;
+
+void fill_line_ref(BlockIter *bi, StringView *line);
+void fill_line_nl_ref(BlockIter *bi, StringView *line);
+size_t fetch_this_line(const BlockIter *bi, StringView *line);
+
+#endif
diff --git a/examples/dte/block.c b/examples/dte/block.c
new file mode 100644
index 0000000..3953571
--- /dev/null
+++ b/examples/dte/block.c
@@ -0,0 +1,19 @@
+#include <stdlib.h>
+#include "block.h"
+#include "util/xmalloc.h"
+
+Block *block_new(size_t alloc)
+{
+ Block *blk = xnew0(Block, 1);
+ alloc = round_size_to_next_multiple(alloc, BLOCK_ALLOC_MULTIPLE);
+ blk->data = xmalloc(alloc);
+ blk->alloc = alloc;
+ return blk;
+}
+
+void block_free(Block *blk)
+{
+ list_del(&blk->node);
+ free(blk->data);
+ free(blk);
+}
diff --git a/examples/dte/block.h b/examples/dte/block.h
new file mode 100644
index 0000000..6fbf361
--- /dev/null
+++ b/examples/dte/block.h
@@ -0,0 +1,39 @@
+#ifndef BLOCK_H
+#define BLOCK_H
+
+#include <stddef.h>
+#include "util/list.h"
+#include "util/macros.h"
+
+enum {
+ BLOCK_ALLOC_MULTIPLE = 64
+};
+
+// Blocks always contain whole lines.
+// There's one zero-sized block for an empty file.
+// Otherwise zero-sized blocks are forbidden.
+typedef struct {
+ ListHead node;
+ unsigned char NONSTRING *data;
+ size_t size;
+ size_t alloc;
+ size_t nl;
+} Block;
+
+#define block_for_each(block_, list_head_) \
+ for ( \
+ block_ = BLOCK((list_head_)->next); \
+ &block_->node != (list_head_); \
+ block_ = BLOCK(block_->node.next) \
+ )
+
+static inline Block *BLOCK(ListHead *item)
+{
+ static_assert(offsetof(Block, node) == 0);
+ return (Block*)item;
+}
+
+Block *block_new(size_t alloc) RETURNS_NONNULL;
+void block_free(Block *blk) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/bookmark.c b/examples/dte/bookmark.c
new file mode 100644
index 0000000..174405f
--- /dev/null
+++ b/examples/dte/bookmark.c
@@ -0,0 +1,103 @@
+#include <stdlib.h>
+#include "bookmark.h"
+#include "buffer.h"
+#include "editor.h"
+#include "misc.h"
+#include "move.h"
+#include "search.h"
+#include "util/debug.h"
+#include "util/xmalloc.h"
+
+FileLocation *get_current_file_location(const View *view)
+{
+ const char *filename = view->buffer->abs_filename;
+ FileLocation *loc = xmalloc(sizeof(*loc));
+ *loc = (FileLocation) {
+ .filename = filename ? xstrdup(filename) : NULL,
+ .buffer_id = view->buffer->id,
+ .line = view->cy + 1,
+ .column = view->cx_char + 1
+ };
+ return loc;
+}
+
+bool file_location_go(Window *window, const FileLocation *loc)
+{
+ View *view = window_open_buffer(window, loc->filename, true, NULL);
+ if (!view) {
+ // Failed to open file; error message should be visible
+ return false;
+ }
+
+ if (window->view != view) {
+ set_view(view);
+ // Force centering view to cursor, because file changed
+ view->force_center = true;
+ }
+
+ if (loc->pattern) {
+ if (!search_tag(view, loc->pattern)) {
+ return false;
+ }
+ } else if (loc->line > 0) {
+ move_to_filepos(view, loc->line, loc->column ? loc->column : 1);
+ }
+
+ unselect(view);
+ return true;
+}
+
+static bool file_location_return(Window *window, const FileLocation *loc)
+{
+ Buffer *buffer = find_buffer_by_id(&window->editor->buffers, loc->buffer_id);
+ View *view;
+ if (buffer) {
+ view = window_get_view(window, buffer);
+ } else {
+ if (!loc->filename) {
+ // Can't restore closed buffer that had no filename; try again
+ return false;
+ }
+ view = window_open_buffer(window, loc->filename, true, NULL);
+ }
+
+ if (!view) {
+ // Open failed; don't try again
+ return true;
+ }
+
+ set_view(view);
+ unselect(view);
+ move_to_filepos(view, loc->line, loc->column);
+ return true;
+}
+
+void file_location_free(FileLocation *loc)
+{
+ free(loc->filename);
+ free(loc->pattern);
+ free(loc);
+}
+
+void bookmark_push(PointerArray *bookmarks, FileLocation *loc)
+{
+ const size_t max_entries = 256;
+ if (bookmarks->count == max_entries) {
+ file_location_free(ptr_array_remove_idx(bookmarks, 0));
+ }
+ BUG_ON(bookmarks->count >= max_entries);
+ ptr_array_append(bookmarks, loc);
+}
+
+void bookmark_pop(Window *window, PointerArray *bookmarks)
+{
+ void **ptrs = bookmarks->ptrs;
+ size_t count = bookmarks->count;
+ bool go = true;
+ while (count > 0 && go) {
+ FileLocation *loc = ptrs[--count];
+ go = !file_location_return(window, loc);
+ file_location_free(loc);
+ }
+ bookmarks->count = count;
+}
diff --git a/examples/dte/bookmark.h b/examples/dte/bookmark.h
new file mode 100644
index 0000000..5496589
--- /dev/null
+++ b/examples/dte/bookmark.h
@@ -0,0 +1,24 @@
+#ifndef BOOKMARK_H
+#define BOOKMARK_H
+
+#include <stdbool.h>
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "view.h"
+#include "window.h"
+
+typedef struct {
+ char *filename; // Needed after buffer is closed
+ unsigned long buffer_id; // Needed if buffer doesn't have a filename
+ char *pattern; // Regex from tag file (if set, line and column are 0)
+ unsigned long line, column; // File position (if non-zero, pattern is NULL)
+} FileLocation;
+
+FileLocation *get_current_file_location(const View *view) NONNULL_ARGS_AND_RETURN;
+bool file_location_go(Window *window, const FileLocation *loc) NONNULL_ARGS WARN_UNUSED_RESULT;
+void file_location_free(FileLocation *loc) NONNULL_ARGS;
+
+void bookmark_push(PointerArray *bookmarks, FileLocation *loc) NONNULL_ARGS;
+void bookmark_pop(Window *window, PointerArray *bookmarks) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/buffer.c b/examples/dte/buffer.c
new file mode 100644
index 0000000..705ec0c
--- /dev/null
+++ b/examples/dte/buffer.c
@@ -0,0 +1,480 @@
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include "buffer.h"
+#include "editor.h"
+#include "file-option.h"
+#include "filetype.h"
+#include "lock.h"
+#include "syntax/state.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/log.h"
+#include "util/numtostr.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/time-util.h"
+#include "util/xmalloc.h"
+
+void set_display_filename(Buffer *buffer, char *name)
+{
+ free(buffer->display_filename);
+ buffer->display_filename = name;
+}
+
+/*
+ * Mark line range min...max (inclusive) "changed". These lines will be
+ * redrawn when screen is updated. This is called even when content has not
+ * been changed, but selection has or line has been deleted and all lines
+ * after the deleted line move up.
+ *
+ * Syntax highlighter has different logic. It cares about contents of the
+ * lines, not about selection or if the lines have been moved up or down.
+ */
+void buffer_mark_lines_changed(Buffer *buffer, long min, long max)
+{
+ if (min > max) {
+ long tmp = min;
+ min = max;
+ max = tmp;
+ }
+ buffer->changed_line_min = MIN(min, buffer->changed_line_min);
+ buffer->changed_line_max = MAX(max, buffer->changed_line_max);
+}
+
+const char *buffer_filename(const Buffer *buffer)
+{
+ const char *name = buffer->display_filename;
+ return name ? name : "(No name)";
+}
+
+void buffer_set_encoding(Buffer *buffer, Encoding encoding, bool utf8_bom)
+{
+ if (
+ buffer->encoding.type != encoding.type
+ || buffer->encoding.name != encoding.name
+ ) {
+ const EncodingType type = encoding.type;
+ if (type == UTF8) {
+ buffer->bom = utf8_bom;
+ } else {
+ buffer->bom = type < NR_ENCODING_TYPES && !!get_bom_for_encoding(type);
+ }
+ buffer->encoding = encoding;
+ }
+}
+
+Buffer *buffer_new(PointerArray *buffers, const GlobalOptions *gopts, const Encoding *encoding)
+{
+ static unsigned long id;
+ Buffer *buffer = xnew0(Buffer, 1);
+ list_init(&buffer->blocks);
+ buffer->cur_change = &buffer->change_head;
+ buffer->saved_change = &buffer->change_head;
+ buffer->id = ++id;
+ buffer->crlf_newlines = gopts->crlf_newlines;
+
+ if (encoding) {
+ buffer_set_encoding(buffer, *encoding, gopts->utf8_bom);
+ } else {
+ buffer->encoding.type = ENCODING_AUTODETECT;
+ }
+
+ static_assert(sizeof(*gopts) >= sizeof(CommonOptions));
+ memcpy(&buffer->options, gopts, sizeof(CommonOptions));
+ buffer->options.brace_indent = 0;
+ buffer->options.filetype = str_intern("none");
+ buffer->options.indent_regex = NULL;
+
+ ptr_array_append(buffers, buffer);
+ return buffer;
+}
+
+Buffer *open_empty_buffer(PointerArray *buffers, const GlobalOptions *gopts)
+{
+ Encoding enc = encoding_from_type(UTF8);
+ Buffer *buffer = buffer_new(buffers, gopts, &enc);
+
+ // At least one block required
+ Block *blk = block_new(1);
+ list_add_before(&blk->node, &buffer->blocks);
+
+ return buffer;
+}
+
+void free_blocks(Buffer *buffer)
+{
+ ListHead *item = buffer->blocks.next;
+ while (item != &buffer->blocks) {
+ ListHead *next = item->next;
+ Block *blk = BLOCK(item);
+ free(blk->data);
+ free(blk);
+ item = next;
+ }
+}
+
+void free_buffer(Buffer *buffer)
+{
+ if (buffer->locked) {
+ unlock_file(buffer->abs_filename);
+ }
+
+ free_changes(&buffer->change_head);
+ free(buffer->line_start_states.ptrs);
+ free(buffer->views.ptrs);
+ free(buffer->display_filename);
+ free(buffer->abs_filename);
+
+ if (buffer->stdout_buffer) {
+ return;
+ }
+
+ free_blocks(buffer);
+ free(buffer);
+}
+
+void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer)
+{
+ ptr_array_remove(buffers, buffer);
+ free_buffer(buffer);
+}
+
+static bool same_file(const Buffer *buffer, const struct stat *st)
+{
+ return (st->st_dev == buffer->file.dev) && (st->st_ino == buffer->file.ino);
+}
+
+Buffer *find_buffer(const PointerArray *buffers, const char *abs_filename)
+{
+ struct stat st;
+ bool st_ok = stat(abs_filename, &st) == 0;
+ for (size_t i = 0, n = buffers->count; i < n; i++) {
+ Buffer *buffer = buffers->ptrs[i];
+ const char *f = buffer->abs_filename;
+ if ((f && streq(f, abs_filename)) || (st_ok && same_file(buffer, &st))) {
+ return buffer;
+ }
+ }
+ return NULL;
+}
+
+Buffer *find_buffer_by_id(const PointerArray *buffers, unsigned long id)
+{
+ for (size_t i = 0, n = buffers->count; i < n; i++) {
+ Buffer *buffer = buffers->ptrs[i];
+ if (buffer->id == id) {
+ return buffer;
+ }
+ }
+ return NULL;
+}
+
+bool buffer_detect_filetype(Buffer *buffer, const PointerArray *filetypes)
+{
+ StringView line = STRING_VIEW_INIT;
+ if (BLOCK(buffer->blocks.next)->size) {
+ BlockIter bi = block_iter(buffer);
+ fill_line_ref(&bi, &line);
+ } else if (!buffer->abs_filename) {
+ return false;
+ }
+
+ const char *ft = find_ft(filetypes, buffer->abs_filename, line);
+ if (ft && !streq(ft, buffer->options.filetype)) {
+ buffer->options.filetype = str_intern(ft);
+ return true;
+ }
+
+ return false;
+}
+
+void update_short_filename_cwd(Buffer *buffer, const StringView *home, const char *cwd)
+{
+ const char *abs = buffer->abs_filename;
+ if (!abs) {
+ return;
+ }
+ char *name = cwd ? short_filename_cwd(abs, cwd, home) : xstrdup(abs);
+ set_display_filename(buffer, name);
+}
+
+void update_short_filename(Buffer *buffer, const StringView *home)
+{
+ BUG_ON(!buffer->abs_filename);
+ set_display_filename(buffer, short_filename(buffer->abs_filename, home));
+}
+
+void buffer_update_syntax(EditorState *e, Buffer *buffer)
+{
+ Syntax *syn = NULL;
+ if (buffer->options.syntax) {
+ // Even "none" can have syntax
+ syn = find_syntax(&e->syntaxes, buffer->options.filetype);
+ if (!syn) {
+ syn = load_syntax_by_filetype(e, buffer->options.filetype);
+ }
+ }
+ if (syn == buffer->syn) {
+ return;
+ }
+
+ buffer->syn = syn;
+ if (syn) {
+ // Start state of first line is constant
+ PointerArray *s = &buffer->line_start_states;
+ if (!s->alloc) {
+ ptr_array_init(s, 64);
+ }
+ s->ptrs[0] = syn->start_state;
+ s->count = 1;
+ }
+
+ mark_all_lines_changed(buffer);
+}
+
+static bool allow_odd_indent(uint8_t indents_bitmask)
+{
+ static_assert(INDENT_WIDTH_MAX == 8);
+ return !!(indents_bitmask & 0x55); // 0x55 == 0b01010101
+}
+
+static int indent_len(StringView line, uint8_t indents_bitmask, bool *tab_indent)
+{
+ bool space_before_tab = false;
+ size_t spaces = 0;
+ size_t tabs = 0;
+ size_t pos = 0;
+
+ for (size_t n = line.length; pos < n; pos++) {
+ switch (line.data[pos]) {
+ case '\t':
+ tabs++;
+ if (spaces) {
+ space_before_tab = true;
+ }
+ continue;
+ case ' ':
+ spaces++;
+ continue;
+ }
+ break;
+ }
+
+ *tab_indent = false;
+ if (pos == line.length) {
+ return -1; // Whitespace only
+ }
+ if (pos == 0) {
+ return 0; // Not indented
+ }
+ if (space_before_tab) {
+ return -2; // Mixed indent
+ }
+ if (tabs) {
+ // Tabs and possible spaces after tab for alignment
+ *tab_indent = true;
+ return tabs * 8;
+ }
+ if (line.length > spaces && line.data[spaces] == '*') {
+ // '*' after indent, could be long C style comment
+ if (spaces & 1 || allow_odd_indent(indents_bitmask)) {
+ return spaces - 1;
+ }
+ }
+ return spaces;
+}
+
+UNITTEST {
+ bool tab;
+ int len = indent_len(strview_from_cstring(" 4 space"), 0, &tab);
+ BUG_ON(len != 4);
+ BUG_ON(tab);
+
+ len = indent_len(strview_from_cstring("\t\t2 tab"), 0, &tab);
+ BUG_ON(len != 16);
+ BUG_ON(!tab);
+
+ len = indent_len(strview_from_cstring("no indent"), 0, &tab);
+ BUG_ON(len != 0);
+
+ len = indent_len(strview_from_cstring(" \t mixed"), 0, &tab);
+ BUG_ON(len != -2);
+
+ len = indent_len(strview_from_cstring("\t \t "), 0, &tab);
+ BUG_ON(len != -1); // whitespace only
+
+ len = indent_len(strview_from_cstring(" * 5 space"), 0, &tab);
+ BUG_ON(len != 4);
+
+ StringView line = strview_from_cstring(" * 4 space");
+ len = indent_len(line, 0, &tab);
+ BUG_ON(len != 4);
+ len = indent_len(line, 1 << 2, &tab);
+ BUG_ON(len != 3);
+}
+
+static bool detect_indent(Buffer *buffer)
+{
+ LocalOptions *options = &buffer->options;
+ unsigned int bitset = options->detect_indent;
+ BlockIter bi = block_iter(buffer);
+ unsigned int tab_count = 0;
+ unsigned int space_count = 0;
+ int current_indent = 0;
+ int counts[INDENT_WIDTH_MAX + 1] = {0};
+ BUG_ON((bitset & ((1u << INDENT_WIDTH_MAX) - 1)) != bitset);
+
+ for (size_t i = 0, j = 1; i < 200 && j > 0; i++, j = block_iter_next_line(&bi)) {
+ StringView line;
+ fill_line_ref(&bi, &line);
+ bool tab;
+ int indent = indent_len(line, bitset, &tab);
+ switch (indent) {
+ case -2: // Ignore mixed indent because tab width might not be 8
+ case -1: // Empty line; no change in indent
+ continue;
+ case 0:
+ current_indent = 0;
+ continue;
+ }
+
+ BUG_ON(indent <= 0);
+ int change = indent - current_indent;
+ if (change >= 1 && change <= INDENT_WIDTH_MAX) {
+ counts[change]++;
+ }
+
+ if (tab) {
+ tab_count++;
+ } else {
+ space_count++;
+ }
+ current_indent = indent;
+ }
+
+ if (tab_count == 0 && space_count == 0) {
+ return false;
+ }
+
+ if (tab_count > space_count) {
+ options->emulate_tab = false;
+ options->expand_tab = false;
+ options->indent_width = options->tab_width;
+ return true;
+ }
+
+ size_t m = 0;
+ for (size_t i = 1; i < ARRAYLEN(counts); i++) {
+ unsigned int bit = 1u << (i - 1);
+ if ((bitset & bit) && counts[i] > counts[m]) {
+ m = i;
+ }
+ }
+
+ if (m == 0) {
+ return false;
+ }
+
+ options->emulate_tab = true;
+ options->expand_tab = true;
+ options->indent_width = m;
+ return true;
+}
+
+void buffer_setup(EditorState *e, Buffer *buffer)
+{
+ const char *filename = buffer->abs_filename;
+ buffer->setup = true;
+ buffer_detect_filetype(buffer, &e->filetypes);
+ set_file_options(e, buffer);
+ set_editorconfig_options(buffer);
+ buffer_update_syntax(e, buffer);
+ if (buffer->options.detect_indent && filename) {
+ detect_indent(buffer);
+ }
+ sanity_check_local_options(&buffer->options);
+}
+
+void buffer_count_blocks_and_bytes(const Buffer *buffer, uintmax_t counts[2])
+{
+ uintmax_t blocks = 0;
+ uintmax_t bytes = 0;
+ Block *blk;
+ block_for_each(blk, &buffer->blocks) {
+ blocks += 1;
+ bytes += blk->size;
+ }
+ counts[0] = blocks;
+ counts[1] = bytes;
+}
+
+// TODO: Human-readable size (MiB/GiB/etc.) for "Bytes" and FileInfo::size
+String dump_buffer(const Buffer *buffer)
+{
+ uintmax_t counts[2];
+ buffer_count_blocks_and_bytes(buffer, counts);
+ BUG_ON(counts[0] < 1);
+ BUG_ON(!buffer->setup);
+ String buf = string_new(1024);
+
+ string_sprintf (
+ &buf,
+ "%s %s\n%s %lu\n%s %s\n%s %s\n%s %ju\n%s %zu\n%s %ju\n",
+ " Name:", buffer_filename(buffer),
+ " ID:", buffer->id,
+ " Encoding:", buffer->encoding.name,
+ " Filetype:", buffer->options.filetype,
+ " Blocks:", counts[0],
+ " Lines:", buffer->nl,
+ " Bytes:", counts[1]
+ );
+
+ if (
+ buffer->stdout_buffer || buffer->temporary || buffer->readonly
+ || buffer->locked || buffer->crlf_newlines || buffer->bom
+ ) {
+ string_sprintf (
+ &buf,
+ " Flags:%s%s%s%s%s%s\n",
+ buffer->stdout_buffer ? " STDOUT" : "",
+ buffer->temporary ? " TMP" : "",
+ buffer->readonly ? " RO" : "",
+ buffer->locked ? " LOCKED" : "",
+ buffer->crlf_newlines ? " CRLF" : "",
+ buffer->bom ? " BOM" : ""
+ );
+ }
+
+ if (buffer->views.count > 1) {
+ string_sprintf(&buf, " Views: %zu\n", buffer->views.count);
+ }
+
+ if (!buffer->abs_filename) {
+ return buf;
+ }
+
+ const FileInfo *file = &buffer->file;
+ unsigned int perms = file->mode & 07777;
+ char modestr[12];
+ char timestr[64];
+ if (!timespec_to_str(&file->mtime, timestr, sizeof(timestr))) {
+ memcpy(timestr, STRN("[error]") + 1);
+ }
+
+ string_sprintf (
+ &buf,
+ "\nLast stat:\n----------\n\n"
+ "%s %s\n%s %s\n%s -%s (%04o)\n%s %jd\n%s %jd\n%s %ju\n%s %jd\n%s %ju\n",
+ " Path:", buffer->abs_filename,
+ " Modified:", timestr,
+ " Mode:", filemode_to_str(file->mode, modestr), perms,
+ " User:", (intmax_t)file->uid,
+ " Group:", (intmax_t)file->gid,
+ " Size:", (uintmax_t)file->size,
+ " Device:", (intmax_t)file->dev,
+ " Inode:", (uintmax_t)file->ino
+ );
+
+ return buf;
+}
diff --git a/examples/dte/buffer.h b/examples/dte/buffer.h
new file mode 100644
index 0000000..4450a8f
--- /dev/null
+++ b/examples/dte/buffer.h
@@ -0,0 +1,103 @@
+#ifndef BUFFER_H
+#define BUFFER_H
+
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <time.h>
+#include "block-iter.h"
+#include "change.h"
+#include "encoding.h"
+#include "options.h"
+#include "syntax/syntax.h"
+#include "util/list.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "util/string.h"
+
+// Subset of stat(3) struct
+typedef struct {
+ dev_t dev;
+ ino_t ino;
+ mode_t mode;
+ uid_t uid;
+ gid_t gid;
+ off_t size;
+ struct timespec mtime;
+} FileInfo;
+
+// A representation of a specific file, as it pertains to editing,
+// including text contents, filename (if saved), undo history and
+// some file-specific metadata and options.
+typedef struct Buffer {
+ ListHead blocks;
+ Change change_head;
+ Change *cur_change;
+ Change *saved_change; // Used to determine if buffer is modified
+ FileInfo file;
+ unsigned long id; // Needed for identifying buffers whose filename is NULL
+ size_t nl;
+ PointerArray views; // Views pointing to this buffer
+ char *display_filename;
+ char *abs_filename;
+ bool readonly;
+ bool temporary;
+ bool stdout_buffer;
+ bool locked;
+ bool setup;
+ bool crlf_newlines;
+ bool bom;
+ Encoding encoding; // Encoding of the file (buffer always contains UTF-8)
+ LocalOptions options;
+ Syntax *syn;
+ long changed_line_min;
+ long changed_line_max;
+ // Index 0 is always syn->states.ptrs[0].
+ // Lowest bit of an invalidated value is 1.
+ PointerArray line_start_states;
+} Buffer;
+
+static inline void mark_all_lines_changed(Buffer *buffer)
+{
+ buffer->changed_line_min = 0;
+ buffer->changed_line_max = LONG_MAX;
+}
+
+static inline bool buffer_modified(const Buffer *buffer)
+{
+ return buffer->saved_change != buffer->cur_change && !buffer->temporary;
+}
+
+static inline BlockIter block_iter(Buffer *buffer)
+{
+ return (BlockIter) {
+ .blk = BLOCK(buffer->blocks.next),
+ .head = &buffer->blocks,
+ .offset = 0
+ };
+}
+
+struct EditorState;
+
+void buffer_mark_lines_changed(Buffer *buffer, long min, long max) NONNULL_ARGS;
+void buffer_set_encoding(Buffer *buffer, Encoding encoding, bool utf8_bom) NONNULL_ARGS;
+const char *buffer_filename(const Buffer *buffer) NONNULL_ARGS_AND_RETURN;
+void set_display_filename(Buffer *buffer, char *name) NONNULL_ARG(1);
+void update_short_filename_cwd(Buffer *buffer, const StringView *home, const char *cwd) NONNULL_ARG(1, 2);
+void update_short_filename(Buffer *buffer, const StringView *home) NONNULL_ARGS;
+Buffer *find_buffer(const PointerArray *buffers, const char *abs_filename) NONNULL_ARGS;
+Buffer *find_buffer_by_id(const PointerArray *buffers, unsigned long id) NONNULL_ARGS;
+Buffer *buffer_new(PointerArray *buffers, const GlobalOptions *gopts, const Encoding *encoding) RETURNS_NONNULL NONNULL_ARG(1, 2);
+Buffer *open_empty_buffer(PointerArray *buffers, const GlobalOptions *gopts) NONNULL_ARGS_AND_RETURN;
+void free_buffer(Buffer *buffer) NONNULL_ARGS;
+void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer) NONNULL_ARGS;
+void free_blocks(Buffer *buffer) NONNULL_ARGS;
+bool buffer_detect_filetype(Buffer *buffer, const PointerArray *filetypes) NONNULL_ARGS;
+void buffer_update_syntax(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
+void buffer_setup(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
+void buffer_count_blocks_and_bytes(const Buffer *buffer, uintmax_t counts[2]) NONNULL_ARGS;
+String dump_buffer(const Buffer *buffer) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/change.c b/examples/dte/change.c
new file mode 100644
index 0000000..529036d
--- /dev/null
+++ b/examples/dte/change.c
@@ -0,0 +1,417 @@
+#include <stdlib.h>
+#include <string.h>
+#include "change.h"
+#include "buffer.h"
+#include "edit.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/xmalloc.h"
+
+static ChangeMergeEnum change_merge;
+static ChangeMergeEnum prev_change_merge;
+
+static Change *alloc_change(void)
+{
+ return xcalloc(sizeof(Change));
+}
+
+static void add_change(Buffer *buffer, Change *change)
+{
+ Change *head = buffer->cur_change;
+ change->next = head;
+ xrenew(head->prev, head->nr_prev + 1);
+ head->prev[head->nr_prev++] = change;
+ buffer->cur_change = change;
+}
+
+// This doesn't need to be local to buffer because commands are atomic
+static Change *change_barrier;
+
+static bool is_change_chain_barrier(const Change *change)
+{
+ return !change->ins_count && !change->del_count;
+}
+
+static Change *new_change(Buffer *buffer)
+{
+ if (change_barrier) {
+ /*
+ * We are recording series of changes (:replace for example)
+ * and now we have just made the first change so we have to
+ * mark beginning of the chain.
+ *
+ * We could have done this before when starting the change
+ * chain but then we may have ended up with an empty chain.
+ * We don't want to record empty changes ever.
+ */
+ add_change(buffer, change_barrier);
+ change_barrier = NULL;
+ }
+
+ Change *change = alloc_change();
+ add_change(buffer, change);
+ return change;
+}
+
+static size_t buffer_offset(const View *view)
+{
+ return block_iter_get_offset(&view->cursor);
+}
+
+static void record_insert(View *view, size_t len)
+{
+ Change *change = view->buffer->cur_change;
+ BUG_ON(!len);
+ if (
+ change_merge == prev_change_merge
+ && change_merge == CHANGE_MERGE_INSERT
+ ) {
+ BUG_ON(change->del_count);
+ change->ins_count += len;
+ return;
+ }
+
+ change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->ins_count = len;
+}
+
+static void record_delete(View *view, char *buf, size_t len, bool move_after)
+{
+ BUG_ON(!len);
+ BUG_ON(!buf);
+
+ Change *change = view->buffer->cur_change;
+ if (change_merge == prev_change_merge) {
+ if (change_merge == CHANGE_MERGE_DELETE) {
+ xrenew(change->buf, change->del_count + len);
+ memcpy(change->buf + change->del_count, buf, len);
+ change->del_count += len;
+ free(buf);
+ return;
+ }
+ if (change_merge == CHANGE_MERGE_ERASE) {
+ xrenew(buf, len + change->del_count);
+ memcpy(buf + len, change->buf, change->del_count);
+ change->del_count += len;
+ free(change->buf);
+ change->buf = buf;
+ change->offset -= len;
+ return;
+ }
+ }
+
+ change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->del_count = len;
+ change->move_after = move_after;
+ change->buf = buf;
+}
+
+static void record_replace(View *view, char *deleted, size_t del_count, size_t ins_count)
+{
+ BUG_ON(del_count && !deleted);
+ BUG_ON(!del_count && deleted);
+ BUG_ON(!del_count && !ins_count);
+
+ Change *change = new_change(view->buffer);
+ change->offset = buffer_offset(view);
+ change->ins_count = ins_count;
+ change->del_count = del_count;
+ change->buf = deleted;
+}
+
+void begin_change(ChangeMergeEnum m)
+{
+ change_merge = m;
+}
+
+void end_change(void)
+{
+ prev_change_merge = change_merge;
+}
+
+void begin_change_chain(void)
+{
+ BUG_ON(change_barrier);
+
+ // Allocate change chain barrier but add it to the change tree only if
+ // there will be any real changes
+ change_barrier = alloc_change();
+ change_merge = CHANGE_MERGE_NONE;
+}
+
+void end_change_chain(View *view)
+{
+ if (change_barrier) {
+ // There were no changes in this change chain
+ free(change_barrier);
+ change_barrier = NULL;
+ } else {
+ // There were some changes; add end of chain marker
+ add_change(view->buffer, alloc_change());
+ }
+}
+
+static void fix_cursors(const View *view, size_t offset, size_t del, size_t ins)
+{
+ const Buffer *buffer = view->buffer;
+ for (size_t i = 0, n = buffer->views.count; i < n; i++) {
+ View *v = buffer->views.ptrs[i];
+ if (v != view && offset < v->saved_cursor_offset) {
+ if (offset + del <= v->saved_cursor_offset) {
+ v->saved_cursor_offset -= del;
+ v->saved_cursor_offset += ins;
+ } else {
+ v->saved_cursor_offset = offset;
+ }
+ }
+ }
+}
+
+static void reverse_change(View *view, Change *change)
+{
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, change->offset, change->ins_count, change->del_count);
+ }
+
+ block_iter_goto_offset(&view->cursor, change->offset);
+ if (!change->ins_count) {
+ // Convert delete to insert
+ do_insert(view, change->buf, change->del_count);
+ if (change->move_after) {
+ block_iter_skip_bytes(&view->cursor, change->del_count);
+ }
+ change->ins_count = change->del_count;
+ change->del_count = 0;
+ free(change->buf);
+ change->buf = NULL;
+ } else if (change->del_count) {
+ // Reverse replace
+ size_t del_count = change->ins_count;
+ size_t ins_count = change->del_count;
+ char *buf = do_replace(view, del_count, change->buf, ins_count);
+ free(change->buf);
+ change->buf = buf;
+ change->ins_count = ins_count;
+ change->del_count = del_count;
+ } else {
+ // Convert insert to delete
+ change->buf = do_delete(view, change->ins_count, true);
+ change->del_count = change->ins_count;
+ change->ins_count = 0;
+ }
+}
+
+bool undo(View *view)
+{
+ Change *change = view->buffer->cur_change;
+ view_reset_preferred_x(view);
+ if (!change->next) {
+ return false;
+ }
+
+ if (is_change_chain_barrier(change)) {
+ unsigned long count = 0;
+ while (1) {
+ change = change->next;
+ if (is_change_chain_barrier(change)) {
+ break;
+ }
+ reverse_change(view, change);
+ count++;
+ }
+ if (count > 1) {
+ info_msg("Undid %lu changes", count);
+ }
+ } else {
+ reverse_change(view, change);
+ }
+
+ view->buffer->cur_change = change->next;
+ return true;
+}
+
+bool redo(View *view, unsigned long change_id)
+{
+ Change *change = view->buffer->cur_change;
+ view_reset_preferred_x(view);
+ if (!change->prev) {
+ // Don't complain if change_id is 0
+ if (change_id) {
+ error_msg("Nothing to redo");
+ }
+ return false;
+ }
+
+ const unsigned long nr_prev = change->nr_prev;
+ BUG_ON(nr_prev == 0);
+ if (change_id == 0) {
+ // Default to newest change
+ change_id = nr_prev - 1;
+ if (nr_prev > 1) {
+ unsigned long i = change_id + 1;
+ info_msg("Redoing newest (%lu) of %lu possible changes", i, nr_prev);
+ }
+ } else {
+ if (--change_id >= nr_prev) {
+ if (nr_prev == 1) {
+ return error_msg("There is only 1 possible change to redo");
+ }
+ return error_msg("There are only %lu possible changes to redo", nr_prev);
+ }
+ }
+
+ change = change->prev[change_id];
+ if (is_change_chain_barrier(change)) {
+ unsigned long count = 0;
+ while (1) {
+ change = change->prev[change->nr_prev - 1];
+ if (is_change_chain_barrier(change)) {
+ break;
+ }
+ reverse_change(view, change);
+ count++;
+ }
+ if (count > 1) {
+ info_msg("Redid %lu changes", count);
+ }
+ } else {
+ reverse_change(view, change);
+ }
+
+ view->buffer->cur_change = change;
+ return true;
+}
+
+void free_changes(Change *c)
+{
+top:
+ while (c->nr_prev) {
+ c = c->prev[c->nr_prev - 1];
+ }
+
+ // c is leaf now
+ while (c->next) {
+ Change *next = c->next;
+ free(c->buf);
+ free(c);
+
+ c = next;
+ if (--c->nr_prev) {
+ goto top;
+ }
+
+ // We have become leaf
+ free(c->prev);
+ }
+}
+
+void buffer_insert_bytes(View *view, const char *buf, const size_t len)
+{
+ view_reset_preferred_x(view);
+ if (len == 0) {
+ return;
+ }
+
+ size_t rec_len = len;
+ if (buf[len - 1] != '\n' && block_iter_is_eof(&view->cursor)) {
+ // Force newline at EOF
+ do_insert(view, "\n", 1);
+ rec_len++;
+ }
+
+ do_insert(view, buf, len);
+ record_insert(view, rec_len);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
+ }
+}
+
+static bool would_delete_last_bytes(const View *view, size_t count)
+{
+ const Block *blk = view->cursor.blk;
+ size_t offset = view->cursor.offset;
+ while (1) {
+ size_t avail = blk->size - offset;
+ if (avail > count) {
+ return false;
+ }
+
+ if (blk->node.next == view->cursor.head) {
+ return true;
+ }
+
+ count -= avail;
+ blk = BLOCK(blk->node.next);
+ offset = 0;
+ }
+}
+
+static void buffer_delete_bytes_internal(View *view, size_t len, bool move_after)
+{
+ view_reset_preferred_x(view);
+ if (len == 0) {
+ return;
+ }
+
+ // Check if all newlines from EOF would be deleted
+ if (would_delete_last_bytes(view, len)) {
+ BlockIter bi = view->cursor;
+ CodePoint u;
+ if (block_iter_prev_char(&bi, &u) && u != '\n') {
+ // No newline before cursor
+ if (--len == 0) {
+ begin_change(CHANGE_MERGE_NONE);
+ return;
+ }
+ }
+ }
+ record_delete(view, do_delete(view, len, true), len, move_after);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
+ }
+}
+
+void buffer_delete_bytes(View *view, size_t len)
+{
+ buffer_delete_bytes_internal(view, len, false);
+}
+
+void buffer_erase_bytes(View *view, size_t len)
+{
+ buffer_delete_bytes_internal(view, len, true);
+}
+
+void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count)
+{
+ view_reset_preferred_x(view);
+ if (del_count == 0) {
+ buffer_insert_bytes(view, ins, ins_count);
+ return;
+ }
+ if (ins_count == 0) {
+ buffer_delete_bytes(view, del_count);
+ return;
+ }
+
+ // Check if all newlines from EOF would be deleted
+ if (would_delete_last_bytes(view, del_count)) {
+ if (ins[ins_count - 1] != '\n') {
+ // Don't replace last newline
+ if (--del_count == 0) {
+ buffer_insert_bytes(view, ins, ins_count);
+ return;
+ }
+ }
+ }
+
+ char *deleted = do_replace(view, del_count, ins, ins_count);
+ record_replace(view, deleted, del_count, ins_count);
+
+ if (view->buffer->views.count > 1) {
+ fix_cursors(view, block_iter_get_offset(&view->cursor), del_count, ins_count);
+ }
+}
diff --git a/examples/dte/change.h b/examples/dte/change.h
new file mode 100644
index 0000000..a0d08f1
--- /dev/null
+++ b/examples/dte/change.h
@@ -0,0 +1,39 @@
+#ifndef CHANGE_H
+#define CHANGE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+#include "view.h"
+
+typedef enum {
+ CHANGE_MERGE_NONE,
+ CHANGE_MERGE_INSERT,
+ CHANGE_MERGE_DELETE,
+ CHANGE_MERGE_ERASE,
+} ChangeMergeEnum;
+
+typedef struct Change {
+ struct Change *next;
+ struct Change **prev;
+ unsigned long nr_prev;
+ bool move_after; // Move after inserted text when undoing delete?
+ size_t offset;
+ size_t del_count;
+ size_t ins_count;
+ char *buf; // Deleted bytes (inserted bytes need not be saved)
+} Change;
+
+void begin_change(ChangeMergeEnum m);
+void end_change(void);
+void begin_change_chain(void);
+void end_change_chain(View *view) NONNULL_ARGS;
+bool undo(View *view) NONNULL_ARGS WARN_UNUSED_RESULT;
+bool redo(View *view, unsigned long change_id) NONNULL_ARGS WARN_UNUSED_RESULT;
+void free_changes(Change *c) NONNULL_ARGS;
+void buffer_insert_bytes(View *view, const char *buf, size_t len) NONNULL_ARG(1);
+void buffer_delete_bytes(View *view, size_t len) NONNULL_ARGS;
+void buffer_erase_bytes(View *view, size_t len) NONNULL_ARGS;
+void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count) NONNULL_ARG(1);
+
+#endif
diff --git a/examples/dte/cmdline.c b/examples/dte/cmdline.c
new file mode 100644
index 0000000..8e57604
--- /dev/null
+++ b/examples/dte/cmdline.c
@@ -0,0 +1,540 @@
+#include <stdlib.h>
+#include <string.h>
+#include "cmdline.h"
+#include "command/args.h"
+#include "command/macro.h"
+#include "commands.h"
+#include "completion.h"
+#include "copy.h"
+#include "editor.h"
+#include "history.h"
+#include "options.h"
+#include "search.h"
+#include "terminal/osc52.h"
+#include "util/ascii.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/log.h"
+#include "util/utf8.h"
+
+static void cmdline_delete(CommandLine *c)
+{
+ size_t pos = c->pos;
+ size_t len = 1;
+
+ if (pos == c->buf.len) {
+ return;
+ }
+
+ u_get_char(c->buf.buffer, c->buf.len, &pos);
+ len = pos - c->pos;
+ string_remove(&c->buf, c->pos, len);
+}
+
+void cmdline_clear(CommandLine *c)
+{
+ string_clear(&c->buf);
+ c->pos = 0;
+ c->search_pos = NULL;
+}
+
+void cmdline_free(CommandLine *c)
+{
+ cmdline_clear(c);
+ string_free(&c->buf);
+ free(c->search_text);
+ reset_completion(c);
+}
+
+static void set_text(CommandLine *c, const char *text)
+{
+ string_clear(&c->buf);
+ const size_t text_len = strlen(text);
+ c->pos = text_len;
+ string_append_buf(&c->buf, text, text_len);
+}
+
+void cmdline_set_text(CommandLine *c, const char *text)
+{
+ c->search_pos = NULL;
+ set_text(c, text);
+}
+
+static bool cmd_bol(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ e->cmdline.pos = 0;
+ reset_completion(&e->cmdline);
+ return true;
+}
+
+static bool cmd_cancel(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ cmdline_clear(c);
+ set_input_mode(e, INPUT_NORMAL);
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_clear(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ cmdline_clear(&e->cmdline);
+ return true;
+}
+
+static bool cmd_copy(EditorState *e, const CommandArgs *a)
+{
+ bool internal = cmdargs_has_flag(a, 'i') || a->flag_set == 0;
+ bool clipboard = cmdargs_has_flag(a, 'b');
+ bool primary = cmdargs_has_flag(a, 'p');
+
+ String *buf = &e->cmdline.buf;
+ size_t len = buf->len;
+ if (internal) {
+ char *str = string_clone_cstring(buf);
+ record_copy(&e->clipboard, str, len, false);
+ }
+
+ Terminal *term = &e->terminal;
+ if ((clipboard || primary) && term->features & TFLAG_OSC52_COPY) {
+ const char *str = string_borrow_cstring(buf);
+ if (!term_osc52_copy(&term->obuf, str, len, clipboard, primary)) {
+ LOG_ERRNO("term_osc52_copy");
+ // TODO: return false ?
+ }
+ }
+
+ return true;
+}
+
+static bool cmd_delete(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ cmdline_delete(c);
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_delete_eol(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ c->buf.len = c->pos;
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_delete_word(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ const unsigned char *buf = c->buf.buffer;
+ const size_t len = c->buf.len;
+ size_t i = c->pos;
+
+ if (i == len) {
+ return true;
+ }
+
+ while (i < len && is_word_byte(buf[i])) {
+ i++;
+ }
+
+ while (i < len && !is_word_byte(buf[i])) {
+ i++;
+ }
+
+ string_remove(&c->buf, c->pos, i - c->pos);
+
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_eol(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ c->pos = c->buf.len;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_erase(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ if (c->pos > 0) {
+ u_prev_char(c->buf.buffer, &c->pos);
+ cmdline_delete(c);
+ }
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_erase_bol(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ string_remove(&c->buf, 0, c->pos);
+ c->pos = 0;
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_erase_word(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ size_t i = c->pos;
+ if (i == 0) {
+ return true;
+ }
+
+ // open /path/to/file^W => open /path/to/
+
+ // erase whitespace
+ while (i && ascii_isspace(c->buf.buffer[i - 1])) {
+ i--;
+ }
+
+ // erase non-word bytes
+ while (i && !is_word_byte(c->buf.buffer[i - 1])) {
+ i--;
+ }
+
+ // erase word bytes
+ while (i && is_word_byte(c->buf.buffer[i - 1])) {
+ i--;
+ }
+
+ string_remove(&c->buf, i, c->pos - i);
+ c->pos = i;
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool do_history_prev(const History *hist, CommandLine *c)
+{
+ if (!c->search_pos) {
+ free(c->search_text);
+ c->search_text = string_clone_cstring(&c->buf);
+ }
+
+ if (history_search_forward(hist, &c->search_pos, c->search_text)) {
+ BUG_ON(!c->search_pos);
+ set_text(c, c->search_pos->text);
+ }
+
+ reset_completion(c);
+ return true;
+}
+
+static bool do_history_next(const History *hist, CommandLine *c)
+{
+ if (!c->search_pos) {
+ goto out;
+ }
+
+ if (history_search_backward(hist, &c->search_pos, c->search_text)) {
+ BUG_ON(!c->search_pos);
+ set_text(c, c->search_pos->text);
+ } else {
+ set_text(c, c->search_text);
+ c->search_pos = NULL;
+ }
+
+out:
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_search_history_next(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ return do_history_next(&e->search_history, &e->cmdline);
+}
+
+static bool cmd_search_history_prev(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ return do_history_prev(&e->search_history, &e->cmdline);
+}
+
+static bool cmd_command_history_next(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ return do_history_next(&e->command_history, &e->cmdline);
+}
+
+static bool cmd_command_history_prev(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ return do_history_prev(&e->command_history, &e->cmdline);
+}
+
+static bool cmd_left(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ if (c->pos) {
+ u_prev_char(c->buf.buffer, &c->pos);
+ }
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_paste(EditorState *e, const CommandArgs *a)
+{
+ CommandLine *c = &e->cmdline;
+ const Clipboard *clip = &e->clipboard;
+ string_insert_buf(&c->buf, c->pos, clip->buf, clip->len);
+ if (cmdargs_has_flag(a, 'm')) {
+ c->pos += clip->len;
+ }
+ c->search_pos = NULL;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_right(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ if (c->pos < c->buf.len) {
+ u_get_char(c->buf.buffer, c->buf.len, &c->pos);
+ }
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_toggle(EditorState *e, const CommandArgs *a)
+{
+ const char *option_name = a->args[0];
+ bool global = cmdargs_has_flag(a, 'g');
+ size_t nr_values = a->nr_args - 1;
+ if (nr_values == 0) {
+ return toggle_option(e, option_name, global, false);
+ }
+
+ char **values = a->args + 1;
+ return toggle_option_values(e, option_name, global, false, values, nr_values);
+}
+
+static bool cmd_word_bwd(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ if (c->pos <= 1) {
+ c->pos = 0;
+ return true;
+ }
+
+ const unsigned char *const buf = c->buf.buffer;
+ size_t i = c->pos - 1;
+
+ while (i > 0 && !is_word_byte(buf[i])) {
+ i--;
+ }
+
+ while (i > 0 && is_word_byte(buf[i])) {
+ i--;
+ }
+
+ if (i > 0) {
+ i++;
+ }
+
+ c->pos = i;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_word_fwd(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ const unsigned char *buf = c->buf.buffer;
+ const size_t len = c->buf.len;
+ size_t i = c->pos;
+
+ while (i < len && is_word_byte(buf[i])) {
+ i++;
+ }
+
+ while (i < len && !is_word_byte(buf[i])) {
+ i++;
+ }
+
+ c->pos = i;
+ reset_completion(c);
+ return true;
+}
+
+static bool cmd_complete_next(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ complete_command_next(e);
+ return true;
+}
+
+static bool cmd_complete_prev(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ complete_command_prev(e);
+ return true;
+}
+
+static bool cmd_direction(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ toggle_search_direction(&e->search);
+ return true;
+}
+
+static bool cmd_command_mode_accept(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ CommandLine *c = &e->cmdline;
+ reset_completion(c);
+ set_input_mode(e, INPUT_NORMAL);
+
+ const char *str = string_borrow_cstring(&c->buf);
+ cmdline_clear(c);
+ if (!cmdargs_has_flag(a, 'H') && str[0] != ' ') {
+ // This is done before handle_command() because "command [text]"
+ // can modify the contents of the command-line
+ history_add(&e->command_history, str);
+ }
+
+ current_command = NULL;
+ return handle_normal_command(e, str, true);
+}
+
+static bool cmd_search_mode_accept(EditorState *e, const CommandArgs *a)
+{
+ CommandLine *c = &e->cmdline;
+ if (cmdargs_has_flag(a, 'e')) {
+ if (c->buf.len == 0) {
+ return true;
+ }
+ // Escape the regex; to match as plain text
+ char *original = string_clone_cstring(&c->buf);
+ size_t len = c->buf.len;
+ string_clear(&c->buf);
+ for (size_t i = 0; i < len; i++) {
+ char ch = original[i];
+ if (is_regex_special_char(ch)) {
+ string_append_byte(&c->buf, '\\');
+ }
+ string_append_byte(&c->buf, ch);
+ }
+ free(original);
+ }
+
+ const char *str = NULL;
+ bool add_to_history = !cmdargs_has_flag(a, 'H');
+ if (c->buf.len > 0) {
+ str = string_borrow_cstring(&c->buf);
+ BUG_ON(!str);
+ search_set_regexp(&e->search, str);
+ if (add_to_history) {
+ history_add(&e->search_history, str);
+ }
+ }
+
+ if (e->macro.recording) {
+ const char *args[5];
+ size_t i = 0;
+ if (str) {
+ if (e->search.reverse) {
+ args[i++] = "-r";
+ }
+ if (!add_to_history) {
+ args[i++] = "-H";
+ }
+ if (unlikely(str[0] == '-')) {
+ args[i++] = "--";
+ }
+ args[i++] = str;
+ } else {
+ args[i++] = e->search.reverse ? "-p" : "-n";
+ }
+ args[i] = NULL;
+ macro_command_hook(&e->macro, "search", (char**)args);
+ }
+
+ current_command = NULL;
+ bool found = search_next(e->view, &e->search, e->options.case_sensitive_search);
+ cmdline_clear(c);
+ set_input_mode(e, INPUT_NORMAL);
+ return found;
+}
+
+IGNORE_WARNING("-Wincompatible-pointer-types")
+
+static const Command common_cmds[] = {
+ {"bol", "", false, 0, 0, cmd_bol},
+ {"cancel", "", false, 0, 0, cmd_cancel},
+ {"clear", "", false, 0, 0, cmd_clear},
+ {"copy", "bip", false, 0, 0, cmd_copy},
+ {"delete", "", false, 0, 0, cmd_delete},
+ {"delete-eol", "", false, 0, 0, cmd_delete_eol},
+ {"delete-word", "", false, 0, 0, cmd_delete_word},
+ {"eol", "", false, 0, 0, cmd_eol},
+ {"erase", "", false, 0, 0, cmd_erase},
+ {"erase-bol", "", false, 0, 0, cmd_erase_bol},
+ {"erase-word", "", false, 0, 0, cmd_erase_word},
+ {"left", "", false, 0, 0, cmd_left},
+ {"paste", "m", false, 0, 0, cmd_paste},
+ {"right", "", false, 0, 0, cmd_right},
+ {"toggle", "g", false, 1, -1, cmd_toggle},
+ {"word-bwd", "", false, 0, 0, cmd_word_bwd},
+ {"word-fwd", "", false, 0, 0, cmd_word_fwd},
+};
+
+static const Command search_cmds[] = {
+ {"accept", "eH", false, 0, 0, cmd_search_mode_accept},
+ {"direction", "", false, 0, 0, cmd_direction},
+ {"history-next", "", false, 0, 0, cmd_search_history_next},
+ {"history-prev", "", false, 0, 0, cmd_search_history_prev},
+};
+
+static const Command command_cmds[] = {
+ {"accept", "H", false, 0, 0, cmd_command_mode_accept},
+ {"complete-next", "", false, 0, 0, cmd_complete_next},
+ {"complete-prev", "", false, 0, 0, cmd_complete_prev},
+ {"history-next", "", false, 0, 0, cmd_command_history_next},
+ {"history-prev", "", false, 0, 0, cmd_command_history_prev},
+};
+
+UNIGNORE_WARNINGS
+
+static const Command *find_cmd_mode_command(const char *name)
+{
+ const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
+ return cmd ? cmd : BSEARCH(name, command_cmds, command_cmp);
+}
+
+static const Command *find_search_mode_command(const char *name)
+{
+ const Command *cmd = BSEARCH(name, common_cmds, command_cmp);
+ return cmd ? cmd : BSEARCH(name, search_cmds, command_cmp);
+}
+
+const CommandSet cmd_mode_commands = {
+ .lookup = find_cmd_mode_command
+};
+
+const CommandSet search_mode_commands = {
+ .lookup = find_search_mode_command
+};
diff --git a/examples/dte/cmdline.h b/examples/dte/cmdline.h
new file mode 100644
index 0000000..70cc7a5
--- /dev/null
+++ b/examples/dte/cmdline.h
@@ -0,0 +1,40 @@
+#ifndef CMDLINE_H
+#define CMDLINE_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include "command/run.h"
+#include "history.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "util/string.h"
+
+typedef struct {
+ char *orig; // Full cmdline string (backing buffer for `escaped` and `tail`)
+ char *parsed; // Result of passing `escaped` through parse_command_arg()
+ StringView escaped; // Middle part of `orig` (string to be replaced)
+ StringView tail; // Suffix part of `orig` (after `escaped`)
+ size_t head_len; // Length of prefix part of `orig` (before `escaped`)
+ PointerArray completions; // Array of completion candidates
+ size_t idx; // Index of currently selected completion
+ bool add_space_after_single_match;
+ bool tilde_expanded;
+} CompletionState;
+
+typedef struct {
+ String buf;
+ size_t pos;
+ const HistoryEntry *search_pos;
+ char *search_text;
+ CompletionState completion;
+} CommandLine;
+
+extern const CommandSet cmd_mode_commands;
+extern const CommandSet search_mode_commands;
+
+void cmdline_set_text(CommandLine *c, const char *text) NONNULL_ARGS;
+void cmdline_clear(CommandLine *c) NONNULL_ARGS;
+void cmdline_free(CommandLine *c) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/commands.c b/examples/dte/commands.c
new file mode 100644
index 0000000..8346309
--- /dev/null
+++ b/examples/dte/commands.c
@@ -0,0 +1,2594 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "commands.h"
+#include "bind.h"
+#include "bookmark.h"
+#include "buffer.h"
+#include "change.h"
+#include "cmdline.h"
+#include "command/alias.h"
+#include "command/args.h"
+#include "command/macro.h"
+#include "compiler.h"
+#include "config.h"
+#include "convert.h"
+#include "copy.h"
+#include "editor.h"
+#include "encoding.h"
+#include "error.h"
+#include "exec.h"
+#include "file-option.h"
+#include "filetype.h"
+#include "frame.h"
+#include "history.h"
+#include "load-save.h"
+#include "lock.h"
+#include "misc.h"
+#include "move.h"
+#include "msg.h"
+#include "regexp.h"
+#include "replace.h"
+#include "screen.h"
+#include "search.h"
+#include "selection.h"
+#include "shift.h"
+#include "show.h"
+#include "spawn.h"
+#include "syntax/color.h"
+#include "syntax/state.h"
+#include "syntax/syntax.h"
+#include "tag.h"
+#include "terminal/cursor.h"
+#include "terminal/mode.h"
+#include "terminal/osc52.h"
+#include "terminal/style.h"
+#include "terminal/terminal.h"
+#include "util/arith.h"
+#include "util/array.h"
+#include "util/ascii.h"
+#include "util/bit.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/log.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/strtonum.h"
+#include "util/time-util.h"
+#include "util/xmalloc.h"
+#include "util/xsnprintf.h"
+#include "vars.h"
+#include "view.h"
+#include "window.h"
+
+NOINLINE
+static void do_selection_noinline(View *view, SelectionType sel)
+{
+ // Should only be called from do_selection()
+ BUG_ON(sel == view->selection);
+
+ if (sel == SELECT_NONE) {
+ unselect(view);
+ return;
+ }
+
+ if (view->selection) {
+ if (view->selection != sel) {
+ view->selection = sel;
+ // TODO: be less brute force about this; only the first/last
+ // line of the selection can change in this case
+ mark_all_lines_changed(view->buffer);
+ }
+ return;
+ }
+
+ view->sel_so = block_iter_get_offset(&view->cursor);
+ view->sel_eo = SEL_EO_RECALC;
+ view->selection = sel;
+
+ // Need to mark current line changed because cursor might
+ // move up or down before screen is updated
+ view_update_cursor_y(view);
+ buffer_mark_lines_changed(view->buffer, view->cy, view->cy);
+}
+
+static void do_selection(View *view, SelectionType sel)
+{
+ if (likely(sel == view->selection)) {
+ // If `sel` is SELECT_NONE here, it's always equal to select_mode
+ BUG_ON(!sel && view->select_mode);
+ return;
+ }
+
+ do_selection_noinline(view, sel);
+}
+
+static char last_flag_or_default(const CommandArgs *a, char def)
+{
+ size_t n = a->nr_flags;
+ return n ? a->flags[n - 1] : def;
+}
+
+static char last_flag(const CommandArgs *a)
+{
+ return last_flag_or_default(a, 0);
+}
+
+static bool has_flag(const CommandArgs *a, unsigned char flag)
+{
+ return cmdargs_has_flag(a, flag);
+}
+
+static void handle_select_chars_or_lines_flags(View *view, const CommandArgs *a)
+{
+ SelectionType sel;
+ if (has_flag(a, 'l')) {
+ sel = SELECT_LINES;
+ } else if (has_flag(a, 'c')) {
+ static_assert(SELECT_CHARS < SELECT_LINES);
+ sel = MAX(SELECT_CHARS, view->select_mode);
+ } else {
+ sel = view->select_mode;
+ }
+ do_selection(view, sel);
+}
+
+static void handle_select_chars_flag(View *view, const CommandArgs *a)
+{
+ BUG_ON(has_flag(a, 'l'));
+ handle_select_chars_or_lines_flags(view, a);
+}
+
+static bool cmd_alias(EditorState *e, const CommandArgs *a)
+{
+ const char *const name = a->args[0];
+ const char *const cmd = a->args[1];
+
+ if (unlikely(name[0] == '\0')) {
+ return error_msg("Empty alias name not allowed");
+ }
+ if (unlikely(name[0] == '-')) {
+ // Disallowing this simplifies auto-completion for "alias "
+ return error_msg("Alias name cannot begin with '-'");
+ }
+
+ for (size_t i = 0; name[i]; i++) {
+ unsigned char c = name[i];
+ if (unlikely(!(is_word_byte(c) || c == '-' || c == '?' || c == '!'))) {
+ return error_msg("Invalid byte in alias name: %c (0x%02hhX)", c, c);
+ }
+ }
+
+ if (unlikely(find_normal_command(name))) {
+ return error_msg("Can't replace existing command %s with an alias", name);
+ }
+
+ if (likely(cmd)) {
+ add_alias(&e->aliases, name, cmd);
+ } else {
+ remove_alias(&e->aliases, name);
+ }
+
+ return true;
+}
+
+static bool cmd_bind(EditorState *e, const CommandArgs *a)
+{
+ const char *keystr = a->args[0];
+ const char *cmd = a->args[1];
+ KeyCode key;
+ if (unlikely(!parse_key_string(&key, keystr))) {
+ return error_msg("invalid key string: %s", keystr);
+ }
+
+ const bool modes[] = {
+ [INPUT_NORMAL] = a->nr_flags == 0 || has_flag(a, 'n'),
+ [INPUT_COMMAND] = has_flag(a, 'c'),
+ [INPUT_SEARCH] = has_flag(a, 's'),
+ };
+
+ static_assert(ARRAYLEN(modes) == ARRAYLEN(e->modes));
+
+ for (InputMode i = 0; i < ARRAYLEN(modes); i++) {
+ if (!modes[i]) {
+ continue;
+ }
+ IntMap *bindings = &e->modes[i].key_bindings;
+ if (likely(cmd)) {
+ CommandRunner runner = cmdrunner_for_mode(e, i, false);
+ add_binding(bindings, key, cached_command_new(&runner, cmd));
+ } else {
+ remove_binding(bindings, key);
+ }
+ }
+
+ return true;
+}
+
+static bool cmd_bof(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_or_lines_flags(e->view, a);
+ move_bof(e->view);
+ return true;
+}
+
+static bool cmd_bol(EditorState *e, const CommandArgs *a)
+{
+ static const FlagMapping map[] = {
+ {'s', BOL_SMART},
+ {'t', BOL_SMART | BOL_SMART_TOGGLE},
+ };
+
+ SmartBolFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
+ handle_select_chars_flag(e->view, a);
+ move_bol_smart(e->view, flags);
+ return true;
+}
+
+static bool cmd_bolsf(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ if (!block_iter_bol(&view->cursor)) {
+ unsigned int margin = e->options.scroll_margin;
+ long top = view->vy + window_get_scroll_margin(e->window, margin);
+ if (view->cy > top) {
+ move_up(view, view->cy - top);
+ } else {
+ block_iter_bof(&view->cursor);
+ }
+ }
+
+ view_reset_preferred_x(view);
+ return true;
+}
+
+static bool cmd_bookmark(EditorState *e, const CommandArgs *a)
+{
+ if (has_flag(a, 'r')) {
+ bookmark_pop(e->window, &e->bookmarks);
+ return true;
+ }
+
+ bookmark_push(&e->bookmarks, get_current_file_location(e->view));
+ return true;
+}
+
+static bool cmd_case(EditorState *e, const CommandArgs *a)
+{
+ change_case(e->view, last_flag_or_default(a, 't'));
+ return true;
+}
+
+static void mark_tabbar_changed(Window *window, void* UNUSED_ARG(data))
+{
+ window->update_tabbar = true;
+}
+
+static bool cmd_cd(EditorState *e, const CommandArgs *a)
+{
+ const char *dir = a->args[0];
+ if (unlikely(dir[0] == '\0')) {
+ return error_msg("directory argument cannot be empty");
+ }
+
+ if (streq(dir, "-")) {
+ dir = xgetenv("OLDPWD");
+ if (!dir) {
+ return error_msg("OLDPWD not set");
+ }
+ }
+
+ char buf[8192];
+ const char *cwd = getcwd(buf, sizeof(buf));
+ if (chdir(dir) != 0) {
+ return error_msg_errno("changing directory failed");
+ }
+
+ if (likely(cwd)) {
+ int r = setenv("OLDPWD", cwd, 1);
+ if (unlikely(r != 0)) {
+ LOG_WARNING("failed to set OLDPWD: %s", strerror(errno));
+ }
+ }
+
+ cwd = getcwd(buf, sizeof(buf));
+ if (likely(cwd)) {
+ int r = setenv("PWD", cwd, 1);
+ if (unlikely(r != 0)) {
+ LOG_WARNING("failed to set PWD: %s", strerror(errno));
+ }
+ }
+
+ for (size_t i = 0, n = e->buffers.count; i < n; i++) {
+ Buffer *buffer = e->buffers.ptrs[i];
+ update_short_filename_cwd(buffer, &e->home_dir, cwd);
+ }
+
+ frame_for_each_window(e->root_frame, mark_tabbar_changed, NULL);
+ return true;
+}
+
+static bool cmd_center_view(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ e->view->force_center = true;
+ return true;
+}
+
+static bool cmd_clear(EditorState *e, const CommandArgs *a)
+{
+ bool auto_indent = e->buffer->options.auto_indent && !has_flag(a, 'i');
+ clear_lines(e->view, auto_indent);
+ return true;
+}
+
+static bool cmd_close(EditorState *e, const CommandArgs *a)
+{
+ bool force = has_flag(a, 'f');
+ if (!force && !view_can_close(e->view)) {
+ bool prompt = has_flag(a, 'p');
+ if (!prompt) {
+ return error_msg (
+ "The buffer is modified; "
+ "save or run 'close -f' to close without saving"
+ );
+ }
+ static const char str[] = "Close without saving changes? [y/N]";
+ if (dialog_prompt(e, str, "ny") != 'y') {
+ return false;
+ }
+ }
+
+ bool allow_quit = has_flag(a, 'q');
+ if (allow_quit && e->buffers.count == 1 && e->root_frame->frames.count <= 1) {
+ e->status = EDITOR_EXIT_OK;
+ return true;
+ }
+
+ bool allow_wclose = has_flag(a, 'w');
+ if (allow_wclose && e->window->views.count <= 1) {
+ window_close(e->window);
+ return true;
+ }
+
+ window_close_current_view(e->window);
+ set_view(e->window->view);
+ return true;
+}
+
+static bool cmd_command(EditorState *e, const CommandArgs *a)
+{
+ const char *text = a->args[0];
+ set_input_mode(e, INPUT_COMMAND);
+ if (text) {
+ cmdline_set_text(&e->cmdline, text);
+ }
+ return true;
+}
+
+static bool cmd_compile(EditorState *e, const CommandArgs *a)
+{
+ static const FlagMapping map[] = {
+ {'1', SPAWN_READ_STDOUT},
+ {'p', SPAWN_PROMPT},
+ {'s', SPAWN_QUIET},
+ };
+
+ Compiler *c = find_compiler(&e->compilers, a->args[0]);
+ if (unlikely(!c)) {
+ return error_msg("No such error parser %s", a->args[0]);
+ }
+
+ SpawnContext ctx = {
+ .editor = e,
+ .argv = (const char **)a->args + 1,
+ .flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)),
+ };
+
+ clear_messages(&e->messages);
+ bool ok = spawn_compiler(&ctx, c, &e->messages);
+ if (e->messages.array.count) {
+ activate_current_message_save(e);
+ }
+ return ok;
+}
+
+static bool cmd_copy(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ const BlockIter save = view->cursor;
+ size_t size;
+ bool line_copy;
+ if (view->selection) {
+ size = prepare_selection(view);
+ line_copy = (view->selection == SELECT_LINES);
+ } else {
+ block_iter_bol(&view->cursor);
+ BlockIter tmp = view->cursor;
+ size = block_iter_eat_line(&tmp);
+ line_copy = true;
+ }
+
+ if (unlikely(size == 0)) {
+ return true;
+ }
+
+ bool internal = has_flag(a, 'i');
+ bool clipboard = has_flag(a, 'b');
+ bool primary = has_flag(a, 'p');
+ if (!(internal || clipboard || primary)) {
+ internal = true;
+ }
+
+ if (internal) {
+ copy(&e->clipboard, view, size, line_copy);
+ }
+
+ Terminal *term = &e->terminal;
+ if ((clipboard || primary) && term->features & TFLAG_OSC52_COPY) {
+ if (internal) {
+ view->cursor = save;
+ if (view->selection) {
+ size = prepare_selection(view);
+ }
+ }
+ char *buf = block_iter_get_bytes(&view->cursor, size);
+ if (!term_osc52_copy(&term->obuf, buf, size, clipboard, primary)) {
+ error_msg_errno("OSC 52 copy failed");
+ }
+ free(buf);
+ }
+
+ if (!has_flag(a, 'k')) {
+ unselect(view);
+ }
+
+ view->cursor = save;
+ // TODO: return false if term_osc52_copy() failed?
+ return true;
+}
+
+static bool cmd_cursor(EditorState *e, const CommandArgs *a)
+{
+ if (unlikely(a->nr_args == 0)) {
+ // Reset all cursor styles
+ for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) {
+ e->cursor_styles[m] = get_default_cursor_style(m);
+ }
+ e->cursor_style_changed = true;
+ return true;
+ }
+
+ CursorInputMode mode = cursor_mode_from_str(a->args[0]);
+ if (unlikely(mode >= NR_CURSOR_MODES)) {
+ return error_msg("invalid mode argument: %s", a->args[0]);
+ }
+
+ TermCursorStyle style = get_default_cursor_style(mode);
+ if (a->nr_args >= 2) {
+ style.type = cursor_type_from_str(a->args[1]);
+ if (unlikely(style.type == CURSOR_INVALID)) {
+ return error_msg("invalid cursor type: %s", a->args[1]);
+ }
+ }
+
+ if (a->nr_args >= 3) {
+ style.color = cursor_color_from_str(a->args[2]);
+ if (unlikely(style.color == COLOR_INVALID)) {
+ return error_msg("invalid cursor color: %s", a->args[2]);
+ }
+ }
+
+ e->cursor_styles[mode] = style;
+ e->cursor_style_changed = true;
+ return true;
+}
+
+static bool cmd_cut(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ View *view = e->view;
+ const long x = view_get_preferred_x(view);
+ if (view->selection) {
+ bool is_lines = view->selection == SELECT_LINES;
+ cut(&e->clipboard, view, prepare_selection(view), is_lines);
+ if (view->selection == SELECT_LINES) {
+ move_to_preferred_x(view, x);
+ }
+ unselect(view);
+ } else {
+ BlockIter tmp;
+ block_iter_bol(&view->cursor);
+ tmp = view->cursor;
+ cut(&e->clipboard, view, block_iter_eat_line(&tmp), true);
+ move_to_preferred_x(view, x);
+ }
+ return true;
+}
+
+static bool cmd_delete(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ delete_ch(e->view);
+ return true;
+}
+
+static bool cmd_delete_eol(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ if (view->selection) {
+ // TODO: return false?
+ return true;
+ }
+
+ bool delete_newline_if_at_eol = has_flag(a, 'n');
+ BlockIter bi = view->cursor;
+ if (delete_newline_if_at_eol) {
+ CodePoint ch;
+ if (block_iter_get_char(&view->cursor, &ch) == 1 && ch == '\n') {
+ delete_ch(view);
+ return true;
+ }
+ }
+
+ buffer_delete_bytes(view, block_iter_eol(&bi));
+ return true;
+}
+
+static bool cmd_delete_line(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ delete_lines(e->view);
+ return true;
+}
+
+static bool cmd_delete_word(EditorState *e, const CommandArgs *a)
+{
+ bool skip_non_word = has_flag(a, 's');
+ BlockIter bi = e->view->cursor;
+ buffer_delete_bytes(e->view, word_fwd(&bi, skip_non_word));
+ return true;
+}
+
+static bool cmd_down(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_or_lines_flags(e->view, a);
+ move_down(e->view, 1);
+ return true;
+}
+
+static bool cmd_eof(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_or_lines_flags(e->view, a);
+ move_eof(e->view);
+ return true;
+}
+
+static bool cmd_eol(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_flag(e->view, a);
+ move_eol(e->view);
+ return true;
+}
+
+static bool cmd_eolsf(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ if (!block_iter_eol(&view->cursor)) {
+ Window *window = e->window;
+ long margin = window_get_scroll_margin(window, e->options.scroll_margin);
+ long bottom = view->vy + window->edit_h - 1 - margin;
+ if (view->cy < bottom) {
+ move_down(view, bottom - view->cy);
+ } else {
+ block_iter_eof(&view->cursor);
+ }
+ }
+
+ view_reset_preferred_x(view);
+ return true;
+}
+
+static bool cmd_erase(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ erase(e->view);
+ return true;
+}
+
+static bool cmd_erase_bol(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ buffer_erase_bytes(e->view, block_iter_bol(&e->view->cursor));
+ return true;
+}
+
+static bool cmd_erase_word(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ bool skip_non_word = has_flag(a, 's');
+ buffer_erase_bytes(view, word_bwd(&view->cursor, skip_non_word));
+ return true;
+}
+
+static bool cmd_errorfmt(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args == 0);
+ const char *name = a->args[0];
+ if (a->nr_args == 1) {
+ remove_compiler(&e->compilers, name);
+ return true;
+ }
+
+ bool ignore = has_flag(a, 'i');
+ return add_error_fmt(&e->compilers, name, ignore, a->args[1], a->args + 2);
+}
+
+static bool cmd_exec(EditorState *e, const CommandArgs *a)
+{
+ ExecAction actions[3] = {EXEC_TTY, EXEC_TTY, EXEC_TTY};
+ SpawnFlags spawn_flags = 0;
+ bool lflag = false;
+ bool move_after_insert = false;
+ bool strip_nl = false;
+
+ for (size_t i = 0, n = a->nr_flags, argidx = 0, fd; i < n; i++) {
+ switch (a->flags[i]) {
+ case 'e': fd = STDERR_FILENO; break;
+ case 'i': fd = STDIN_FILENO; break;
+ case 'o': fd = STDOUT_FILENO; break;
+ case 'p': spawn_flags |= SPAWN_PROMPT; continue;
+ case 's': spawn_flags |= SPAWN_QUIET; continue;
+ case 't': spawn_flags &= ~SPAWN_QUIET; continue;
+ case 'l': lflag = true; continue;
+ case 'm': move_after_insert = true; continue;
+ case 'n': strip_nl = true; continue;
+ default: BUG("unexpected flag"); return false;
+ }
+ const char *action_name = a->args[argidx++];
+ ExecAction action = lookup_exec_action(action_name, fd);
+ if (unlikely(action == EXEC_INVALID)) {
+ return error_msg("invalid action for -%c: '%s'", a->flags[i], action_name);
+ }
+ actions[fd] = action;
+ }
+
+ if (lflag && actions[STDIN_FILENO] == EXEC_BUFFER) {
+ // For compat. with old "filter" and "pipe-to" commands
+ actions[STDIN_FILENO] = EXEC_LINE;
+ }
+
+ const char **argv = (const char **)a->args + a->nr_flag_args;
+ ssize_t outlen = handle_exec(e, argv, actions, spawn_flags, strip_nl);
+ if (outlen <= 0) {
+ return outlen == 0;
+ }
+
+ if (move_after_insert && actions[STDOUT_FILENO] == EXEC_BUFFER) {
+ block_iter_skip_bytes(&e->view->cursor, outlen);
+ }
+ return true;
+}
+
+static bool cmd_ft(EditorState *e, const CommandArgs *a)
+{
+ char **args = a->args;
+ const char *filetype = args[0];
+ if (unlikely(!is_valid_filetype_name(filetype))) {
+ return error_msg("Invalid filetype name: '%s'", filetype);
+ }
+
+ FileDetectionType dt = FT_EXTENSION;
+ switch (last_flag(a)) {
+ case 'b':
+ dt = FT_BASENAME;
+ break;
+ case 'c':
+ dt = FT_CONTENT;
+ break;
+ case 'f':
+ dt = FT_FILENAME;
+ break;
+ case 'i':
+ dt = FT_INTERPRETER;
+ break;
+ }
+
+ size_t nfailed = 0;
+ for (size_t i = 1, n = a->nr_args; i < n; i++) {
+ if (!add_filetype(&e->filetypes, filetype, args[i], dt)) {
+ nfailed++;
+ }
+ }
+
+ return nfailed == 0;
+}
+
+static bool cmd_hi(EditorState *e, const CommandArgs *a)
+{
+ if (unlikely(a->nr_args == 0)) {
+ exec_builtin_color_reset(e);
+ goto update;
+ }
+
+ char **strs = a->args + 1;
+ size_t strs_len = a->nr_args - 1;
+ TermColor color;
+ ssize_t n = parse_term_color(&color, strs, strs_len);
+ if (unlikely(n != strs_len)) {
+ if (n < 0) {
+ return error_msg("too many colors");
+ }
+ BUG_ON(n > strs_len);
+ return error_msg("invalid color or attribute: '%s'", strs[n]);
+ }
+
+ TermColorCapabilityType color_type = e->terminal.color_type;
+ bool optimize = e->options.optimize_true_color;
+ int32_t fg = color_to_nearest(color.fg, color_type, optimize);
+ int32_t bg = color_to_nearest(color.bg, color_type, optimize);
+ if (
+ color_type != TERM_TRUE_COLOR
+ && has_flag(a, 'c')
+ && (fg != color.fg || bg != color.bg)
+ ) {
+ return true;
+ }
+
+ color.fg = fg;
+ color.bg = bg;
+ set_highlight_color(&e->colors, a->args[0], &color);
+
+update:
+ // Don't call update_all_syntax_colors() needlessly; it's called
+ // right after config has been loaded
+ if (e->status != EDITOR_INITIALIZING) {
+ update_all_syntax_colors(&e->syntaxes, &e->colors);
+ mark_everything_changed(e);
+ }
+ return true;
+}
+
+static bool cmd_include(EditorState *e, const CommandArgs *a)
+{
+ ConfigFlags flags = has_flag(a, 'q') ? CFG_NOFLAGS : CFG_MUST_EXIST;
+ if (has_flag(a, 'b')) {
+ flags |= CFG_BUILTIN;
+ }
+ int err = read_normal_config(e, a->args[0], flags);
+ // TODO: Clean up read_normal_config() so this can be simplified to `err == 0`
+ return err == 0 || (err == ENOENT && !(flags & CFG_MUST_EXIST));
+}
+
+static bool cmd_insert(EditorState *e, const CommandArgs *a)
+{
+ const char *str = a->args[0];
+ if (has_flag(a, 'k')) {
+ for (size_t i = 0; str[i]; i++) {
+ insert_ch(e->view, str[i]);
+ }
+ return true;
+ }
+
+ bool move_after = has_flag(a, 'm');
+ insert_text(e->view, str, strlen(str), move_after);
+ return true;
+}
+
+static bool cmd_join(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ join_lines(e->view);
+ return true;
+}
+
+static bool cmd_left(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_flag(e->view, a);
+ move_cursor_left(e->view);
+ return true;
+}
+
+static bool cmd_line(EditorState *e, const CommandArgs *a)
+{
+ const char *str = a->args[0];
+ size_t line, column;
+ if (unlikely(!str_to_xfilepos(str, &line, &column))) {
+ return error_msg("Invalid line number: %s", str);
+ }
+
+ View *view = e->view;
+ long x = view_get_preferred_x(view);
+ unselect(view);
+
+ if (column >= 1) {
+ // Column was specified; move to exact position
+ move_to_filepos(view, line, column);
+ } else {
+ // Column was omitted; move to line while preserving current column
+ move_to_line(view, line);
+ move_to_preferred_x(view, x);
+ }
+
+ return true;
+}
+
+static bool cmd_load_syntax(EditorState *e, const CommandArgs *a)
+{
+ const char *arg = a->args[0];
+ const char *slash = strrchr(arg, '/');
+ if (!slash) {
+ const char *filetype = arg;
+ if (find_syntax(&e->syntaxes, filetype)) {
+ return true;
+ }
+ return !!load_syntax_by_filetype(e, filetype);
+ }
+
+ const char *filetype = slash + 1;
+ if (find_syntax(&e->syntaxes, filetype)) {
+ return error_msg("Syntax for filetype %s already loaded", filetype);
+ }
+
+ int err;
+ return !!load_syntax_file(e, arg, CFG_MUST_EXIST, &err);
+}
+
+static bool cmd_macro(EditorState *e, const CommandArgs *a)
+{
+ CommandMacroState *m = &e->macro;
+ const char *action = a->args[0];
+
+ if (streq(action, "play") || streq(action, "run")) {
+ for (size_t i = 0, n = m->macro.count; i < n; i++) {
+ const char *cmd_str = m->macro.ptrs[i];
+ if (!handle_normal_command(e, cmd_str, false)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ const char *msg;
+ if (streq(action, "toggle")) {
+ if (m->recording) {
+ goto stop;
+ }
+ goto record;
+ }
+
+ if (streq(action, "record")) {
+ record:
+ msg = macro_record(m) ? "Recording macro" : "Already recording";
+ goto message;
+ }
+
+ if (streq(action, "stop")) {
+ stop:
+ if (!macro_stop(m)) {
+ msg = "Not recording";
+ goto message;
+ }
+ size_t count = m->macro.count;
+ const char *plural = (count != 1) ? "s" : "";
+ info_msg("Macro recording stopped; %zu command%s saved", count, plural);
+ return true;
+ }
+
+ if (streq(action, "cancel")) {
+ msg = macro_cancel(m) ? "Macro recording cancelled" : "Not recording";
+ goto message;
+ }
+
+ return error_msg("Unknown action '%s'", action);
+
+message:
+ info_msg("%s", msg);
+ // TODO: make this conditional?
+ return true;
+}
+
+static bool cmd_match_bracket(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ View *view = e->view;
+ CodePoint cursor_char;
+ if (!block_iter_get_char(&view->cursor, &cursor_char)) {
+ return error_msg("No character under cursor");
+ }
+
+ CodePoint target = cursor_char;
+ BlockIter bi = view->cursor;
+ size_t level = 0;
+ CodePoint u = 0;
+
+ switch (cursor_char) {
+ case '<':
+ case '[':
+ case '{':
+ target++;
+ // Fallthrough
+ case '(':
+ target++;
+ goto search_fwd;
+ case '>':
+ case ']':
+ case '}':
+ target--;
+ // Fallthrough
+ case ')':
+ target--;
+ goto search_bwd;
+ default:
+ return error_msg("Character under cursor not matchable");
+ }
+
+search_fwd:
+ block_iter_next_char(&bi, &u);
+ BUG_ON(u != cursor_char);
+ while (block_iter_next_char(&bi, &u)) {
+ if (u == target) {
+ if (level == 0) {
+ block_iter_prev_char(&bi, &u);
+ view->cursor = bi;
+ return true; // Found
+ }
+ level--;
+ } else if (u == cursor_char) {
+ level++;
+ }
+ }
+ goto not_found;
+
+search_bwd:
+ while (block_iter_prev_char(&bi, &u)) {
+ if (u == target) {
+ if (level == 0) {
+ view->cursor = bi;
+ return true; // Found
+ }
+ level--;
+ } else if (u == cursor_char) {
+ level++;
+ }
+ }
+
+not_found:
+ return error_msg("No matching bracket found");
+}
+
+static bool cmd_move_tab(EditorState *e, const CommandArgs *a)
+{
+ Window *window = e->window;
+ const size_t ntabs = window->views.count;
+ const char *str = a->args[0];
+ size_t to, from = ptr_array_idx(&window->views, e->view);
+ BUG_ON(from >= ntabs);
+ if (streq(str, "left")) {
+ to = size_decrement_wrapped(from, ntabs);
+ } else if (streq(str, "right")) {
+ to = size_increment_wrapped(from, ntabs);
+ } else {
+ if (!str_to_size(str, &to) || to == 0) {
+ return error_msg("Invalid tab position %s", str);
+ }
+ to = MIN(to, ntabs) - 1;
+ }
+ ptr_array_move(&window->views, from, to);
+ window->update_tabbar = true;
+ return true;
+}
+
+static bool cmd_msg(EditorState *e, const CommandArgs *a)
+{
+ const char *str = a->args[0];
+ uint_least64_t np = cmdargs_flagset_value('n') | cmdargs_flagset_value('p');
+ if (u64_popcount(a->flag_set & np) + !!str >= 2) {
+ return error_msg("flags [-n|-p] and [number] argument are mutually exclusive");
+ }
+
+ MessageArray *msgs = &e->messages;
+ size_t count = msgs->array.count;
+ if (count == 0) {
+ return true;
+ }
+
+ size_t p = msgs->pos;
+ BUG_ON(p >= count);
+ if (has_flag(a, 'n')) {
+ p = MIN(p + 1, count - 1);
+ } else if (has_flag(a, 'p')) {
+ p = p ? p - 1 : 0;
+ } else if (str) {
+ if (!str_to_size(str, &p) || p == 0) {
+ return error_msg("invalid message index: %s", str);
+ }
+ p = MIN(p - 1, count - 1);
+ }
+
+ msgs->pos = p;
+ return activate_current_message(e);
+}
+
+static bool cmd_new_line(EditorState *e, const CommandArgs *a)
+{
+ new_line(e->view, has_flag(a, 'a'));
+ return true;
+}
+
+static bool cmd_next(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ size_t i = ptr_array_idx(&e->window->views, e->view);
+ size_t n = e->window->views.count;
+ BUG_ON(i >= n);
+ set_view(e->window->views.ptrs[size_increment_wrapped(i, n)]);
+ return true;
+}
+
+static bool xglob(char **args, glob_t *globbuf)
+{
+ BUG_ON(!args);
+ BUG_ON(!args[0]);
+ int err = glob(*args, GLOB_NOCHECK, NULL, globbuf);
+ while (err == 0 && *++args) {
+ err = glob(*args, GLOB_NOCHECK | GLOB_APPEND, NULL, globbuf);
+ }
+
+ if (likely(err == 0)) {
+ BUG_ON(globbuf->gl_pathc == 0);
+ BUG_ON(!globbuf->gl_pathv);
+ BUG_ON(!globbuf->gl_pathv[0]);
+ return true;
+ }
+
+ BUG_ON(err == GLOB_NOMATCH);
+ globfree(globbuf);
+ return error_msg("glob: %s", (err == GLOB_NOSPACE) ? strerror(ENOMEM) : "failed");
+}
+
+static bool cmd_open(EditorState *e, const CommandArgs *a)
+{
+ bool temporary = has_flag(a, 't');
+ if (unlikely(temporary && a->nr_args > 0)) {
+ return error_msg("'open -t' can't be used with filename arguments");
+ }
+
+ const char *requested_encoding = NULL;
+ char **args = a->args;
+ if (unlikely(a->nr_flag_args > 0)) {
+ // The "-e" flag is the only one that takes an argument, so the
+ // above condition implies it was used
+ BUG_ON(!has_flag(a, 'e'));
+ requested_encoding = args[a->nr_flag_args - 1];
+ args += a->nr_flag_args;
+ }
+
+ Encoding encoding = {.type = ENCODING_AUTODETECT};
+ if (requested_encoding) {
+ EncodingType enctype = lookup_encoding(requested_encoding);
+ if (enctype == UTF8) {
+ encoding = encoding_from_type(enctype);
+ } else if (conversion_supported_by_iconv(requested_encoding, "UTF-8")) {
+ encoding = encoding_from_name(requested_encoding);
+ } else {
+ if (errno == EINVAL) {
+ return error_msg("Unsupported encoding '%s'", requested_encoding);
+ }
+ return error_msg (
+ "iconv conversion from '%s' failed: %s",
+ requested_encoding,
+ strerror(errno)
+ );
+ }
+ }
+
+ if (a->nr_args == 0) {
+ View *view = window_open_new_file(e->window);
+ view->buffer->temporary = temporary;
+ if (requested_encoding) {
+ buffer_set_encoding(view->buffer, encoding, e->options.utf8_bom);
+ }
+ return true;
+ }
+
+ char **paths = args;
+ glob_t globbuf;
+ bool use_glob = has_flag(a, 'g');
+ if (use_glob) {
+ if (!xglob(args, &globbuf)) {
+ return false;
+ }
+ paths = globbuf.gl_pathv;
+ }
+
+ View *first_opened;
+ if (!paths[1]) {
+ // Previous view is remembered when opening single file
+ first_opened = window_open_file(e->window, paths[0], &encoding);
+ } else {
+ // It makes no sense to remember previous view when opening multiple files
+ first_opened = window_open_files(e->window, paths, &encoding);
+ }
+
+ if (use_glob) {
+ globfree(&globbuf);
+ }
+
+ return !!first_opened;
+}
+
+static bool cmd_option(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args < 3);
+ size_t nstrs = a->nr_args - 1;
+ if (unlikely(nstrs & 1)) {
+ return error_msg("Missing option value");
+ }
+
+ char **strs = a->args + 1;
+ if (unlikely(!validate_local_options(strs))) {
+ return false;
+ }
+
+ PointerArray *opts = &e->file_options;
+ if (has_flag(a, 'r')) {
+ const StringView pattern = strview_from_cstring(a->args[0]);
+ return add_file_options(opts, FOPTS_FILENAME, pattern, strs, nstrs);
+ }
+
+ const char *ft_list = a->args[0];
+ size_t errors = 0;
+ for (size_t pos = 0, len = strlen(ft_list); pos < len; ) {
+ const StringView filetype = get_delim(ft_list, &pos, len, ',');
+ if (!add_file_options(opts, FOPTS_FILETYPE, filetype, strs, nstrs)) {
+ errors++;
+ }
+ }
+
+ return !errors;
+}
+
+static bool cmd_blkdown(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ // If current line is blank, skip past consecutive blank lines
+ StringView line;
+ fetch_this_line(&view->cursor, &line);
+ if (strview_isblank(&line)) {
+ while (block_iter_next_line(&view->cursor)) {
+ fill_line_ref(&view->cursor, &line);
+ if (!strview_isblank(&line)) {
+ break;
+ }
+ }
+ }
+
+ // Skip past non-blank lines
+ while (block_iter_next_line(&view->cursor)) {
+ fill_line_ref(&view->cursor, &line);
+ if (strview_isblank(&line)) {
+ break;
+ }
+ }
+
+ // If we reach the last populated line in the buffer, move down one line
+ BlockIter tmp = view->cursor;
+ block_iter_eol(&tmp);
+ block_iter_skip_bytes(&tmp, 1);
+ if (block_iter_is_eof(&tmp)) {
+ view->cursor = tmp;
+ }
+
+ return true;
+}
+
+static bool cmd_blkup(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ // If cursor is on the first line, just move to bol
+ if (view->cy == 0) {
+ block_iter_bol(&view->cursor);
+ return true;
+ }
+
+ // If current line is blank, skip past consecutive blank lines
+ StringView line;
+ fetch_this_line(&view->cursor, &line);
+ if (strview_isblank(&line)) {
+ while (block_iter_prev_line(&view->cursor)) {
+ fill_line_ref(&view->cursor, &line);
+ if (!strview_isblank(&line)) {
+ break;
+ }
+ }
+ }
+
+ // Skip past non-blank lines
+ while (block_iter_prev_line(&view->cursor)) {
+ fill_line_ref(&view->cursor, &line);
+ if (strview_isblank(&line)) {
+ break;
+ }
+ }
+
+ return true;
+}
+
+static bool cmd_paste(EditorState *e, const CommandArgs *a)
+{
+ bool move_after = has_flag(a, 'm');
+ bool above_cursor = has_flag(a, 'a');
+ bool at_cursor = has_flag(a, 'c');
+ PasteLinesType type = PASTE_LINES_BELOW_CURSOR;
+
+ if (above_cursor && at_cursor) {
+ return error_msg("flags -a and -c are mutually exclusive");
+ } else if (above_cursor) {
+ type = PASTE_LINES_ABOVE_CURSOR;
+ } else if (at_cursor) {
+ type = PASTE_LINES_INLINE;
+ }
+
+ paste(&e->clipboard, e->view, type, move_after);
+ return true;
+}
+
+static bool cmd_pgdown(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ Window *window = e->window;
+ long margin = window_get_scroll_margin(window, e->options.scroll_margin);
+ long bottom = view->vy + window->edit_h - 1 - margin;
+ long count;
+
+ if (view->cy < bottom) {
+ count = bottom - view->cy;
+ } else {
+ count = window->edit_h - 1 - margin * 2;
+ }
+
+ move_down(view, count);
+ return true;
+}
+
+static bool cmd_pgup(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ handle_select_chars_or_lines_flags(view, a);
+
+ Window *window = e->window;
+ long margin = window_get_scroll_margin(window, e->options.scroll_margin);
+ long top = view->vy + margin;
+ long count;
+
+ if (view->cy > top) {
+ count = view->cy - top;
+ } else {
+ count = window->edit_h - 1 - margin * 2;
+ }
+
+ move_up(view, count);
+ return true;
+}
+
+static bool cmd_prev(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ size_t i = ptr_array_idx(&e->window->views, e->view);
+ size_t n = e->window->views.count;
+ BUG_ON(i >= n);
+ set_view(e->window->views.ptrs[size_decrement_wrapped(i, n)]);
+ return true;
+}
+
+static View *window_find_modified_view(Window *window)
+{
+ if (buffer_modified(window->view->buffer)) {
+ return window->view;
+ }
+ for (size_t i = 0, n = window->views.count; i < n; i++) {
+ View *view = window->views.ptrs[i];
+ if (buffer_modified(view->buffer)) {
+ return view;
+ }
+ }
+ return NULL;
+}
+
+static size_t count_modified_buffers(const PointerArray *buffers, View **first)
+{
+ View *modified = NULL;
+ size_t nr_modified = 0;
+ for (size_t i = 0, n = buffers->count; i < n; i++) {
+ Buffer *buffer = buffers->ptrs[i];
+ if (!buffer_modified(buffer)) {
+ continue;
+ }
+ nr_modified++;
+ if (!modified) {
+ modified = buffer->views.ptrs[0];
+ }
+ }
+
+ BUG_ON(nr_modified > 0 && !modified);
+ *first = modified;
+ return nr_modified;
+}
+
+static bool cmd_quit(EditorState *e, const CommandArgs *a)
+{
+ int exit_code = EDITOR_EXIT_OK;
+ if (a->nr_args) {
+ if (!str_to_int(a->args[0], &exit_code)) {
+ return error_msg("Not a valid integer argument: '%s'", a->args[0]);
+ }
+ int max = EDITOR_EXIT_MAX;
+ if (exit_code < 0 || exit_code > max) {
+ return error_msg("Exit code should be between 0 and %d", max);
+ }
+ }
+
+ View *first_modified = NULL;
+ size_t n = count_modified_buffers(&e->buffers, &first_modified);
+ if (n == 0) {
+ goto exit;
+ }
+
+ BUG_ON(!first_modified);
+ const char *plural = (n > 1) ? "s" : "";
+ if (has_flag(a, 'f')) {
+ LOG_INFO("force quitting with %zu modified buffer%s", n, plural);
+ goto exit;
+ }
+
+ // Activate a modified view (giving preference to the current view or
+ // a view in the current window)
+ View *view = window_find_modified_view(e->window);
+ set_view(view ? view : first_modified);
+
+ if (!has_flag(a, 'p')) {
+ return error_msg("Save modified files or run 'quit -f' to quit without saving");
+ }
+
+ char question[128];
+ xsnprintf (
+ question, sizeof question,
+ "Quit without saving %zu modified buffer%s? [y/N]",
+ n, plural
+ );
+
+ if (dialog_prompt(e, question, "ny") != 'y') {
+ return false;
+ }
+
+ LOG_INFO("quit prompt accepted with %zu modified buffer%s", n, plural);
+
+exit:
+ e->status = exit_code;
+ return true;
+}
+
+static bool cmd_redo(EditorState *e, const CommandArgs *a)
+{
+ char *arg = a->args[0];
+ unsigned long change_id = 0;
+ if (arg) {
+ if (!str_to_ulong(arg, &change_id) || change_id == 0) {
+ return error_msg("Invalid change id: %s", arg);
+ }
+ }
+ if (!redo(e->view, change_id)) {
+ return false;
+ }
+
+ unselect(e->view);
+ return true;
+}
+
+static bool cmd_refresh(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ mark_everything_changed(e);
+ return true;
+}
+
+static bool repeat_insert(EditorState *e, const char *str, unsigned int count, bool move_after)
+{
+ size_t str_len = strlen(str);
+ size_t bufsize;
+ if (unlikely(size_multiply_overflows(count, str_len, &bufsize))) {
+ return error_msg("Repeated insert would overflow");
+ }
+ if (unlikely(bufsize == 0)) {
+ return true;
+ }
+
+ char *buf = malloc(bufsize);
+ if (unlikely(!buf)) {
+ return error_msg_errno("malloc");
+ }
+
+ char tmp[4096];
+ if (str_len == 1) {
+ memset(buf, str[0], bufsize);
+ goto insert;
+ } else if (bufsize < 2 * sizeof(tmp) || str_len > sizeof(tmp) / 8) {
+ for (size_t i = 0; i < count; i++) {
+ memcpy(buf + (i * str_len), str, str_len);
+ }
+ goto insert;
+ }
+
+ size_t strs_per_tmp = sizeof(tmp) / str_len;
+ size_t tmp_len = strs_per_tmp * str_len;
+ size_t tmps_per_buf = bufsize / tmp_len;
+ size_t remainder = bufsize % tmp_len;
+
+ // Create a block of text containing `strs_per_tmp` concatenated strs
+ for (size_t i = 0; i < strs_per_tmp; i++) {
+ memcpy(tmp + (i * str_len), str, str_len);
+ }
+
+ // Copy `tmps_per_buf` copies of `tmp` into `buf`
+ for (size_t i = 0; i < tmps_per_buf; i++) {
+ memcpy(buf + (i * tmp_len), tmp, tmp_len);
+ }
+
+ // Copy the remainder into `buf` (if any)
+ if (remainder) {
+ memcpy(buf + (tmps_per_buf * tmp_len), tmp, remainder);
+ }
+
+ LOG_DEBUG (
+ "Optimized %u inserts of %zu bytes into %zu inserts of %zu bytes",
+ count, str_len,
+ tmps_per_buf, tmp_len
+ );
+
+insert:
+ insert_text(e->view, buf, bufsize, move_after);
+ free(buf);
+ return true;
+}
+
+static bool cmd_repeat(EditorState *e, const CommandArgs *a)
+{
+ unsigned int count;
+ if (unlikely(!str_to_uint(a->args[0], &count))) {
+ return error_msg("Not a valid repeat count: %s", a->args[0]);
+ }
+ if (unlikely(count == 0)) {
+ return true;
+ }
+
+ const Command *cmd = find_normal_command(a->args[1]);
+ if (unlikely(!cmd)) {
+ return error_msg("No such command: %s", a->args[1]);
+ }
+
+ CommandArgs a2 = cmdargs_new(a->args + 2);
+ current_command = cmd;
+ bool ok = parse_args(cmd, &a2);
+ current_command = NULL;
+ if (unlikely(!ok)) {
+ return false;
+ }
+
+ CommandFunc fn = cmd->cmd;
+ if (fn == (CommandFunc)cmd_insert && !has_flag(&a2, 'k')) {
+ // Use optimized implementation for repeated "insert"
+ return repeat_insert(e, a2.args[0], count, has_flag(&a2, 'm'));
+ }
+
+ while (count--) {
+ fn(e, &a2);
+ }
+ // TODO: return false if fn() fails?
+ return true;
+}
+
+static bool cmd_replace(EditorState *e, const CommandArgs *a)
+{
+ static const FlagMapping map[] = {
+ {'b', REPLACE_BASIC},
+ {'c', REPLACE_CONFIRM},
+ {'g', REPLACE_GLOBAL},
+ {'i', REPLACE_IGNORE_CASE},
+ };
+
+ ReplaceFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map));
+ return reg_replace(e->view, a->args[0], a->args[1], flags);
+}
+
+static bool cmd_right(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_flag(e->view, a);
+ move_cursor_right(e->view);
+ return true;
+}
+
+static bool stat_changed(const FileInfo *file, const struct stat *st)
+{
+ // Don't compare st_mode because we allow chmod 755 etc.
+ return !timespecs_equal(get_stat_mtime(st), &file->mtime)
+ || st->st_dev != file->dev
+ || st->st_ino != file->ino
+ || st->st_size != file->size;
+}
+
+static bool save_unmodified_buffer(Buffer *buffer, const char *filename)
+{
+ SaveUnmodifiedType type = buffer->options.save_unmodified;
+ if (type == SAVE_NONE) {
+ LOG_INFO("buffer unchanged; leaving file untouched");
+ return true;
+ }
+
+ BUG_ON(type != SAVE_TOUCH);
+ struct timespec times[2];
+ if (unlikely(clock_gettime(CLOCK_REALTIME, &times[0]) != 0)) {
+ LOG_ERRNO("aborting partial save; clock_gettime() failed");
+ return false;
+ }
+
+ times[1] = times[0];
+ if (unlikely(utimensat(AT_FDCWD, filename, times, 0) != 0)) {
+ LOG_ERRNO("aborting partial save; utimensat() failed");
+ return false;
+ }
+
+ buffer->file.mtime = times[0];
+ LOG_INFO("buffer unchanged; mtime/atime updated");
+ return true;
+}
+
+static bool cmd_save(EditorState *e, const CommandArgs *a)
+{
+ Buffer *buffer = e->buffer;
+ if (unlikely(buffer->stdout_buffer)) {
+ const char *f = buffer_filename(buffer);
+ info_msg("%s can't be saved; it will be piped to stdout on exit", f);
+ return true;
+ }
+
+ bool dos_nl = has_flag(a, 'd');
+ bool unix_nl = has_flag(a, 'u');
+ bool crlf = buffer->crlf_newlines;
+ if (unlikely(dos_nl && unix_nl)) {
+ return error_msg("flags -d and -u can't be used together");
+ } else if (dos_nl) {
+ crlf = true;
+ } else if (unix_nl) {
+ crlf = false;
+ }
+
+ const char *requested_encoding = NULL;
+ char **args = a->args;
+ if (unlikely(a->nr_flag_args > 0)) {
+ BUG_ON(!has_flag(a, 'e'));
+ requested_encoding = args[a->nr_flag_args - 1];
+ args += a->nr_flag_args;
+ }
+
+ Encoding encoding = buffer->encoding;
+ bool bom = buffer->bom;
+ if (requested_encoding) {
+ EncodingType et = lookup_encoding(requested_encoding);
+ if (et == UTF8) {
+ if (encoding.type != UTF8) {
+ // Encoding changed
+ encoding = encoding_from_type(et);
+ bom = e->options.utf8_bom;
+ }
+ } else if (conversion_supported_by_iconv("UTF-8", requested_encoding)) {
+ encoding = encoding_from_name(requested_encoding);
+ if (encoding.name != buffer->encoding.name) {
+ // Encoding changed
+ bom = !!get_bom_for_encoding(encoding.type);
+ }
+ } else {
+ if (errno == EINVAL) {
+ return error_msg("Unsupported encoding '%s'", requested_encoding);
+ }
+ return error_msg (
+ "iconv conversion to '%s' failed: %s",
+ requested_encoding,
+ strerror(errno)
+ );
+ }
+ }
+
+ bool b = has_flag(a, 'b');
+ bool B = has_flag(a, 'B');
+ if (unlikely(b && B)) {
+ return error_msg("flags -b and -B can't be used together");
+ } else if (b) {
+ bom = true;
+ } else if (B) {
+ bom = false;
+ }
+
+ char *absolute = buffer->abs_filename;
+ bool force = has_flag(a, 'f');
+ bool new_locked = false;
+ if (a->nr_args > 0) {
+ if (args[0][0] == '\0') {
+ return error_msg("Empty filename not allowed");
+ }
+ char *tmp = path_absolute(args[0]);
+ if (!tmp) {
+ return error_msg_errno("Failed to make absolute path");
+ }
+ if (absolute && streq(tmp, absolute)) {
+ free(tmp);
+ } else {
+ absolute = tmp;
+ }
+ } else {
+ if (!absolute) {
+ if (!has_flag(a, 'p')) {
+ return error_msg("No filename");
+ }
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, "save ");
+ return true;
+ }
+ if (buffer->readonly && !force) {
+ return error_msg("Use -f to force saving read-only file");
+ }
+ }
+
+ mode_t old_mode = buffer->file.mode;
+ bool hardlinks = false;
+ struct stat st;
+ bool stat_ok = !stat(absolute, &st);
+ if (!stat_ok) {
+ if (errno != ENOENT) {
+ error_msg("stat failed for %s: %s", absolute, strerror(errno));
+ goto error;
+ }
+ } else {
+ if (
+ absolute == buffer->abs_filename
+ && !force
+ && stat_changed(&buffer->file, &st)
+ ) {
+ error_msg (
+ "File has been modified by another process; "
+ "use 'save -f' to force overwrite"
+ );
+ goto error;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ error_msg("Will not overwrite directory %s", absolute);
+ goto error;
+ }
+ hardlinks = (st.st_nlink >= 2);
+ }
+
+ if (e->options.lock_files) {
+ if (absolute == buffer->abs_filename) {
+ if (!buffer->locked) {
+ if (!lock_file(absolute)) {
+ if (!force) {
+ error_msg("Can't lock file %s", absolute);
+ goto error;
+ }
+ } else {
+ buffer->locked = true;
+ }
+ }
+ } else {
+ if (!lock_file(absolute)) {
+ if (!force) {
+ error_msg("Can't lock file %s", absolute);
+ goto error;
+ }
+ } else {
+ new_locked = true;
+ }
+ }
+ }
+
+ if (stat_ok) {
+ if (absolute != buffer->abs_filename && !force) {
+ error_msg("Use -f to overwrite %s", absolute);
+ goto error;
+ }
+ // Allow chmod 755 etc.
+ buffer->file.mode = st.st_mode;
+ }
+
+ if (
+ stat_ok
+ && buffer->options.save_unmodified != SAVE_FULL
+ && !stat_changed(&buffer->file, &st)
+ && st.st_uid == buffer->file.uid
+ && st.st_gid == buffer->file.gid
+ && !buffer_modified(buffer)
+ && absolute == buffer->abs_filename
+ && encoding.name == buffer->encoding.name
+ && crlf == buffer->crlf_newlines
+ && bom == buffer->bom
+ && save_unmodified_buffer(buffer, absolute)
+ ) {
+ BUG_ON(new_locked);
+ return true;
+ }
+
+ if (!save_buffer(buffer, absolute, &encoding, crlf, bom, hardlinks)) {
+ goto error;
+ }
+
+ buffer->saved_change = buffer->cur_change;
+ buffer->readonly = false;
+ buffer->temporary = false;
+ buffer->crlf_newlines = crlf;
+ buffer->bom = bom;
+ if (requested_encoding) {
+ buffer->encoding = encoding;
+ }
+
+ if (absolute != buffer->abs_filename) {
+ if (buffer->locked) {
+ // Filename changes, release old file lock
+ unlock_file(buffer->abs_filename);
+ }
+ buffer->locked = new_locked;
+
+ free(buffer->abs_filename);
+ buffer->abs_filename = absolute;
+ update_short_filename(buffer, &e->home_dir);
+
+ // Filename change is not detected (only buffer_modified() change)
+ mark_buffer_tabbars_changed(buffer);
+ }
+ if (!old_mode && streq(buffer->options.filetype, "none")) {
+ // New file and most likely user has not changed the filetype
+ if (buffer_detect_filetype(buffer, &e->filetypes)) {
+ set_file_options(e, buffer);
+ set_editorconfig_options(buffer);
+ buffer_update_syntax(e, buffer);
+ }
+ }
+
+ return true;
+
+error:
+ if (new_locked) {
+ unlock_file(absolute);
+ }
+ if (absolute != buffer->abs_filename) {
+ free(absolute);
+ }
+ return false;
+}
+
+static bool cmd_scroll_down(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ View *view = e->view;
+ view->vy++;
+ if (view->cy < view->vy) {
+ move_down(view, 1);
+ }
+ return true;
+}
+
+static bool cmd_scroll_pgdown(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ Window *window = e->window;
+ View *view = e->view;
+ long max = view->buffer->nl - window->edit_h + 1;
+ if (view->vy < max && max > 0) {
+ long count = window->edit_h - 1;
+ if (view->vy + count > max) {
+ count = max - view->vy;
+ }
+ view->vy += count;
+ move_down(view, count);
+ } else if (view->cy < view->buffer->nl) {
+ move_down(view, view->buffer->nl - view->cy);
+ }
+ return true;
+}
+
+static bool cmd_scroll_pgup(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ Window *window = e->window;
+ View *view = e->view;
+ if (view->vy > 0) {
+ long count = MIN(window->edit_h - 1, view->vy);
+ view->vy -= count;
+ move_up(view, count);
+ } else if (view->cy > 0) {
+ move_up(view, view->cy);
+ }
+ return true;
+}
+
+static bool cmd_scroll_up(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ Window *window = e->window;
+ View *view = e->view;
+ if (view->vy) {
+ view->vy--;
+ }
+ if (view->vy + window->edit_h <= view->cy) {
+ move_up(view, 1);
+ }
+ return true;
+}
+
+static uint_least64_t get_flagset_npw(void)
+{
+ uint_least64_t npw = 0;
+ npw |= cmdargs_flagset_value('n');
+ npw |= cmdargs_flagset_value('p');
+ npw |= cmdargs_flagset_value('w');
+ return npw;
+}
+
+static bool cmd_search(EditorState *e, const CommandArgs *a)
+{
+ const char *pattern = a->args[0];
+ if (u64_popcount(a->flag_set & get_flagset_npw()) + !!pattern >= 2) {
+ return error_msg("flags [-n|-p|-w] and [pattern] argument are mutually exclusive");
+ }
+
+ View *view = e->view;
+ char pattbuf[4096];
+ bool use_word_under_cursor = has_flag(a, 'w');
+
+ if (use_word_under_cursor) {
+ StringView word = view_get_word_under_cursor(view);
+ if (word.length == 0) {
+ // Error message would not be very useful here
+ return false;
+ }
+ const RegexpWordBoundaryTokens *rwbt = &e->regexp_word_tokens;
+ const size_t bmax = sizeof(rwbt->start);
+ static_assert_compatible_types(rwbt->start, char[8]);
+ if (unlikely(word.length >= sizeof(pattbuf) - (bmax * 2))) {
+ return error_msg("word under cursor too long");
+ }
+ char *ptr = stpncpy(pattbuf, rwbt->start, bmax);
+ memcpy(ptr, word.data, word.length);
+ memcpy(ptr + word.length, rwbt->end, bmax);
+ pattern = pattbuf;
+ }
+
+ SearchState *search = &e->search;
+ SearchCaseSensitivity cs = e->options.case_sensitive_search;
+ unselect(view);
+
+ if (has_flag(a, 'n')) {
+ return search_next(view, search, cs);
+ }
+ if (has_flag(a, 'p')) {
+ return search_prev(view, search, cs);
+ }
+
+ search->reverse = has_flag(a, 'r');
+ if (!pattern) {
+ set_input_mode(e, INPUT_SEARCH);
+ return true;
+ }
+
+ bool found;
+ search_set_regexp(search, pattern);
+ if (use_word_under_cursor) {
+ found = search_next_word(view, search, cs);
+ } else {
+ found = search_next(view, search, cs);
+ }
+
+ if (!has_flag(a, 'H')) {
+ history_add(&e->search_history, pattern);
+ }
+
+ return found;
+}
+
+static bool cmd_select_block(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ select_block(e->view);
+
+ // TODO: return false if select_block() doesn't select anything?
+ return true;
+}
+
+static bool cmd_select(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ SelectionType sel = has_flag(a, 'l') ? SELECT_LINES : SELECT_CHARS;
+ bool keep = has_flag(a, 'k');
+ if (!keep && view->selection && view->selection == sel) {
+ sel = SELECT_NONE;
+ }
+
+ view->select_mode = sel;
+ do_selection(view, sel);
+ return true;
+}
+
+static bool cmd_set(EditorState *e, const CommandArgs *a)
+{
+ bool global = has_flag(a, 'g');
+ bool local = has_flag(a, 'l');
+ if (!e->buffer) {
+ if (unlikely(local)) {
+ return error_msg("Flag -l makes no sense in config file");
+ }
+ global = true;
+ }
+
+ char **args = a->args;
+ size_t count = a->nr_args;
+ if (count == 1) {
+ return set_bool_option(e, args[0], local, global);
+ }
+ if (count & 1) {
+ return error_msg("One or even number of arguments expected");
+ }
+
+ size_t errors = 0;
+ for (size_t i = 0; i < count; i += 2) {
+ if (!set_option(e, args[i], args[i + 1], local, global)) {
+ errors++;
+ }
+ }
+
+ return !errors;
+}
+
+static bool cmd_setenv(EditorState* UNUSED_ARG(e), const CommandArgs *a)
+{
+ const char *name = a->args[0];
+ if (unlikely(streq(name, "DTE_VERSION"))) {
+ return error_msg("$DTE_VERSION cannot be changed");
+ }
+
+ const size_t nr_args = a->nr_args;
+ int res;
+ if (nr_args == 2) {
+ res = setenv(name, a->args[1], true);
+ } else {
+ BUG_ON(nr_args != 1);
+ res = unsetenv(name);
+ }
+
+ if (likely(res == 0)) {
+ return true;
+ }
+
+ if (errno == EINVAL) {
+ return error_msg("Invalid environment variable name '%s'", name);
+ }
+
+ return error_msg_errno(nr_args == 2 ? "setenv" : "unsetenv");
+}
+
+static bool cmd_shift(EditorState *e, const CommandArgs *a)
+{
+ const char *arg = a->args[0];
+ int count;
+ if (!str_to_int(arg, &count)) {
+ return error_msg("Invalid number: %s", arg);
+ }
+ if (count == 0) {
+ return error_msg("Count must be non-zero");
+ }
+ shift_lines(e->view, count);
+ return true;
+}
+
+static bool cmd_show(EditorState *e, const CommandArgs *a)
+{
+ bool write_to_cmdline = has_flag(a, 'c');
+ if (write_to_cmdline && a->nr_args < 2) {
+ return error_msg("\"show -c\" requires 2 arguments");
+ }
+ return show(e, a->args[0], a->args[1], write_to_cmdline);
+}
+
+static bool cmd_suspend(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ if (e->status == EDITOR_INITIALIZING) {
+ LOG_WARNING("suspend request ignored");
+ return false;
+ }
+
+ if (e->session_leader) {
+ return error_msg("Session leader can't suspend");
+ }
+
+ ui_end(e);
+ bool suspended = !kill(0, SIGSTOP);
+ if (!suspended) {
+ error_msg_errno("kill");
+ }
+
+ term_raw();
+ ui_start(e);
+ return suspended;
+}
+
+static bool cmd_tag(EditorState *e, const CommandArgs *a)
+{
+ if (has_flag(a, 'r')) {
+ bookmark_pop(e->window, &e->bookmarks);
+ return true;
+ }
+
+ StringView name;
+ if (a->args[0]) {
+ name = strview_from_cstring(a->args[0]);
+ } else {
+ name = view_get_word_under_cursor(e->view);
+ if (name.length == 0) {
+ return false;
+ }
+ }
+
+ const char *filename = e->buffer->abs_filename;
+ size_t ntags = tag_lookup(&e->tagfile, &name, filename, &e->messages);
+ activate_current_message_save(e);
+ return (ntags > 0);
+}
+
+static bool cmd_title(EditorState *e, const CommandArgs *a)
+{
+ Buffer *buffer = e->buffer;
+ if (buffer->abs_filename) {
+ return error_msg("saved buffers can't be retitled");
+ }
+ set_display_filename(buffer, xstrdup(a->args[0]));
+ mark_buffer_tabbars_changed(buffer);
+ return true;
+}
+
+static bool cmd_toggle(EditorState *e, const CommandArgs *a)
+{
+ bool global = has_flag(a, 'g');
+ bool verbose = has_flag(a, 'v');
+ const char *option_name = a->args[0];
+ size_t nr_values = a->nr_args - 1;
+ if (nr_values == 0) {
+ return toggle_option(e, option_name, global, verbose);
+ }
+
+ char **values = a->args + 1;
+ return toggle_option_values(e, option_name, global, verbose, values, nr_values);
+}
+
+static bool cmd_undo(EditorState *e, const CommandArgs *a)
+{
+ View *view = e->view;
+ bool move_only = has_flag(a, 'm');
+ if (move_only) {
+ const Change *change = view->buffer->cur_change;
+ if (!change->next) {
+ // If there's only 1 change, there's nothing meaningful to move to
+ return false;
+ }
+ block_iter_goto_offset(&view->cursor, change->offset);
+ view_reset_preferred_x(view);
+ return true;
+ }
+
+ if (!undo(view)) {
+ return false;
+ }
+
+ unselect(view);
+ return true;
+}
+
+static bool cmd_unselect(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ unselect(e->view);
+ return true;
+}
+
+static bool cmd_up(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_or_lines_flags(e->view, a);
+ move_up(e->view, 1);
+ return true;
+}
+
+static bool cmd_view(EditorState *e, const CommandArgs *a)
+{
+ Window *window = e->window;
+ BUG_ON(window->views.count == 0);
+ const char *arg = a->args[0];
+ size_t idx;
+ if (streq(arg, "last")) {
+ idx = window->views.count - 1;
+ } else {
+ if (!str_to_size(arg, &idx) || idx == 0) {
+ return error_msg("Invalid view index: %s", arg);
+ }
+ idx = MIN(idx, window->views.count) - 1;
+ }
+ set_view(window->views.ptrs[idx]);
+ return true;
+}
+
+static bool cmd_wclose(EditorState *e, const CommandArgs *a)
+{
+ View *view = window_find_unclosable_view(e->window);
+ bool force = has_flag(a, 'f');
+ if (!view || force) {
+ goto close;
+ }
+
+ bool prompt = has_flag(a, 'p');
+ set_view(view);
+ if (!prompt) {
+ return error_msg (
+ "Save modified files or run 'wclose -f' to close "
+ "window without saving"
+ );
+ }
+
+ if (dialog_prompt(e, "Close window without saving? [y/N]", "ny") != 'y') {
+ return false;
+ }
+
+close:
+ window_close(e->window);
+ return true;
+}
+
+static bool cmd_wflip(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ Frame *frame = e->window->frame;
+ if (!frame->parent) {
+ return false;
+ }
+ frame->parent->vertical ^= 1;
+ mark_everything_changed(e);
+ return true;
+}
+
+static bool cmd_wnext(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ e->window = next_window(e->window);
+ set_view(e->window->view);
+ mark_everything_changed(e);
+ debug_frame(e->root_frame);
+ return true;
+}
+
+static bool cmd_word_bwd(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_flag(e->view, a);
+ bool skip_non_word = has_flag(a, 's');
+ word_bwd(&e->view->cursor, skip_non_word);
+ view_reset_preferred_x(e->view);
+ return true;
+}
+
+static bool cmd_word_fwd(EditorState *e, const CommandArgs *a)
+{
+ handle_select_chars_flag(e->view, a);
+ bool skip_non_word = has_flag(a, 's');
+ word_fwd(&e->view->cursor, skip_non_word);
+ view_reset_preferred_x(e->view);
+ return true;
+}
+
+static bool cmd_wprev(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ e->window = prev_window(e->window);
+ set_view(e->window->view);
+ mark_everything_changed(e);
+ debug_frame(e->root_frame);
+ return true;
+}
+
+static bool cmd_wrap_paragraph(EditorState *e, const CommandArgs *a)
+{
+ const char *arg = a->args[0];
+ unsigned int width = e->buffer->options.text_width;
+ if (arg) {
+ if (!str_to_uint(arg, &width)) {
+ return error_msg("invalid paragraph width: %s", arg);
+ }
+ unsigned int max = TEXT_WIDTH_MAX;
+ if (width < 1 || width > max) {
+ return error_msg("width must be between 1 and %u", max);
+ }
+ }
+ format_paragraph(e->view, width);
+ return true;
+}
+
+static bool cmd_wresize(EditorState *e, const CommandArgs *a)
+{
+ Window *window = e->window;
+ if (!window->frame->parent) {
+ // Only window
+ return false;
+ }
+
+ ResizeDirection dir = RESIZE_DIRECTION_AUTO;
+ switch (last_flag(a)) {
+ case 'h':
+ dir = RESIZE_DIRECTION_HORIZONTAL;
+ break;
+ case 'v':
+ dir = RESIZE_DIRECTION_VERTICAL;
+ break;
+ }
+
+ const char *arg = a->args[0];
+ if (arg) {
+ int n;
+ if (!str_to_int(arg, &n)) {
+ return error_msg("Invalid resize value: %s", arg);
+ }
+ if (arg[0] == '+' || arg[0] == '-') {
+ add_to_frame_size(window->frame, dir, n);
+ } else {
+ resize_frame(window->frame, dir, n);
+ }
+ } else {
+ equalize_frame_sizes(window->frame->parent);
+ }
+
+ mark_everything_changed(e);
+ debug_frame(e->root_frame);
+ // TODO: return false if resize failed?
+ return true;
+}
+
+static bool cmd_wsplit(EditorState *e, const CommandArgs *a)
+{
+ bool before = has_flag(a, 'b');
+ bool use_glob = has_flag(a, 'g') && a->nr_args > 0;
+ bool vertical = has_flag(a, 'h');
+ bool root = has_flag(a, 'r');
+ bool temporary = has_flag(a, 't');
+ bool empty = temporary || has_flag(a, 'n');
+
+ if (unlikely(empty && a->nr_args > 0)) {
+ return error_msg("flags -n and -t can't be used with filename arguments");
+ }
+
+ char **paths = a->args;
+ glob_t globbuf;
+ if (use_glob) {
+ if (!xglob(a->args, &globbuf)) {
+ return false;
+ }
+ paths = globbuf.gl_pathv;
+ }
+
+ Frame *frame;
+ if (root) {
+ frame = split_root_frame(e, vertical, before);
+ } else {
+ frame = split_frame(e->window, vertical, before);
+ }
+
+ View *save = e->view;
+ e->window = frame->window;
+ e->view = NULL;
+ e->buffer = NULL;
+ mark_everything_changed(e);
+
+ View *view;
+ if (empty) {
+ view = window_open_new_file(e->window);
+ view->buffer->temporary = temporary;
+ } else if (paths[0]) {
+ view = window_open_files(e->window, paths, NULL);
+ } else {
+ view = window_add_buffer(e->window, save->buffer);
+ view->cursor = save->cursor;
+ set_view(view);
+ }
+
+ if (use_glob) {
+ globfree(&globbuf);
+ }
+
+ if (!view) {
+ // Open failed, remove new window
+ remove_frame(e, e->window->frame);
+ e->view = save;
+ e->buffer = save->buffer;
+ e->window = save->window;
+ }
+
+ debug_frame(e->root_frame);
+ return !!view;
+}
+
+static bool cmd_wswap(EditorState *e, const CommandArgs *a)
+{
+ BUG_ON(a->nr_args);
+ Frame *frame = e->window->frame;
+ Frame *parent = frame->parent;
+ if (!parent) {
+ return false;
+ }
+
+ size_t count = parent->frames.count;
+ size_t current = ptr_array_idx(&parent->frames, frame);
+ BUG_ON(current >= count);
+ size_t next = size_increment_wrapped(current, count);
+
+ void **ptrs = parent->frames.ptrs;
+ Frame *tmp = ptrs[current];
+ ptrs[current] = ptrs[next];
+ ptrs[next] = tmp;
+ mark_everything_changed(e);
+ return true;
+}
+
+IGNORE_WARNING("-Wincompatible-pointer-types")
+
+static const Command cmds[] = {
+ {"alias", "-", true, 1, 2, cmd_alias},
+ {"bind", "-cns", true, 1, 2, cmd_bind},
+ {"blkdown", "cl", false, 0, 0, cmd_blkdown},
+ {"blkup", "cl", false, 0, 0, cmd_blkup},
+ {"bof", "cl", false, 0, 0, cmd_bof},
+ {"bol", "cst", false, 0, 0, cmd_bol},
+ {"bolsf", "cl", false, 0, 0, cmd_bolsf},
+ {"bookmark", "r", false, 0, 0, cmd_bookmark},
+ {"case", "lu", false, 0, 0, cmd_case},
+ {"cd", "", true, 1, 1, cmd_cd},
+ {"center-view", "", false, 0, 0, cmd_center_view},
+ {"clear", "i", false, 0, 0, cmd_clear},
+ {"close", "fpqw", false, 0, 0, cmd_close},
+ {"command", "-", false, 0, 1, cmd_command},
+ {"compile", "-1ps", false, 2, -1, cmd_compile},
+ {"copy", "bikp", false, 0, 0, cmd_copy},
+ {"cursor", "", true, 0, 3, cmd_cursor},
+ {"cut", "", false, 0, 0, cmd_cut},
+ {"delete", "", false, 0, 0, cmd_delete},
+ {"delete-eol", "n", false, 0, 0, cmd_delete_eol},
+ {"delete-line", "", false, 0, 0, cmd_delete_line},
+ {"delete-word", "s", false, 0, 0, cmd_delete_word},
+ {"down", "cl", false, 0, 0, cmd_down},
+ {"eof", "cl", false, 0, 0, cmd_eof},
+ {"eol", "c", false, 0, 0, cmd_eol},
+ {"eolsf", "cl", false, 0, 0, cmd_eolsf},
+ {"erase", "", false, 0, 0, cmd_erase},
+ {"erase-bol", "", false, 0, 0, cmd_erase_bol},
+ {"erase-word", "s", false, 0, 0, cmd_erase_word},
+ {"errorfmt", "i", true, 1, 2 + ERRORFMT_CAPTURE_MAX, cmd_errorfmt},
+ {"exec", "-e=i=o=lmnpst", false, 1, -1, cmd_exec},
+ {"ft", "-bcfi", true, 2, -1, cmd_ft},
+ {"hi", "-c", true, 0, -1, cmd_hi},
+ {"include", "bq", true, 1, 1, cmd_include},
+ {"insert", "km", false, 1, 1, cmd_insert},
+ {"join", "", false, 0, 0, cmd_join},
+ {"left", "c", false, 0, 0, cmd_left},
+ {"line", "", false, 1, 1, cmd_line},
+ {"load-syntax", "", true, 1, 1, cmd_load_syntax},
+ {"macro", "", false, 1, 1, cmd_macro},
+ {"match-bracket", "", false, 0, 0, cmd_match_bracket},
+ {"move-tab", "", false, 1, 1, cmd_move_tab},
+ {"msg", "np", false, 0, 1, cmd_msg},
+ {"new-line", "a", false, 0, 0, cmd_new_line},
+ {"next", "", false, 0, 0, cmd_next},
+ {"open", "e=gt", false, 0, -1, cmd_open},
+ {"option", "-r", true, 3, -1, cmd_option},
+ {"paste", "acm", false, 0, 0, cmd_paste},
+ {"pgdown", "cl", false, 0, 0, cmd_pgdown},
+ {"pgup", "cl", false, 0, 0, cmd_pgup},
+ {"prev", "", false, 0, 0, cmd_prev},
+ {"quit", "fp", false, 0, 1, cmd_quit},
+ {"redo", "", false, 0, 1, cmd_redo},
+ {"refresh", "", false, 0, 0, cmd_refresh},
+ {"repeat", "-", false, 2, -1, cmd_repeat},
+ {"replace", "bcgi", false, 2, 2, cmd_replace},
+ {"right", "c", false, 0, 0, cmd_right},
+ {"save", "Bbde=fpu", false, 0, 1, cmd_save},
+ {"scroll-down", "", false, 0, 0, cmd_scroll_down},
+ {"scroll-pgdown", "", false, 0, 0, cmd_scroll_pgdown},
+ {"scroll-pgup", "", false, 0, 0, cmd_scroll_pgup},
+ {"scroll-up", "", false, 0, 0, cmd_scroll_up},
+ {"search", "Hnprw", false, 0, 1, cmd_search},
+ {"select", "kl", false, 0, 0, cmd_select},
+ {"select-block", "", false, 0, 0, cmd_select_block},
+ {"set", "gl", true, 1, -1, cmd_set},
+ {"setenv", "", true, 1, 2, cmd_setenv},
+ {"shift", "", false, 1, 1, cmd_shift},
+ {"show", "c", false, 1, 2, cmd_show},
+ {"suspend", "", false, 0, 0, cmd_suspend},
+ {"tag", "r", false, 0, 1, cmd_tag},
+ {"title", "", false, 1, 1, cmd_title},
+ {"toggle", "gv", false, 1, -1, cmd_toggle},
+ {"undo", "m", false, 0, 0, cmd_undo},
+ {"unselect", "", false, 0, 0, cmd_unselect},
+ {"up", "cl", false, 0, 0, cmd_up},
+ {"view", "", false, 1, 1, cmd_view},
+ {"wclose", "fp", false, 0, 0, cmd_wclose},
+ {"wflip", "", false, 0, 0, cmd_wflip},
+ {"wnext", "", false, 0, 0, cmd_wnext},
+ {"word-bwd", "cs", false, 0, 0, cmd_word_bwd},
+ {"word-fwd", "cs", false, 0, 0, cmd_word_fwd},
+ {"wprev", "", false, 0, 0, cmd_wprev},
+ {"wrap-paragraph", "", false, 0, 1, cmd_wrap_paragraph},
+ {"wresize", "hv", false, 0, 1, cmd_wresize},
+ {"wsplit", "bghnrt", false, 0, -1, cmd_wsplit},
+ {"wswap", "", false, 0, 0, cmd_wswap},
+};
+
+UNIGNORE_WARNINGS
+
+static bool allow_macro_recording(const Command *cmd, char **args)
+{
+ CommandFunc fn = cmd->cmd;
+ if (fn == (CommandFunc)cmd_macro || fn == (CommandFunc)cmd_command) {
+ return false;
+ }
+
+ if (fn == (CommandFunc)cmd_search) {
+ char **args_copy = copy_string_array(args, string_array_length(args));
+ CommandArgs a = cmdargs_new(args_copy);
+ bool ret = true;
+ if (do_parse_args(cmd, &a) == ARGERR_NONE) {
+ if (a.nr_args == 0 && !(a.flag_set & get_flagset_npw())) {
+ // If command is "search" with no pattern argument and without
+ // flags -n, -p or -w, the command would put the editor into
+ // search mode, which shouldn't be recorded.
+ ret = false;
+ }
+ }
+ free_string_array(args_copy);
+ return ret;
+ }
+
+ if (fn == (CommandFunc)cmd_exec) {
+ // TODO: don't record -o with open/tag/eval/msg
+ }
+
+ return true;
+}
+
+UNITTEST {
+ const char *args[4] = {NULL};
+ char **argp = (char**)args;
+ const Command *cmd = find_normal_command("left");
+ BUG_ON(!cmd);
+ BUG_ON(!allow_macro_recording(cmd, argp));
+
+ cmd = find_normal_command("exec");
+ BUG_ON(!cmd);
+ BUG_ON(!allow_macro_recording(cmd, argp));
+
+ cmd = find_normal_command("command");
+ BUG_ON(!cmd);
+ BUG_ON(allow_macro_recording(cmd, argp));
+
+ cmd = find_normal_command("macro");
+ BUG_ON(!cmd);
+ BUG_ON(allow_macro_recording(cmd, argp));
+
+ cmd = find_normal_command("search");
+ BUG_ON(!cmd);
+ BUG_ON(allow_macro_recording(cmd, argp));
+ args[0] = "xyz";
+ BUG_ON(!allow_macro_recording(cmd, argp));
+ args[0] = "-n";
+ BUG_ON(!allow_macro_recording(cmd, argp));
+ args[0] = "-p";
+ BUG_ON(!allow_macro_recording(cmd, argp));
+ args[0] = "-w";
+ BUG_ON(!allow_macro_recording(cmd, argp));
+ args[0] = "-Hr";
+ BUG_ON(allow_macro_recording(cmd, argp));
+ args[1] = "str";
+ BUG_ON(!allow_macro_recording(cmd, argp));
+}
+
+static void record_command(const Command *cmd, char **args, void *userdata)
+{
+ if (!allow_macro_recording(cmd, args)) {
+ return;
+ }
+ EditorState *e = userdata;
+ macro_command_hook(&e->macro, cmd->name, args);
+}
+
+const Command *find_normal_command(const char *name)
+{
+ return BSEARCH(name, cmds, command_cmp);
+}
+
+const CommandSet normal_commands = {
+ .lookup = find_normal_command,
+ .macro_record = record_command,
+ .expand_variable = expand_normal_var,
+ .expand_env_vars = true,
+};
+
+const char *find_normal_alias(const char *name, void *userdata)
+{
+ EditorState *e = userdata;
+ return find_alias(&e->aliases, name);
+}
+
+bool handle_normal_command(EditorState *e, const char *cmd, bool allow_recording)
+{
+ CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, allow_recording);
+ return handle_command(&runner, cmd);
+}
+
+void exec_normal_config(EditorState *e, StringView config)
+{
+ CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false);
+ exec_config(&runner, config);
+}
+
+int read_normal_config(EditorState *e, const char *filename, ConfigFlags flags)
+{
+ CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false);
+ return read_config(&runner, filename, flags);
+}
+
+void collect_normal_commands(PointerArray *a, const char *prefix)
+{
+ COLLECT_STRING_FIELDS(cmds, name, a, prefix);
+}
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(cmds, name, strcmp);
+
+ for (size_t i = 0, n = ARRAYLEN(cmds); i < n; i++) {
+ // Check that flags arrays is null-terminated within bounds
+ const char *const flags = cmds[i].flags;
+ BUG_ON(flags[ARRAYLEN(cmds[0].flags) - 1] != '\0');
+
+ // Count number of real flags (i.e. not including '-' or '=')
+ size_t nr_real_flags = 0;
+ for (size_t j = (flags[0] == '-' ? 1 : 0); flags[j]; j++) {
+ unsigned char flag = flags[j];
+ if (ascii_isalnum(flag)) {
+ nr_real_flags++;
+ } else if (flag != '=') {
+ BUG("invalid command flag: 0x%02hhX", flag);
+ }
+ }
+
+ // Check that max. number of real flags fits in CommandArgs::flags
+ // array (and also leaves 1 byte for null-terminator)
+ CommandArgs a;
+ BUG_ON(nr_real_flags >= ARRAYLEN(a.flags));
+ }
+}
diff --git a/examples/dte/commands.h b/examples/dte/commands.h
new file mode 100644
index 0000000..cfebdd2
--- /dev/null
+++ b/examples/dte/commands.h
@@ -0,0 +1,22 @@
+#ifndef COMMANDS_H
+#define COMMANDS_H
+
+#include <stdbool.h>
+#include "command/run.h"
+#include "config.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+
+extern const CommandSet normal_commands;
+
+struct EditorState;
+
+const Command *find_normal_command(const char *name) NONNULL_ARGS;
+const char *find_normal_alias(const char *name, void *userdata) NONNULL_ARGS;
+bool handle_normal_command(struct EditorState *e, const char *cmd, bool allow_recording) NONNULL_ARGS;
+void exec_normal_config(struct EditorState *e, StringView config) NONNULL_ARGS;
+int read_normal_config(struct EditorState *e, const char *filename, ConfigFlags flags) NONNULL_ARGS;
+void collect_normal_commands(PointerArray *a, const char *prefix) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/compat.c b/examples/dte/compat.c
new file mode 100644
index 0000000..9a26950
--- /dev/null
+++ b/examples/dte/compat.c
@@ -0,0 +1,35 @@
+#include "compat.h"
+
+const char feature_string[] =
+ ""
+#if HAVE_DUP3
+ " dup3"
+#endif
+#if HAVE_PIPE2
+ " pipe2"
+#endif
+#if HAVE_FSYNC
+ " fsync"
+#endif
+#if HAVE_MEMMEM
+ " memmem"
+#endif
+#if HAVE_SIG2STR
+ " sig2str"
+#endif
+#if HAVE_SIGABBREV_NP && !HAVE_SIG2STR
+ " sigabbrev_np"
+#endif
+#if HAVE_TIOCGWINSZ
+ " TIOCGWINSZ"
+#endif
+#if HAVE_TCGETWINSIZE && !HAVE_TIOCGWINSZ
+ " tcgetwinsize"
+#endif
+#if HAVE_TIOCNOTTY
+ " TIOCNOTTY"
+#endif
+#if HAVE_POSIX_MADVISE
+ " posix_madvise"
+#endif
+;
diff --git a/examples/dte/compat.h b/examples/dte/compat.h
new file mode 100644
index 0000000..bbb6d53
--- /dev/null
+++ b/examples/dte/compat.h
@@ -0,0 +1,8 @@
+#ifndef COMPAT_H
+#define COMPAT_H
+
+#include "../build/feature.h"
+
+extern const char feature_string[];
+
+#endif
diff --git a/examples/dte/compiler.c b/examples/dte/compiler.c
new file mode 100644
index 0000000..b1a0eaa
--- /dev/null
+++ b/examples/dte/compiler.c
@@ -0,0 +1,151 @@
+#include <stdlib.h>
+#include <string.h>
+#include "compiler.h"
+#include "command/serialize.h"
+#include "error.h"
+#include "regexp.h"
+#include "util/array.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+
+static const char capture_names[][8] = {
+ [ERRFMT_FILE] = "file",
+ [ERRFMT_LINE] = "line",
+ [ERRFMT_COLUMN] = "column",
+ [ERRFMT_MESSAGE] = "message"
+};
+
+UNITTEST {
+ CHECK_STRING_ARRAY(capture_names);
+}
+
+static Compiler *find_or_add_compiler(HashMap *compilers, const char *name)
+{
+ Compiler *c = find_compiler(compilers, name);
+ return c ? c : hashmap_insert(compilers, xstrdup(name), xnew0(Compiler, 1));
+}
+
+Compiler *find_compiler(const HashMap *compilers, const char *name)
+{
+ return hashmap_get(compilers, name);
+}
+
+bool add_error_fmt (
+ HashMap *compilers,
+ const char *name,
+ bool ignore,
+ const char *format,
+ char **desc
+) {
+ int8_t idx[] = {
+ [ERRFMT_FILE] = -1,
+ [ERRFMT_LINE] = -1,
+ [ERRFMT_COLUMN] = -1,
+ [ERRFMT_MESSAGE] = 0,
+ };
+
+ size_t max_idx = 0;
+ for (size_t i = 0, j = 0, n = ARRAYLEN(capture_names); desc[i]; i++) {
+ BUG_ON(i >= ERRORFMT_CAPTURE_MAX);
+ if (streq(desc[i], "_")) {
+ continue;
+ }
+ for (j = 0; j < n; j++) {
+ if (streq(desc[i], capture_names[j])) {
+ max_idx = i + 1;
+ idx[j] = max_idx;
+ break;
+ }
+ }
+ if (unlikely(j == n)) {
+ return error_msg("unknown substring name %s", desc[i]);
+ }
+ }
+
+ ErrorFormat *f = xnew(ErrorFormat, 1);
+ f->ignore = ignore;
+ static_assert_compatible_types(f->capture_index, idx);
+ memcpy(f->capture_index, idx, sizeof(idx));
+
+ if (unlikely(!regexp_compile(&f->re, format, 0))) {
+ free(f);
+ return false;
+ }
+
+ if (unlikely(max_idx > f->re.re_nsub)) {
+ regfree(&f->re);
+ free(f);
+ return error_msg("invalid substring count");
+ }
+
+ Compiler *compiler = find_or_add_compiler(compilers, name);
+ f->pattern = str_intern(format);
+ ptr_array_append(&compiler->error_formats, f);
+ return true;
+}
+
+static void free_error_format(ErrorFormat *f)
+{
+ regfree(&f->re);
+ free(f);
+}
+
+void free_compiler(Compiler *c)
+{
+ ptr_array_free_cb(&c->error_formats, FREE_FUNC(free_error_format));
+ free(c);
+}
+
+void remove_compiler(HashMap *compilers, const char *name)
+{
+ Compiler *c = hashmap_remove(compilers, name);
+ if (c) {
+ free_compiler(c);
+ }
+}
+
+void collect_errorfmt_capture_names(PointerArray *a, const char *prefix)
+{
+ COLLECT_STRINGS(capture_names, a, prefix);
+ if (str_has_prefix("_", prefix)) {
+ ptr_array_append(a, xstrdup("_"));
+ }
+}
+
+void dump_compiler(const Compiler *c, const char *name, String *s)
+{
+ for (size_t i = 0, n = c->error_formats.count; i < n; i++) {
+ ErrorFormat *e = c->error_formats.ptrs[i];
+ string_append_literal(s, "errorfmt ");
+ if (e->ignore) {
+ string_append_literal(s, "-i ");
+ }
+ if (unlikely(name[0] == '-' || e->pattern[0] == '-')) {
+ string_append_literal(s, "-- ");
+ }
+ string_append_escaped_arg(s, name, true);
+ string_append_byte(s, ' ');
+ string_append_escaped_arg(s, e->pattern, true);
+
+ static_assert(ARRAYLEN(e->capture_index) == 4);
+ const int8_t *a = e->capture_index;
+ int max_idx = MAX4(a[0], a[1], a[2], a[3]);
+ BUG_ON(max_idx > ERRORFMT_CAPTURE_MAX);
+
+ for (int j = 1; j <= max_idx; j++) {
+ const char *capname = "_";
+ for (size_t k = 0; k < ARRAYLEN(capture_names); k++) {
+ if (j == a[k]) {
+ capname = capture_names[k];
+ break;
+ }
+ }
+ string_append_byte(s, ' ');
+ string_append_cstring(s, capname);
+ }
+
+ string_append_byte(s, '\n');
+ }
+}
diff --git a/examples/dte/compiler.h b/examples/dte/compiler.h
new file mode 100644
index 0000000..c1b0de6
--- /dev/null
+++ b/examples/dte/compiler.h
@@ -0,0 +1,49 @@
+#ifndef COMPILER_H
+#define COMPILER_H
+
+#include <regex.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include "util/hashmap.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string.h"
+
+enum {
+ ERRORFMT_CAPTURE_MAX = 16
+};
+
+enum {
+ ERRFMT_FILE,
+ ERRFMT_LINE,
+ ERRFMT_COLUMN,
+ ERRFMT_MESSAGE,
+};
+
+typedef struct {
+ int8_t capture_index[4];
+ bool ignore;
+ const char *pattern; // Original pattern string (interned)
+ regex_t re; // Compiled pattern
+} ErrorFormat;
+
+typedef struct {
+ PointerArray error_formats;
+} Compiler;
+
+Compiler *find_compiler(const HashMap *compilers, const char *name) NONNULL_ARGS;
+void remove_compiler(HashMap *compilers, const char *name) NONNULL_ARGS;
+void free_compiler(Compiler *c) NONNULL_ARGS;
+void collect_errorfmt_capture_names(PointerArray *a, const char *prefix) NONNULL_ARGS;
+void dump_compiler(const Compiler *c, const char *name, String *s) NONNULL_ARGS;
+
+NONNULL_ARGS WARN_UNUSED_RESULT
+bool add_error_fmt (
+ HashMap *compilers,
+ const char *name,
+ bool ignore,
+ const char *format,
+ char **desc
+);
+
+#endif
diff --git a/examples/dte/completion.c b/examples/dte/completion.c
new file mode 100644
index 0000000..89e8c8c
--- /dev/null
+++ b/examples/dte/completion.c
@@ -0,0 +1,879 @@
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "completion.h"
+#include "bind.h"
+#include "command/alias.h"
+#include "command/args.h"
+#include "command/parse.h"
+#include "command/run.h"
+#include "command/serialize.h"
+#include "commands.h"
+#include "compiler.h"
+#include "config.h"
+#include "filetype.h"
+#include "options.h"
+#include "show.h"
+#include "syntax/color.h"
+#include "tag.h"
+#include "terminal/cursor.h"
+#include "terminal/key.h"
+#include "terminal/style.h"
+#include "util/arith.h"
+#include "util/array.h"
+#include "util/ascii.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/intmap.h"
+#include "util/log.h"
+#include "util/numtostr.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/string-view.h"
+#include "util/string.h"
+#include "util/xdirent.h"
+#include "util/xmalloc.h"
+#include "vars.h"
+
+extern char **environ;
+
+typedef enum {
+ COLLECT_ALL, // (directories and files)
+ COLLECT_EXECUTABLES, // (directories and executable files)
+ COLLECT_DIRS_ONLY,
+} FileCollectionType;
+
+static bool is_executable(int dir_fd, const char *filename)
+{
+ return faccessat(dir_fd, filename, X_OK, 0) == 0;
+}
+
+static bool do_collect_files (
+ PointerArray *array,
+ const char *dirname,
+ const char *dirprefix,
+ const char *fileprefix,
+ FileCollectionType type
+) {
+ DIR *const dir = xopendir(dirname);
+ if (!dir) {
+ return false;
+ }
+
+ const int dir_fd = dirfd(dir);
+ if (unlikely(dir_fd < 0)) {
+ LOG_ERRNO("dirfd");
+ xclosedir(dir);
+ return false;
+ }
+
+ size_t dlen = strlen(dirprefix);
+ size_t flen = strlen(fileprefix);
+ const struct dirent *de;
+
+ while ((de = xreaddir(dir))) {
+ const char *name = de->d_name;
+ if (streq(name, ".") || streq(name, "..") || unlikely(streq(name, ""))) {
+ continue;
+ }
+
+ // TODO: add a global option to allow dotfiles to be included
+ // even when there's no prefix
+ if (flen ? strncmp(name, fileprefix, flen) : name[0] == '.') {
+ continue;
+ }
+
+ struct stat st;
+ if (fstatat(dir_fd, name, &st, AT_SYMLINK_NOFOLLOW)) {
+ continue;
+ }
+
+ bool is_dir = S_ISDIR(st.st_mode);
+ if (S_ISLNK(st.st_mode)) {
+ if (!fstatat(dir_fd, name, &st, 0)) {
+ is_dir = S_ISDIR(st.st_mode);
+ }
+ }
+
+ if (!is_dir) {
+ switch (type) {
+ case COLLECT_DIRS_ONLY:
+ continue;
+ case COLLECT_ALL:
+ break;
+ case COLLECT_EXECUTABLES:
+ if (!is_executable(dir_fd, name)) {
+ continue;
+ }
+ if (!dlen) {
+ dirprefix = "./";
+ dlen = 2;
+ }
+ break;
+ default:
+ BUG("unhandled FileCollectionType value");
+ }
+ }
+
+ ptr_array_append(array, path_joinx(dirprefix, name, is_dir));
+ }
+
+ xclosedir(dir);
+ return true;
+}
+
+static void collect_files(EditorState *e, CompletionState *cs, FileCollectionType type)
+{
+ StringView esc = cs->escaped;
+ if (strview_has_prefix(&esc, "~/")) {
+ CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false);
+ char *str = parse_command_arg(&runner, esc.data, esc.length, false);
+ const char *slash = strrchr(str, '/');
+ BUG_ON(!slash);
+ cs->tilde_expanded = true;
+ char *dir = path_dirname(cs->parsed);
+ char *dirprefix = path_dirname(str);
+ do_collect_files(&cs->completions, dir, dirprefix, slash + 1, type);
+ free(dirprefix);
+ free(dir);
+ free(str);
+ } else {
+ const char *slash = strrchr(cs->parsed, '/');
+ if (!slash) {
+ do_collect_files(&cs->completions, ".", "", cs->parsed, type);
+ } else {
+ char *dir = path_dirname(cs->parsed);
+ do_collect_files(&cs->completions, dir, dir, slash + 1, type);
+ free(dir);
+ }
+ }
+
+ if (cs->completions.count == 1) {
+ // Add space if completed string is not a directory
+ const char *s = cs->completions.ptrs[0];
+ size_t len = strlen(s);
+ if (len > 0) {
+ cs->add_space_after_single_match = s[len - 1] != '/';
+ }
+ }
+}
+
+void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix)
+{
+ collect_hashmap_keys(&e->aliases, a, prefix);
+}
+
+static void collect_bound_keys(const IntMap *bindings, PointerArray *a, const char *prefix)
+{
+ char keystr[KEYCODE_STR_MAX];
+ for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) {
+ size_t keylen = keycode_to_string(it.entry->key, keystr);
+ if (str_has_prefix(keystr, prefix)) {
+ ptr_array_append(a, xmemdup(keystr, keylen + 1));
+ }
+ }
+}
+
+void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *prefix)
+{
+ collect_bound_keys(&e->modes[INPUT_NORMAL].key_bindings, a, prefix);
+}
+
+void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix)
+{
+ collect_builtin_colors(a, prefix);
+ collect_hashmap_keys(&e->colors.other, a, prefix);
+}
+
+void collect_compilers(EditorState *e, PointerArray *a, const char *prefix)
+{
+ collect_hashmap_keys(&e->compilers, a, prefix);
+}
+
+void collect_env(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
+{
+ if (strchr(prefix, '=')) {
+ return;
+ }
+
+ for (size_t i = 0; environ[i]; i++) {
+ const char *var = environ[i];
+ if (str_has_prefix(var, prefix)) {
+ const char *delim = strchr(var, '=');
+ if (likely(delim && delim != var)) {
+ ptr_array_append(a, xstrcut(var, delim - var));
+ }
+ }
+ }
+}
+
+static void complete_alias(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_normal_aliases(e, &cs->completions, cs->parsed);
+ } else if (a->nr_args == 1 && cs->parsed[0] == '\0') {
+ const char *cmd = find_alias(&e->aliases, a->args[0]);
+ if (cmd) {
+ ptr_array_append(&cs->completions, xstrdup(cmd));
+ }
+ }
+}
+
+static void complete_bind(EditorState *e, const CommandArgs *a)
+{
+ static const char flags[] = {
+ [INPUT_NORMAL] = 'n',
+ [INPUT_COMMAND] = 'c',
+ [INPUT_SEARCH] = 's',
+ };
+
+ static_assert(ARRAYLEN(flags) == ARRAYLEN(e->modes));
+ InputMode mode = INPUT_NORMAL;
+ for (size_t i = 0, count = 0; i < ARRAYLEN(flags); i++) {
+ if (cmdargs_has_flag(a, flags[i])) {
+ if (++count >= 2) {
+ return; // Don't complete bindings for multiple modes
+ }
+ mode = i;
+ }
+ }
+
+ const IntMap *key_bindings = &e->modes[mode].key_bindings;
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_bound_keys(key_bindings, &cs->completions, cs->parsed);
+ return;
+ }
+
+ if (a->nr_args != 1 || cs->parsed[0] != '\0') {
+ return;
+ }
+
+ KeyCode key;
+ if (!parse_key_string(&key, a->args[0])) {
+ return;
+ }
+ const CachedCommand *cmd = lookup_binding(key_bindings, key);
+ if (!cmd) {
+ return;
+ }
+
+ ptr_array_append(&cs->completions, xstrdup(cmd->cmd_str));
+}
+
+static void complete_cd(EditorState *e, const CommandArgs* UNUSED_ARG(a))
+{
+ CompletionState *cs = &e->cmdline.completion;
+ collect_files(e, cs, COLLECT_DIRS_ONLY);
+ if (str_has_prefix("-", cs->parsed)) {
+ if (likely(xgetenv("OLDPWD"))) {
+ ptr_array_append(&cs->completions, xstrdup("-"));
+ }
+ }
+}
+
+static void complete_exec(EditorState *e, const CommandArgs *a)
+{
+ // TODO: add completion for [-ioe] option arguments
+ CompletionState *cs = &e->cmdline.completion;
+ collect_files(e, cs, a->nr_args == 0 ? COLLECT_EXECUTABLES : COLLECT_ALL);
+}
+
+static void complete_compile(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ size_t n = a->nr_args;
+ if (n == 0) {
+ collect_compilers(e, &cs->completions, cs->parsed);
+ } else {
+ collect_files(e, cs, n == 1 ? COLLECT_EXECUTABLES : COLLECT_ALL);
+ }
+}
+
+static void complete_cursor(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ size_t n = a->nr_args;
+ if (n == 0) {
+ collect_cursor_modes(&cs->completions, cs->parsed);
+ } else if (n == 1) {
+ collect_cursor_types(&cs->completions, cs->parsed);
+ } else if (n == 2) {
+ collect_cursor_colors(&cs->completions, cs->parsed);
+ // Add an example #rrggbb color, to make things more discoverable
+ static const char rgb_example[] = "#22AABB";
+ if (str_has_prefix(rgb_example, cs->parsed)) {
+ ptr_array_append(&cs->completions, xstrdup(rgb_example));
+ }
+ }
+}
+
+static void complete_errorfmt(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_compilers(e, &cs->completions, cs->parsed);
+ } else if (a->nr_args >= 2 && !cmdargs_has_flag(a, 'i')) {
+ collect_errorfmt_capture_names(&cs->completions, cs->parsed);
+ }
+}
+
+static void complete_ft(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_ft(&e->filetypes, &cs->completions, cs->parsed);
+ }
+}
+
+static void complete_hi(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_hl_colors(e, &cs->completions, cs->parsed);
+ } else {
+ collect_colors_and_attributes(&cs->completions, cs->parsed);
+ }
+}
+
+static void complete_include(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ if (cmdargs_has_flag(a, 'b')) {
+ collect_builtin_includes(&cs->completions, cs->parsed);
+ } else {
+ collect_files(e, cs, COLLECT_ALL);
+ }
+ }
+}
+
+static void complete_macro(EditorState *e, const CommandArgs *a)
+{
+ static const char verbs[][8] = {
+ "cancel",
+ "play",
+ "record",
+ "stop",
+ "toggle",
+ };
+
+ if (a->nr_args != 0) {
+ return;
+ }
+
+ CompletionState *cs = &e->cmdline.completion;
+ COLLECT_STRINGS(verbs, &cs->completions, cs->parsed);
+}
+
+static void complete_move_tab(EditorState *e, const CommandArgs *a)
+{
+ if (a->nr_args != 0) {
+ return;
+ }
+
+ static const char words[][8] = {"left", "right"};
+ CompletionState *cs = &e->cmdline.completion;
+ COLLECT_STRINGS(words, &cs->completions, cs->parsed);
+}
+
+static void complete_open(EditorState *e, const CommandArgs *a)
+{
+ if (!cmdargs_has_flag(a, 't')) {
+ collect_files(e, &e->cmdline.completion, COLLECT_ALL);
+ }
+}
+
+static void complete_option(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ if (!cmdargs_has_flag(a, 'r')) {
+ collect_ft(&e->filetypes, &cs->completions, cs->parsed);
+ }
+ } else if (a->nr_args & 1) {
+ collect_auto_options(&cs->completions, cs->parsed);
+ } else {
+ collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed);
+ }
+}
+
+static void complete_save(EditorState *e, const CommandArgs* UNUSED_ARG(a))
+{
+ collect_files(e, &e->cmdline.completion, COLLECT_ALL);
+}
+
+static void complete_quit(EditorState *e, const CommandArgs* UNUSED_ARG(a))
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (str_has_prefix("0", cs->parsed)) {
+ ptr_array_append(&cs->completions, xstrdup("0"));
+ }
+ if (str_has_prefix("1", cs->parsed)) {
+ ptr_array_append(&cs->completions, xstrdup("1"));
+ }
+}
+
+static void complete_redo(EditorState *e, const CommandArgs* UNUSED_ARG(a))
+{
+ const Change *change = e->buffer->cur_change;
+ CompletionState *cs = &e->cmdline.completion;
+ for (unsigned long i = 1, n = change->nr_prev; i <= n; i++) {
+ ptr_array_append(&cs->completions, xstrdup(ulong_to_str(i)));
+ }
+}
+
+static void complete_set(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if ((a->nr_args + 1) & 1) {
+ bool local = cmdargs_has_flag(a, 'l');
+ bool global = cmdargs_has_flag(a, 'g');
+ collect_options(&cs->completions, cs->parsed, local, global);
+ } else {
+ collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed);
+ }
+}
+
+static void complete_setenv(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_env(e, &cs->completions, cs->parsed);
+ } else if (a->nr_args == 1 && cs->parsed[0] == '\0') {
+ BUG_ON(!a->args[0]);
+ const char *value = getenv(a->args[0]);
+ if (value) {
+ ptr_array_append(&cs->completions, xstrdup(value));
+ }
+ }
+}
+
+static void complete_show(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ collect_show_subcommands(&cs->completions, cs->parsed);
+ } else if (a->nr_args == 1) {
+ BUG_ON(!a->args[0]);
+ collect_show_subcommand_args(e, &cs->completions, a->args[0], cs->parsed);
+ }
+}
+
+static void complete_tag(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0 && !cmdargs_has_flag(a, 'r')) {
+ BUG_ON(!cs->parsed);
+ StringView prefix = strview_from_cstring(cs->parsed);
+ collect_tags(&e->tagfile, &cs->completions, &prefix);
+ }
+}
+
+static void complete_toggle(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (a->nr_args == 0) {
+ bool global = cmdargs_has_flag(a, 'g');
+ collect_toggleable_options(&cs->completions, cs->parsed, global);
+ }
+}
+
+static void complete_wsplit(EditorState *e, const CommandArgs *a)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ if (!cmdargs_has_flag(a, 't') && !cmdargs_has_flag(a, 'n')) {
+ collect_files(e, cs, COLLECT_ALL);
+ }
+}
+
+typedef struct {
+ char cmd_name[12];
+ void (*complete)(EditorState *e, const CommandArgs *a);
+} CompletionHandler;
+
+static const CompletionHandler completion_handlers[] = {
+ {"alias", complete_alias},
+ {"bind", complete_bind},
+ {"cd", complete_cd},
+ {"compile", complete_compile},
+ {"cursor", complete_cursor},
+ {"errorfmt", complete_errorfmt},
+ {"exec", complete_exec},
+ {"ft", complete_ft},
+ {"hi", complete_hi},
+ {"include", complete_include},
+ {"macro", complete_macro},
+ {"move-tab", complete_move_tab},
+ {"open", complete_open},
+ {"option", complete_option},
+ {"quit", complete_quit},
+ {"redo", complete_redo},
+ {"save", complete_save},
+ {"set", complete_set},
+ {"setenv", complete_setenv},
+ {"show", complete_show},
+ {"tag", complete_tag},
+ {"toggle", complete_toggle},
+ {"wsplit", complete_wsplit},
+};
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(completion_handlers, cmd_name, strcmp);
+ // Ensure handlers are kept in sync with renamed/removed commands
+ for (size_t i = 0; i < ARRAYLEN(completion_handlers); i++) {
+ const char *name = completion_handlers[i].cmd_name;
+ if (!find_normal_command(name)) {
+ BUG("completion handler for non-existent command: \"%s\"", name);
+ }
+ }
+}
+
+static bool can_collect_flags (
+ char **args,
+ size_t argc,
+ size_t nr_flag_args,
+ bool allow_flags_after_nonflags
+) {
+ if (allow_flags_after_nonflags) {
+ for (size_t i = 0; i < argc; i++) {
+ if (streq(args[i], "--")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ for (size_t i = 0, nonflag = 0; i < argc; i++) {
+ if (args[i][0] != '-') {
+ if (++nonflag > nr_flag_args) {
+ return false;
+ }
+ continue;
+ }
+ if (streq(args[i], "--")) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool collect_command_flags (
+ PointerArray *array,
+ char **args,
+ size_t argc,
+ const Command *cmd,
+ const CommandArgs *a,
+ const char *prefix
+) {
+ BUG_ON(prefix[0] != '-');
+ const char *flags = cmd->flags;
+ bool flags_after_nonflags = (flags[0] != '-');
+
+ if (!can_collect_flags(args, argc, a->nr_flag_args, flags_after_nonflags)) {
+ return false;
+ }
+
+ flags += flags_after_nonflags ? 0 : 1;
+ if (ascii_isalnum(prefix[1]) && prefix[2] == '\0') {
+ if (strchr(flags, prefix[1])) {
+ ptr_array_append(array, xmemdup(prefix, 3));
+ }
+ return true;
+ }
+
+ if (prefix[1] != '\0') {
+ return true;
+ }
+
+ char buf[3] = "-";
+ for (size_t i = 0; flags[i]; i++) {
+ if (!ascii_isalnum(flags[i]) || cmdargs_has_flag(a, flags[i])) {
+ continue;
+ }
+ buf[1] = flags[i];
+ ptr_array_append(array, xmemdup(buf, 3));
+ }
+
+ return true;
+}
+
+static void collect_completions(EditorState *e, char **args, size_t argc)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ PointerArray *arr = &cs->completions;
+ const char *prefix = cs->parsed;
+ if (!argc) {
+ collect_normal_commands(arr, prefix);
+ collect_normal_aliases(e, arr, prefix);
+ return;
+ }
+
+ for (size_t i = 0; i < argc; i++) {
+ if (!args[i]) {
+ // Embedded NULLs indicate there are multiple commands.
+ // Just return early here and avoid handling this case.
+ return;
+ }
+ }
+
+ const Command *cmd = find_normal_command(args[0]);
+ if (!cmd) {
+ return;
+ }
+
+ char **args_copy = copy_string_array(args + 1, argc - 1);
+ CommandArgs a = cmdargs_new(args_copy);
+ ArgParseError err = do_parse_args(cmd, &a);
+ bool dash = (prefix[0] == '-');
+ if (
+ (err != ARGERR_NONE && err != ARGERR_TOO_FEW_ARGUMENTS)
+ || (a.nr_args >= cmd->max_args && cmd->max_args != 0xFF && !dash)
+ ) {
+ goto out;
+ }
+
+ if (dash && collect_command_flags(arr, args + 1, argc - 1, cmd, &a, prefix)) {
+ goto out;
+ }
+
+ if (cmd->max_args == 0) {
+ goto out;
+ }
+
+ const CompletionHandler *h = BSEARCH(args[0], completion_handlers, vstrcmp);
+ if (h) {
+ h->complete(e, &a);
+ } else if (streq(args[0], "repeat")) {
+ if (a.nr_args == 1) {
+ collect_normal_commands(arr, prefix);
+ } else if (a.nr_args >= 2) {
+ collect_completions(e, args + 2, argc - 2);
+ }
+ }
+
+out:
+ free_string_array(args_copy);
+}
+
+static bool is_var(const char *str, size_t len)
+{
+ if (len == 0 || str[0] != '$') {
+ return false;
+ }
+ if (len == 1) {
+ return true;
+ }
+ if (!is_alpha_or_underscore(str[1])) {
+ return false;
+ }
+ for (size_t i = 2; i < len; i++) {
+ if (!is_alnum_or_underscore(str[i])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+UNITTEST {
+ BUG_ON(!is_var(STRN("$VAR")));
+ BUG_ON(!is_var(STRN("$xy_190")));
+ BUG_ON(!is_var(STRN("$__x_y_z")));
+ BUG_ON(!is_var(STRN("$x")));
+ BUG_ON(!is_var(STRN("$A")));
+ BUG_ON(!is_var(STRN("$_0")));
+ BUG_ON(!is_var(STRN("$")));
+ BUG_ON(is_var(STRN("")));
+ BUG_ON(is_var(STRN("A")));
+ BUG_ON(is_var(STRN("$.a")));
+ BUG_ON(is_var(STRN("$xyz!")));
+ BUG_ON(is_var(STRN("$1")));
+ BUG_ON(is_var(STRN("$09")));
+ BUG_ON(is_var(STRN("$1a")));
+}
+
+static int strptrcmp(const void *v1, const void *v2)
+{
+ const char *const *s1 = v1;
+ const char *const *s2 = v2;
+ return strcmp(*s1, *s2);
+}
+
+static void init_completion(EditorState *e, const CommandLine *cmdline)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ const CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false);
+ BUG_ON(cs->orig);
+ BUG_ON(runner.userdata != e);
+ BUG_ON(!runner.lookup_alias);
+
+ const size_t cmdline_pos = cmdline->pos;
+ char *const cmd = string_clone_cstring(&cmdline->buf);
+ PointerArray array = PTR_ARRAY_INIT;
+ ssize_t semicolon = -1;
+ ssize_t completion_pos = -1;
+
+ for (size_t pos = 0; true; ) {
+ while (ascii_isspace(cmd[pos])) {
+ pos++;
+ }
+
+ if (pos >= cmdline_pos) {
+ completion_pos = cmdline_pos;
+ break;
+ }
+
+ if (!cmd[pos]) {
+ break;
+ }
+
+ if (cmd[pos] == ';') {
+ semicolon = array.count;
+ ptr_array_append(&array, NULL);
+ pos++;
+ continue;
+ }
+
+ CommandParseError err;
+ size_t end = find_end(cmd, pos, &err);
+ if (err != CMDERR_NONE || end >= cmdline_pos) {
+ completion_pos = pos;
+ break;
+ }
+
+ if (semicolon + 1 == array.count) {
+ char *name = xstrslice(cmd, pos, end);
+ const char *value = runner.lookup_alias(name, runner.userdata);
+ if (value) {
+ size_t save = array.count;
+ if (parse_commands(&runner, &array, value) != CMDERR_NONE) {
+ for (size_t i = save, n = array.count; i < n; i++) {
+ free(array.ptrs[i]);
+ array.ptrs[i] = NULL;
+ }
+ array.count = save;
+ ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true));
+ } else {
+ // Remove NULL
+ array.count--;
+ }
+ } else {
+ ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true));
+ }
+ free(name);
+ } else {
+ ptr_array_append(&array, parse_command_arg(&runner, cmd + pos, end - pos, true));
+ }
+ pos = end;
+ }
+
+ const char *str = cmd + completion_pos;
+ size_t len = cmdline_pos - completion_pos;
+ if (is_var(str, len)) {
+ char *name = xstrslice(str, 1, len);
+ completion_pos++;
+ collect_env(e, &cs->completions, name);
+ collect_normal_vars(&cs->completions, name);
+ free(name);
+ } else {
+ cs->escaped = string_view(str, len);
+ cs->parsed = parse_command_arg(&runner, str, len, true);
+ cs->add_space_after_single_match = true;
+ size_t count = array.count;
+ char **args = count ? (char**)array.ptrs + 1 + semicolon : NULL;
+ size_t argc = count ? array.count - semicolon - 1 : 0;
+ collect_completions(e, args, argc);
+ }
+
+ ptr_array_free(&array);
+ ptr_array_sort(&cs->completions, strptrcmp);
+ cs->orig = cmd; // (takes ownership)
+ cs->tail = strview_from_cstring(cmd + cmdline_pos);
+ cs->head_len = completion_pos;
+}
+
+static void do_complete_command(CommandLine *cmdline)
+{
+ const CompletionState *cs = &cmdline->completion;
+ const PointerArray *arr = &cs->completions;
+ const StringView middle = strview_from_cstring(arr->ptrs[cs->idx]);
+ const StringView tail = cs->tail;
+ const size_t head_length = cs->head_len;
+
+ String buf = string_new(head_length + tail.length + middle.length + 16);
+ string_append_buf(&buf, cs->orig, head_length);
+ string_append_escaped_arg_sv(&buf, middle, !cs->tilde_expanded);
+
+ bool single_completion = (arr->count == 1);
+ if (single_completion && cs->add_space_after_single_match) {
+ string_append_byte(&buf, ' ');
+ }
+
+ size_t pos = buf.len;
+ string_append_strview(&buf, &tail);
+ cmdline_set_text(cmdline, string_borrow_cstring(&buf));
+ cmdline->pos = pos;
+ string_free(&buf);
+
+ if (single_completion) {
+ reset_completion(cmdline);
+ }
+}
+
+void complete_command_next(EditorState *e)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ const bool init = !cs->orig;
+ if (init) {
+ init_completion(e, &e->cmdline);
+ }
+ size_t count = cs->completions.count;
+ if (!count) {
+ return;
+ }
+ if (!init) {
+ cs->idx = size_increment_wrapped(cs->idx, count);
+ }
+ do_complete_command(&e->cmdline);
+}
+
+void complete_command_prev(EditorState *e)
+{
+ CompletionState *cs = &e->cmdline.completion;
+ const bool init = !cs->orig;
+ if (init) {
+ init_completion(e, &e->cmdline);
+ }
+ size_t count = cs->completions.count;
+ if (!count) {
+ return;
+ }
+ if (!init) {
+ cs->idx = size_decrement_wrapped(cs->idx, count);
+ }
+ do_complete_command(&e->cmdline);
+}
+
+void reset_completion(CommandLine *cmdline)
+{
+ CompletionState *cs = &cmdline->completion;
+ free(cs->parsed);
+ free(cs->orig);
+ ptr_array_free(&cs->completions);
+ *cs = (CompletionState){.orig = NULL};
+}
+
+void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix)
+{
+ for (HashMapIter it = hashmap_iter(map); hashmap_next(&it); ) {
+ const char *name = it.entry->key;
+ if (str_has_prefix(name, prefix)) {
+ ptr_array_append(a, xstrdup(name));
+ }
+ }
+}
diff --git a/examples/dte/completion.h b/examples/dte/completion.h
new file mode 100644
index 0000000..873e994
--- /dev/null
+++ b/examples/dte/completion.h
@@ -0,0 +1,21 @@
+#ifndef COMPLETION_H
+#define COMPLETION_H
+
+#include "cmdline.h"
+#include "editor.h"
+#include "util/hashmap.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+
+void complete_command_next(EditorState *e) NONNULL_ARGS;
+void complete_command_prev(EditorState *e) NONNULL_ARGS;
+void reset_completion(CommandLine *cmdline) NONNULL_ARGS;
+
+void collect_env(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *keystr_prefix) NONNULL_ARGS;
+void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_compilers(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/config.c b/examples/dte/config.c
new file mode 100644
index 0000000..dd24465
--- /dev/null
+++ b/examples/dte/config.c
@@ -0,0 +1,185 @@
+#include <errno.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include "config.h"
+#include "commands.h"
+#include "editor.h"
+#include "error.h"
+#include "syntax/color.h"
+#include "util/debug.h"
+#include "util/readfile.h"
+#include "util/str-util.h"
+#include "../build/builtin-config.h"
+
+ConfigState current_config;
+
+// Odd number of backslashes at end of line?
+static bool has_line_continuation(StringView line)
+{
+ ssize_t pos = line.length - 1;
+ while (pos >= 0 && line.data[pos] == '\\') {
+ pos--;
+ }
+ return (line.length - 1 - pos) & 1;
+}
+
+UNITTEST {
+ BUG_ON(has_line_continuation(string_view(NULL, 0)));
+ BUG_ON(has_line_continuation(strview_from_cstring("0")));
+ BUG_ON(!has_line_continuation(strview_from_cstring("1 \\")));
+ BUG_ON(has_line_continuation(strview_from_cstring("2 \\\\")));
+ BUG_ON(!has_line_continuation(strview_from_cstring("3 \\\\\\")));
+ BUG_ON(has_line_continuation(strview_from_cstring("4 \\\\\\\\")));
+}
+
+void exec_config(CommandRunner *runner, StringView config)
+{
+ String buf = string_new(1024);
+
+ for (size_t i = 0, n = config.length; i < n; current_config.line++) {
+ StringView line = buf_slice_next_line(config.data, &i, n);
+ strview_trim_left(&line);
+ if (buf.len == 0 && strview_has_prefix(&line, "#")) {
+ // Comment line
+ continue;
+ }
+ if (has_line_continuation(line)) {
+ line.length--;
+ string_append_strview(&buf, &line);
+ } else {
+ string_append_strview(&buf, &line);
+ handle_command(runner, string_borrow_cstring(&buf));
+ string_clear(&buf);
+ }
+ }
+
+ if (unlikely(buf.len)) {
+ // This can only happen if the last line had a line continuation
+ handle_command(runner, string_borrow_cstring(&buf));
+ }
+
+ string_free(&buf);
+}
+
+String dump_builtin_configs(void)
+{
+ String str = string_new(1024);
+ for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) {
+ string_append_cstring(&str, builtin_configs[i].name);
+ string_append_byte(&str, '\n');
+ }
+ return str;
+}
+
+const BuiltinConfig *get_builtin_config(const char *name)
+{
+ for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) {
+ if (streq(name, builtin_configs[i].name)) {
+ return &builtin_configs[i];
+ }
+ }
+ return NULL;
+}
+
+const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs)
+{
+ *nconfigs = ARRAYLEN(builtin_configs);
+ return &builtin_configs[0];
+}
+
+int do_read_config(CommandRunner *runner, const char *filename, ConfigFlags flags)
+{
+ const bool must_exist = flags & CFG_MUST_EXIST;
+ const bool builtin = flags & CFG_BUILTIN;
+
+ if (builtin) {
+ const BuiltinConfig *cfg = get_builtin_config(filename);
+ int err = 0;
+ if (cfg) {
+ current_config.file = filename;
+ current_config.line = 1;
+ exec_config(runner, cfg->text);
+ } else if (must_exist) {
+ error_msg (
+ "Error reading '%s': no built-in config exists for that path",
+ filename
+ );
+ err = 1;
+ }
+ return err;
+ }
+
+ char *buf;
+ ssize_t size = read_file(filename, &buf);
+ if (size < 0) {
+ int err = errno;
+ if (err != ENOENT || must_exist) {
+ error_msg("Error reading %s: %s", filename, strerror(err));
+ }
+ return err;
+ }
+
+ current_config.file = filename;
+ current_config.line = 1;
+ exec_config(runner, string_view(buf, size));
+ free(buf);
+ return 0;
+}
+
+int read_config(CommandRunner *runner, const char *filename, ConfigFlags flags)
+{
+ // Recursive
+ const ConfigState saved = current_config;
+ int ret = do_read_config(runner, filename, flags);
+ current_config = saved;
+ return ret;
+}
+
+void exec_builtin_color_reset(EditorState *e)
+{
+ clear_hl_colors(&e->colors);
+ const StringView reset = string_view(builtin_color_reset, sizeof(builtin_color_reset) - 1);
+ const ConfigState saved = current_config;
+ current_config.file = "color/reset";
+ current_config.line = 1;
+ exec_normal_config(e, reset);
+ current_config = saved;
+}
+
+void exec_builtin_rc(EditorState *e)
+{
+ exec_builtin_color_reset(e);
+ const StringView rc = string_view(builtin_rc, sizeof(builtin_rc) - 1);
+ const ConfigState saved = current_config;
+ current_config.file = "rc";
+ current_config.line = 1;
+ exec_normal_config(e, rc);
+ current_config = saved;
+}
+
+void collect_builtin_configs(PointerArray *a, const char *prefix)
+{
+ for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) {
+ const char *name = builtin_configs[i].name;
+ if (str_has_prefix(name, prefix)) {
+ ptr_array_append(a, xstrdup(name));
+ }
+ }
+}
+
+void collect_builtin_includes(PointerArray *a, const char *prefix)
+{
+ for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) {
+ const char *name = builtin_configs[i].name;
+ if (str_has_prefix(name, prefix) && !str_has_prefix(name, "syntax/")) {
+ ptr_array_append(a, xstrdup(name));
+ }
+ }
+}
+
+UNITTEST {
+ BUG_ON(!get_builtin_config("rc"));
+ BUG_ON(!get_builtin_config("color/reset"));
+}
diff --git a/examples/dte/config.h b/examples/dte/config.h
new file mode 100644
index 0000000..43d4c97
--- /dev/null
+++ b/examples/dte/config.h
@@ -0,0 +1,42 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#include <stddef.h>
+#include "command/run.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "util/string.h"
+
+typedef enum {
+ CFG_NOFLAGS = 0,
+ CFG_MUST_EXIST = 1 << 0,
+ CFG_BUILTIN = 1 << 1
+} ConfigFlags;
+
+typedef struct {
+ const char *const name;
+ const StringView text;
+} BuiltinConfig;
+
+typedef struct {
+ const char *file;
+ unsigned int line;
+} ConfigState;
+
+extern ConfigState current_config;
+
+struct EditorState;
+
+String dump_builtin_configs(void);
+const BuiltinConfig *get_builtin_config(const char *name) PURE;
+const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs);
+void exec_config(CommandRunner *runner, StringView config);
+int do_read_config(CommandRunner *runner, const char *filename, ConfigFlags flags) WARN_UNUSED_RESULT;
+int read_config(CommandRunner *runner, const char *filename, ConfigFlags f);
+void exec_builtin_color_reset(struct EditorState *e);
+void exec_builtin_rc(struct EditorState *e);
+void collect_builtin_configs(PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_builtin_includes(PointerArray *a, const char *prefix) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/convert.c b/examples/dte/convert.c
new file mode 100644
index 0000000..2020ee9
--- /dev/null
+++ b/examples/dte/convert.c
@@ -0,0 +1,581 @@
+#include <errno.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <string.h>
+#include "convert.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/log.h"
+#include "util/str-util.h"
+#include "util/utf8.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+
+struct FileEncoder {
+ struct cconv *cconv;
+ unsigned char *nbuf;
+ size_t nsize;
+ bool crlf;
+ int fd;
+};
+
+struct FileDecoder {
+ const char *encoding;
+ const unsigned char *ibuf;
+ ssize_t ipos, isize;
+ struct cconv *cconv;
+ bool (*read_line)(struct FileDecoder *dec, const char **linep, size_t *lenp);
+};
+
+const char *file_decoder_get_encoding(const FileDecoder *dec)
+{
+ return dec->encoding;
+}
+
+static bool read_utf8_line(FileDecoder *dec, const char **linep, size_t *lenp)
+{
+ const char *line = dec->ibuf + dec->ipos;
+ const char *nl = memchr(line, '\n', dec->isize - dec->ipos);
+ size_t len;
+
+ if (nl) {
+ len = nl - line;
+ dec->ipos += len + 1;
+ } else {
+ len = dec->isize - dec->ipos;
+ if (len == 0) {
+ return false;
+ }
+ dec->ipos += len;
+ }
+
+ *linep = line;
+ *lenp = len;
+ return true;
+}
+
+static size_t unix_to_dos (
+ FileEncoder *enc,
+ const unsigned char *buf,
+ size_t size
+) {
+ if (enc->nsize < size * 2) {
+ enc->nsize = size * 2;
+ xrenew(enc->nbuf, enc->nsize);
+ }
+ size_t d = 0;
+ for (size_t s = 0; s < size; s++) {
+ unsigned char ch = buf[s];
+ if (ch == '\n') {
+ enc->nbuf[d++] = '\r';
+ }
+ enc->nbuf[d++] = ch;
+ }
+ return d;
+}
+
+#ifdef ICONV_DISABLE // iconv not available; use basic, UTF-8 implementation:
+
+bool conversion_supported_by_iconv (
+ const char* UNUSED_ARG(from),
+ const char* UNUSED_ARG(to)
+) {
+ errno = EINVAL;
+ return false;
+}
+
+FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd)
+{
+ if (unlikely(encoding->type != UTF8)) {
+ errno = EINVAL;
+ return NULL;
+ }
+ FileEncoder *enc = xnew0(FileEncoder, 1);
+ enc->crlf = crlf;
+ enc->fd = fd;
+ return enc;
+}
+
+void free_file_encoder(FileEncoder *enc)
+{
+ free(enc->nbuf);
+ free(enc);
+}
+
+ssize_t file_encoder_write(FileEncoder *enc, const unsigned char *buf, size_t n)
+{
+ if (enc->crlf) {
+ n = unix_to_dos(enc, buf, n);
+ buf = enc->nbuf;
+ }
+ return xwrite_all(enc->fd, buf, n);
+}
+
+size_t file_encoder_get_nr_errors(const FileEncoder* UNUSED_ARG(enc))
+{
+ return 0;
+}
+
+FileDecoder *new_file_decoder(const char *encoding, const unsigned char *buf, size_t n)
+{
+ if (unlikely(encoding && !streq(encoding, "UTF-8"))) {
+ errno = EINVAL;
+ return NULL;
+ }
+ FileDecoder *dec = xnew0(FileDecoder, 1);
+ dec->ibuf = buf;
+ dec->isize = n;
+ return dec;
+}
+
+void free_file_decoder(FileDecoder *dec)
+{
+ free(dec);
+}
+
+bool file_decoder_read_line(FileDecoder *dec, const char **linep, size_t *lenp)
+{
+ return read_utf8_line(dec, linep, lenp);
+}
+
+#else // ICONV_DISABLE is undefined; use full iconv implementation:
+
+#include <iconv.h>
+
+static const unsigned char replacement[2] = "\xc2\xbf"; // U+00BF
+
+struct cconv {
+ iconv_t cd;
+ char *obuf;
+ size_t osize;
+ size_t opos;
+ size_t consumed;
+ size_t errors;
+
+ // Temporary input buffer
+ char tbuf[16];
+ size_t tcount;
+
+ // Replacement character 0xBF (inverted question mark)
+ char rbuf[4];
+ size_t rcount;
+
+ // Input character size in bytes, or zero for UTF-8
+ size_t char_size;
+};
+
+static struct cconv *create(iconv_t cd)
+{
+ struct cconv *c = xnew0(struct cconv, 1);
+ c->cd = cd;
+ c->osize = 8192;
+ c->obuf = xmalloc(c->osize);
+ return c;
+}
+
+static size_t encoding_char_size(const char *encoding)
+{
+ if (str_has_prefix(encoding, "UTF-16")) {
+ return 2;
+ }
+ if (str_has_prefix(encoding, "UTF-32")) {
+ return 4;
+ }
+ return 1;
+}
+
+static size_t iconv_wrapper (
+ iconv_t cd,
+ const char **restrict inbuf,
+ size_t *restrict inbytesleft,
+ char **restrict outbuf,
+ size_t *restrict outbytesleft
+) {
+ // POSIX defines the second parameter of iconv(3) as "char **restrict"
+ // but NetBSD declares it as "const char **restrict"
+#ifdef __NetBSD__
+ const char **restrict in = inbuf;
+#else
+ char **restrict in = (char **restrict)inbuf;
+#endif
+
+ return iconv(cd, in, inbytesleft, outbuf, outbytesleft);
+}
+
+static void encode_replacement(struct cconv *c)
+{
+ const char *ib = replacement;
+ char *ob = c->rbuf;
+ size_t ic = sizeof(replacement);
+ size_t oc = sizeof(c->rbuf);
+ size_t rc = iconv_wrapper(c->cd, &ib, &ic, &ob, &oc);
+
+ if (rc == (size_t)-1) {
+ c->rbuf[0] = '\xbf';
+ c->rcount = 1;
+ } else {
+ c->rcount = ob - c->rbuf;
+ }
+}
+
+static void resize_obuf(struct cconv *c)
+{
+ c->osize *= 2;
+ xrenew(c->obuf, c->osize);
+}
+
+static void add_replacement(struct cconv *c)
+{
+ if (c->osize - c->opos < 4) {
+ resize_obuf(c);
+ }
+
+ memcpy(c->obuf + c->opos, c->rbuf, c->rcount);
+ c->opos += c->rcount;
+}
+
+static size_t handle_invalid(struct cconv *c, const char *buf, size_t count)
+{
+ LOG_DEBUG("%zu %zu", c->char_size, count);
+ add_replacement(c);
+ if (c->char_size == 0) {
+ // Converting from UTF-8
+ size_t idx = 0;
+ CodePoint u = u_get_char(buf, count, &idx);
+ LOG_DEBUG("U+%04" PRIX32, u);
+ return idx;
+ }
+ if (c->char_size > count) {
+ // wtf
+ return 1;
+ }
+ return c->char_size;
+}
+
+static int xiconv(struct cconv *c, const char **ib, size_t *ic)
+{
+ while (1) {
+ char *ob = c->obuf + c->opos;
+ size_t oc = c->osize - c->opos;
+ size_t rc = iconv_wrapper(c->cd, ib, ic, &ob, &oc);
+ c->opos = ob - c->obuf;
+ if (rc == (size_t)-1) {
+ switch (errno) {
+ case EILSEQ:
+ c->errors++;
+ // Reset
+ iconv(c->cd, NULL, NULL, NULL, NULL);
+ return errno;
+ case EINVAL:
+ return errno;
+ case E2BIG:
+ resize_obuf(c);
+ continue;
+ default:
+ BUG("iconv: %s", strerror(errno));
+ }
+ } else {
+ c->errors += rc;
+ }
+ return 0;
+ }
+}
+
+static size_t convert_incomplete(struct cconv *c, const char *input, size_t len)
+{
+ size_t ipos = 0;
+ while (c->tcount < sizeof(c->tbuf) && ipos < len) {
+ c->tbuf[c->tcount++] = input[ipos++];
+ const char *ib = c->tbuf;
+ size_t ic = c->tcount;
+ int rc = xiconv(c, &ib, &ic);
+ if (ic > 0) {
+ memmove(c->tbuf, ib, ic);
+ }
+ c->tcount = ic;
+ if (rc == EINVAL) {
+ // Incomplete character at end of input buffer; try again
+ // with more input data
+ continue;
+ }
+ if (rc == EILSEQ) {
+ // Invalid multibyte sequence
+ size_t skip = handle_invalid(c, c->tbuf, c->tcount);
+ c->tcount -= skip;
+ if (c->tcount > 0) {
+ LOG_DEBUG("tcount=%zu, skip=%zu", c->tcount, skip);
+ memmove(c->tbuf, c->tbuf + skip, c->tcount);
+ continue;
+ }
+ return ipos;
+ }
+ break;
+ }
+
+ LOG_DEBUG("%zu %zu", ipos, c->tcount);
+ return ipos;
+}
+
+static void cconv_process(struct cconv *c, const char *input, size_t len)
+{
+ if (c->consumed > 0) {
+ size_t fill = c->opos - c->consumed;
+ memmove(c->obuf, c->obuf + c->consumed, fill);
+ c->opos = fill;
+ c->consumed = 0;
+ }
+
+ if (c->tcount > 0) {
+ size_t ipos = convert_incomplete(c, input, len);
+ input += ipos;
+ len -= ipos;
+ }
+
+ const char *ib = input;
+ for (size_t ic = len; ic > 0; ) {
+ int r = xiconv(c, &ib, &ic);
+ if (r == EINVAL) {
+ // Incomplete character at end of input buffer
+ if (ic < sizeof(c->tbuf)) {
+ memcpy(c->tbuf, ib, ic);
+ c->tcount = ic;
+ } else {
+ // FIXME
+ }
+ ic = 0;
+ continue;
+ }
+ if (r == EILSEQ) {
+ // Invalid multibyte sequence
+ size_t skip = handle_invalid(c, ib, ic);
+ ic -= skip;
+ ib += skip;
+ continue;
+ }
+ }
+}
+
+static struct cconv *cconv_to_utf8(const char *encoding)
+{
+ iconv_t cd = iconv_open("UTF-8", encoding);
+ if (cd == (iconv_t)-1) {
+ return NULL;
+ }
+ struct cconv *c = create(cd);
+ memcpy(c->rbuf, replacement, sizeof(replacement));
+ c->rcount = sizeof(replacement);
+ c->char_size = encoding_char_size(encoding);
+ return c;
+}
+
+static struct cconv *cconv_from_utf8(const char *encoding)
+{
+ iconv_t cd = iconv_open(encoding, "UTF-8");
+ if (cd == (iconv_t)-1) {
+ return NULL;
+ }
+ struct cconv *c = create(cd);
+ encode_replacement(c);
+ return c;
+}
+
+static void cconv_flush(struct cconv *c)
+{
+ if (c->tcount > 0) {
+ // Replace incomplete character at end of input buffer
+ LOG_DEBUG("incomplete character at EOF");
+ add_replacement(c);
+ c->tcount = 0;
+ }
+}
+
+static char *cconv_consume_line(struct cconv *c, size_t *len)
+{
+ char *line = c->obuf + c->consumed;
+ char *nl = memchr(line, '\n', c->opos - c->consumed);
+ if (!nl) {
+ *len = 0;
+ return NULL;
+ }
+
+ size_t n = nl - line + 1;
+ c->consumed += n;
+ *len = n;
+ return line;
+}
+
+static char *cconv_consume_all(struct cconv *c, size_t *len)
+{
+ char *buf = c->obuf + c->consumed;
+ *len = c->opos - c->consumed;
+ c->consumed = c->opos;
+ return buf;
+}
+
+static void cconv_free(struct cconv *c)
+{
+ iconv_close(c->cd);
+ free(c->obuf);
+ free(c);
+}
+
+bool conversion_supported_by_iconv(const char *from, const char *to)
+{
+ if (unlikely(from[0] == '\0' || to[0] == '\0')) {
+ errno = EINVAL;
+ return false;
+ }
+
+ iconv_t cd = iconv_open(to, from);
+ if (cd == (iconv_t)-1) {
+ return false;
+ }
+
+ iconv_close(cd);
+ return true;
+}
+
+FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd)
+{
+ FileEncoder *enc = xnew0(FileEncoder, 1);
+ enc->crlf = crlf;
+ enc->fd = fd;
+
+ if (encoding->type != UTF8) {
+ enc->cconv = cconv_from_utf8(encoding->name);
+ if (!enc->cconv) {
+ free(enc);
+ return NULL;
+ }
+ }
+
+ return enc;
+}
+
+void free_file_encoder(FileEncoder *enc)
+{
+ if (enc->cconv) {
+ cconv_free(enc->cconv);
+ }
+ free(enc->nbuf);
+ free(enc);
+}
+
+// NOTE: buf must contain whole characters!
+ssize_t file_encoder_write (
+ FileEncoder *enc,
+ const unsigned char *buf,
+ size_t size
+) {
+ if (enc->crlf) {
+ size = unix_to_dos(enc, buf, size);
+ buf = enc->nbuf;
+ }
+ if (enc->cconv) {
+ cconv_process(enc->cconv, buf, size);
+ cconv_flush(enc->cconv);
+ buf = cconv_consume_all(enc->cconv, &size);
+ }
+ return xwrite_all(enc->fd, buf, size);
+}
+
+size_t file_encoder_get_nr_errors(const FileEncoder *enc)
+{
+ return enc->cconv ? enc->cconv->errors : 0;
+}
+
+static bool fill(FileDecoder *dec)
+{
+ if (dec->ipos == dec->isize) {
+ return false;
+ }
+
+ // Smaller than cconv.obuf to make realloc less likely
+ size_t max = 7 * 1024;
+
+ size_t icount = MIN(dec->isize - dec->ipos, max);
+ cconv_process(dec->cconv, dec->ibuf + dec->ipos, icount);
+ dec->ipos += icount;
+ if (dec->ipos == dec->isize) {
+ // Must be flushed after all input has been fed
+ cconv_flush(dec->cconv);
+ }
+ return true;
+}
+
+static bool decode_and_read_line(FileDecoder *dec, const char **linep, size_t *lenp)
+{
+ char *line;
+ size_t len;
+ while (1) {
+ line = cconv_consume_line(dec->cconv, &len);
+ if (line || !fill(dec)) {
+ break;
+ }
+ }
+
+ if (line) {
+ // Newline not wanted
+ len--;
+ } else {
+ line = cconv_consume_all(dec->cconv, &len);
+ if (len == 0) {
+ return false;
+ }
+ }
+
+ *linep = line;
+ *lenp = len;
+ return true;
+}
+
+static bool set_encoding(FileDecoder *dec, const char *encoding)
+{
+ if (strcmp(encoding, "UTF-8") == 0) {
+ dec->read_line = read_utf8_line;
+ } else {
+ dec->cconv = cconv_to_utf8(encoding);
+ if (!dec->cconv) {
+ return false;
+ }
+ dec->read_line = decode_and_read_line;
+ }
+ dec->encoding = str_intern(encoding);
+ return true;
+}
+
+FileDecoder *new_file_decoder (
+ const char *encoding,
+ const unsigned char *buf,
+ size_t size
+) {
+ FileDecoder *dec = xnew0(FileDecoder, 1);
+ dec->ibuf = buf;
+ dec->isize = size;
+
+ if (!encoding) {
+ encoding = "UTF-8";
+ }
+
+ if (!set_encoding(dec, encoding)) {
+ free_file_decoder(dec);
+ return NULL;
+ }
+
+ return dec;
+}
+
+void free_file_decoder(FileDecoder *dec)
+{
+ if (dec->cconv) {
+ cconv_free(dec->cconv);
+ }
+ free(dec);
+}
+
+bool file_decoder_read_line(FileDecoder *dec, const char **linep, size_t *lenp)
+{
+ return dec->read_line(dec, linep, lenp);
+}
+
+#endif
diff --git a/examples/dte/convert.h b/examples/dte/convert.h
new file mode 100644
index 0000000..306609e
--- /dev/null
+++ b/examples/dte/convert.h
@@ -0,0 +1,24 @@
+#ifndef ENCODING_CONVERT_H
+#define ENCODING_CONVERT_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include "encoding.h"
+#include "util/macros.h"
+
+typedef struct FileDecoder FileDecoder;
+typedef struct FileEncoder FileEncoder;
+
+bool conversion_supported_by_iconv(const char *from, const char *to) NONNULL_ARGS;
+
+FileDecoder *new_file_decoder(const char *encoding, const unsigned char *buf, size_t size);
+void free_file_decoder(FileDecoder *dec);
+bool file_decoder_read_line(FileDecoder *dec, const char **line, size_t *len) NONNULL_ARGS WARN_UNUSED_RESULT;
+const char *file_decoder_get_encoding(const FileDecoder *dec) NONNULL_ARGS;
+
+FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd) NONNULL_ARGS;
+void free_file_encoder(FileEncoder *enc) NONNULL_ARGS;
+ssize_t file_encoder_write(FileEncoder *enc, const unsigned char *buf, size_t size) NONNULL_ARGS WARN_UNUSED_RESULT;
+size_t file_encoder_get_nr_errors(const FileEncoder *enc) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/copy.c b/examples/dte/copy.c
new file mode 100644
index 0000000..c3b989e
--- /dev/null
+++ b/examples/dte/copy.c
@@ -0,0 +1,74 @@
+#include <stdlib.h>
+#include "copy.h"
+#include "block-iter.h"
+#include "change.h"
+#include "misc.h"
+#include "move.h"
+#include "selection.h"
+#include "util/debug.h"
+
+void record_copy(Clipboard *clip, char *buf, size_t len, bool is_lines)
+{
+ BUG_ON(len && !buf);
+ free(clip->buf);
+ clip->buf = buf;
+ clip->len = len;
+ clip->is_lines = is_lines;
+}
+
+void copy(Clipboard *clip, View *view, size_t len, bool is_lines)
+{
+ if (len) {
+ char *buf = block_iter_get_bytes(&view->cursor, len);
+ record_copy(clip, buf, len, is_lines);
+ }
+}
+
+void cut(Clipboard *clip, View *view, size_t len, bool is_lines)
+{
+ if (len) {
+ copy(clip, view, len, is_lines);
+ buffer_delete_bytes(view, len);
+ }
+}
+
+void paste(Clipboard *clip, View *view, PasteLinesType type, bool move_after)
+{
+ if (clip->len == 0) {
+ return;
+ }
+
+ BUG_ON(!clip->buf);
+ if (!clip->is_lines || type == PASTE_LINES_INLINE) {
+ insert_text(view, clip->buf, clip->len, move_after);
+ return;
+ }
+
+ size_t del_count = 0;
+ if (view->selection) {
+ del_count = prepare_selection(view);
+ unselect(view);
+ }
+
+ const long x = view_get_preferred_x(view);
+ if (!del_count) {
+ if (type == PASTE_LINES_BELOW_CURSOR) {
+ block_iter_eat_line(&view->cursor);
+ } else {
+ BUG_ON(type != PASTE_LINES_ABOVE_CURSOR);
+ block_iter_bol(&view->cursor);
+ }
+ }
+
+ buffer_replace_bytes(view, del_count, clip->buf, clip->len);
+
+ if (move_after) {
+ block_iter_skip_bytes(&view->cursor, clip->len);
+ } else {
+ // Try to keep cursor column
+ move_to_preferred_x(view, x);
+ }
+
+ // New preferred_x
+ view_reset_preferred_x(view);
+}
diff --git a/examples/dte/copy.h b/examples/dte/copy.h
new file mode 100644
index 0000000..2281b09
--- /dev/null
+++ b/examples/dte/copy.h
@@ -0,0 +1,26 @@
+#ifndef COPY_H
+#define COPY_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+#include "view.h"
+
+typedef struct {
+ char *buf;
+ size_t len;
+ bool is_lines;
+} Clipboard;
+
+typedef enum {
+ PASTE_LINES_BELOW_CURSOR,
+ PASTE_LINES_ABOVE_CURSOR,
+ PASTE_LINES_INLINE,
+} PasteLinesType;
+
+void record_copy(Clipboard *clip, char *buf, size_t len, bool is_lines);
+void copy(Clipboard *clip, View *view, size_t len, bool is_lines);
+void cut(Clipboard *clip, View *view, size_t len, bool is_lines);
+void paste(Clipboard *clip, View *view, PasteLinesType type, bool move_after);
+
+#endif
diff --git a/examples/dte/ctags.c b/examples/dte/ctags.c
new file mode 100644
index 0000000..6035630
--- /dev/null
+++ b/examples/dte/ctags.c
@@ -0,0 +1,157 @@
+#include <stdlib.h>
+#include <string.h>
+#include "ctags.h"
+#include "util/ascii.h"
+#include "util/debug.h"
+#include "util/str-util.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+
+static size_t parse_ex_pattern(const char *buf, size_t size, char **escaped)
+{
+ BUG_ON(size == 0);
+ BUG_ON(buf[0] != '/' && buf[0] != '?');
+
+ // The search pattern is not a real regular expression; special characters
+ // need to be escaped
+ char *pattern = xmalloc(size * 2);
+ char open_delim = buf[0];
+ for (size_t i = 1, j = 0; i < size; i++) {
+ if (unlikely(buf[i] == '\0')) {
+ break;
+ }
+ if (buf[i] == '\\' && i + 1 < size) {
+ i++;
+ if (buf[i] == '\\') {
+ pattern[j++] = '\\';
+ }
+ pattern[j++] = buf[i];
+ continue;
+ }
+ if (buf[i] == open_delim) {
+ pattern[j] = '\0';
+ *escaped = pattern;
+ return i + 1;
+ }
+ char c = buf[i];
+ if (c == '*' || c == '[' || c == ']') {
+ pattern[j++] = '\\';
+ }
+ pattern[j++] = buf[i];
+ }
+
+ free(pattern);
+ return 0;
+}
+
+static size_t parse_ex_cmd(Tag *tag, const char *buf, size_t size)
+{
+ if (unlikely(size == 0)) {
+ return 0;
+ }
+
+ size_t n;
+ if (buf[0] == '/' || buf[0] == '?') {
+ n = parse_ex_pattern(buf, size, &tag->pattern);
+ } else {
+ n = buf_parse_ulong(buf, size, &tag->lineno);
+ }
+
+ if (n == 0) {
+ return 0;
+ }
+
+ if (n + 1 < size && buf[n] == ';' && buf[n + 1] == '"') {
+ n += 2;
+ }
+
+ return n;
+}
+
+bool parse_ctags_line(Tag *tag, const char *line, size_t line_len)
+{
+ size_t pos = 0;
+ *tag = (Tag){.name = get_delim(line, &pos, line_len, '\t')};
+ if (tag->name.length == 0 || pos >= line_len) {
+ return false;
+ }
+
+ tag->filename = get_delim(line, &pos, line_len, '\t');
+ if (tag->filename.length == 0 || pos >= line_len) {
+ return false;
+ }
+
+ size_t len = parse_ex_cmd(tag, line + pos, line_len - pos);
+ if (len == 0) {
+ BUG_ON(tag->pattern);
+ return false;
+ }
+
+ pos += len;
+ if (pos >= line_len) {
+ return true;
+ }
+
+ /*
+ * Extension fields (key:[value]):
+ *
+ * file: visibility limited to this file
+ * struct:NAME tag is member of struct NAME
+ * union:NAME tag is member of union NAME
+ * typeref:struct:NAME::MEMBER_TYPE MEMBER_TYPE is type of the tag
+ */
+ if (line[pos++] != '\t') {
+ // free `pattern` allocated by parse_ex_cmd()
+ free_tag(tag);
+ tag->pattern = NULL;
+ return false;
+ }
+
+ while (pos < line_len) {
+ StringView field = get_delim(line, &pos, line_len, '\t');
+ if (field.length == 1 && ascii_isalpha(field.data[0])) {
+ tag->kind = field.data[0];
+ } else if (strview_equal_cstring(&field, "file:")) {
+ tag->local = true;
+ }
+ // TODO: struct/union/typeref
+ }
+
+ return true;
+}
+
+bool next_tag (
+ const char *buf,
+ size_t buf_len,
+ size_t *posp,
+ const StringView *prefix,
+ bool exact,
+ Tag *tag
+) {
+ const char *p = prefix->data;
+ size_t plen = prefix->length;
+ for (size_t pos = *posp; pos < buf_len; ) {
+ StringView line = buf_slice_next_line(buf, &pos, buf_len);
+ if (line.length == 0 || line.data[0] == '!') {
+ continue;
+ }
+ if (!strview_has_strn_prefix(&line, p, plen)) {
+ continue;
+ }
+ if (exact && line.data[plen] != '\t') {
+ continue;
+ }
+ if (!parse_ctags_line(tag, line.data, line.length)) {
+ continue;
+ }
+ *posp = pos;
+ return true;
+ }
+ return false;
+}
+
+// NOTE: tag itself is not freed
+void free_tag(Tag *tag)
+{
+ free(tag->pattern);
+}
diff --git a/examples/dte/ctags.h b/examples/dte/ctags.h
new file mode 100644
index 0000000..4f22ba6
--- /dev/null
+++ b/examples/dte/ctags.h
@@ -0,0 +1,31 @@
+#ifndef CTAGS_H
+#define CTAGS_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+#include "util/string-view.h"
+
+typedef struct {
+ StringView name; // Name of tag (points into TagFile::buf)
+ StringView filename; // File containing tag (points into TagFile::buf)
+ char *pattern; // Regex pattern used to locate tag (escaped ex command)
+ unsigned long lineno; // Line number in file (mutually exclusive with pattern)
+ char kind; // ASCII letter representing type of tag (e.g. f=function)
+ bool local; // Indicates if tag is local to file (e.g. "static" in C)
+} Tag;
+
+NONNULL_ARGS WARN_UNUSED_RESULT
+bool next_tag (
+ const char *buf,
+ size_t buf_len,
+ size_t *posp,
+ const StringView *prefix,
+ bool exact,
+ Tag *t
+);
+
+bool parse_ctags_line(Tag *t, const char *line, size_t line_len) NONNULL_ARG(1);
+void free_tag(Tag *t) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/edit.c b/examples/dte/edit.c
new file mode 100644
index 0000000..337a1e9
--- /dev/null
+++ b/examples/dte/edit.c
@@ -0,0 +1,394 @@
+#include <string.h>
+#include "edit.h"
+#include "block.h"
+#include "buffer.h"
+#include "syntax/highlight.h"
+#include "util/debug.h"
+#include "util/list.h"
+#include "util/xmalloc.h"
+
+enum {
+ BLOCK_EDIT_SIZE = 512
+};
+
+static void sanity_check_blocks(const View *view, bool check_newlines)
+{
+#if DEBUG >= 1
+ const Buffer *buffer = view->buffer;
+ BUG_ON(list_empty(&buffer->blocks));
+ BUG_ON(view->cursor.offset > view->cursor.blk->size);
+
+ const Block *blk = BLOCK(buffer->blocks.next);
+ if (blk->size == 0) {
+ // The only time a zero-sized block is valid is when it's the
+ // first and only block
+ BUG_ON(buffer->blocks.next->next != &buffer->blocks);
+ BUG_ON(view->cursor.blk != blk);
+ return;
+ }
+
+ bool cursor_seen = false;
+ block_for_each(blk, &buffer->blocks) {
+ const size_t size = blk->size;
+ BUG_ON(size == 0);
+ BUG_ON(size > blk->alloc);
+ if (blk == view->cursor.blk) {
+ cursor_seen = true;
+ }
+ if (check_newlines) {
+ BUG_ON(blk->data[size - 1] != '\n');
+ }
+ if (DEBUG > 2) {
+ BUG_ON(count_nl(blk->data, size) != blk->nl);
+ }
+ }
+ BUG_ON(!cursor_seen);
+#else
+ // Silence "unused parameter" warnings
+ (void)view;
+ (void)check_newlines;
+#endif
+}
+
+static size_t copy_count_nl(char *dst, const char *src, size_t len)
+{
+ size_t nl = 0;
+ for (size_t i = 0; i < len; i++) {
+ dst[i] = src[i];
+ if (src[i] == '\n') {
+ nl++;
+ }
+ }
+ return nl;
+}
+
+static size_t insert_to_current(BlockIter *cursor, const char *buf, size_t len)
+{
+ Block *blk = cursor->blk;
+ size_t offset = cursor->offset;
+ size_t size = blk->size + len;
+
+ if (size > blk->alloc) {
+ blk->alloc = round_size_to_next_multiple(size, BLOCK_ALLOC_MULTIPLE);
+ xrenew(blk->data, blk->alloc);
+ }
+ memmove(blk->data + offset + len, blk->data + offset, blk->size - offset);
+ size_t nl = copy_count_nl(blk->data + offset, buf, len);
+ blk->nl += nl;
+ blk->size = size;
+ return nl;
+}
+
+/*
+ * Combine current block and new data into smaller blocks:
+ * - Block _must_ contain whole lines
+ * - Block _must_ contain at least one line
+ * - Preferred maximum size of block is BLOCK_EDIT_SIZE
+ * - Size of any block can be larger than BLOCK_EDIT_SIZE
+ * only if there's a very long line
+ */
+static size_t split_and_insert(BlockIter *cursor, const char *buf, size_t len)
+{
+ Block *blk = cursor->blk;
+ ListHead *prev_node = blk->node.prev;
+ const char *buf1 = blk->data;
+ const char *buf2 = buf;
+ const char *buf3 = blk->data + cursor->offset;
+ size_t size1 = cursor->offset;
+ size_t size2 = len;
+ size_t size3 = blk->size - size1;
+ size_t total = size1 + size2 + size3;
+ size_t start = 0; // Beginning of new block
+ size_t size = 0; // Size of new block
+ size_t pos = 0; // Current position
+ size_t nl_added = 0;
+
+ while (start < total) {
+ // Size of new block if next line would be added
+ size_t new_size = 0;
+ size_t copied = 0;
+
+ if (pos < size1) {
+ const char *nl = memchr(buf1 + pos, '\n', size1 - pos);
+ if (nl) {
+ new_size = nl - buf1 + 1 - start;
+ }
+ }
+
+ if (!new_size && pos < size1 + size2) {
+ size_t offset = 0;
+ if (pos > size1) {
+ offset = pos - size1;
+ }
+
+ const char *nl = memchr(buf2 + offset, '\n', size2 - offset);
+ if (nl) {
+ new_size = size1 + nl - buf2 + 1 - start;
+ }
+ }
+
+ if (!new_size && pos < total) {
+ size_t offset = 0;
+ if (pos > size1 + size2) {
+ offset = pos - size1 - size2;
+ }
+
+ const char *nl = memchr(buf3 + offset, '\n', size3 - offset);
+ if (nl) {
+ new_size = size1 + size2 + nl - buf3 + 1 - start;
+ } else {
+ new_size = total - start;
+ }
+ }
+
+ if (new_size <= BLOCK_EDIT_SIZE) {
+ // Fits
+ size = new_size;
+ pos = start + new_size;
+ if (pos < total) {
+ continue;
+ }
+ } else {
+ // Does not fit
+ if (!size) {
+ // One block containing one very long line
+ size = new_size;
+ pos = start + new_size;
+ }
+ }
+
+ BUG_ON(!size);
+ Block *new = block_new(size);
+ if (start < size1) {
+ size_t avail = size1 - start;
+ size_t count = MIN(size, avail);
+ new->nl += copy_count_nl(new->data, buf1 + start, count);
+ copied += count;
+ start += count;
+ }
+ if (start >= size1 && start < size1 + size2) {
+ size_t offset = start - size1;
+ size_t avail = size2 - offset;
+ size_t count = MIN(size - copied, avail);
+ new->nl += copy_count_nl(new->data + copied, buf2 + offset, count);
+ copied += count;
+ start += count;
+ }
+ if (start >= size1 + size2) {
+ size_t offset = start - size1 - size2;
+ size_t avail = size3 - offset;
+ size_t count = size - copied;
+ BUG_ON(count > avail);
+ new->nl += copy_count_nl(new->data + copied, buf3 + offset, count);
+ copied += count;
+ start += count;
+ }
+
+ new->size = size;
+ BUG_ON(copied != size);
+ list_add_before(&new->node, &blk->node);
+
+ nl_added += new->nl;
+ size = 0;
+ }
+
+ cursor->blk = BLOCK(prev_node->next);
+ while (cursor->offset > cursor->blk->size) {
+ cursor->offset -= cursor->blk->size;
+ cursor->blk = BLOCK(cursor->blk->node.next);
+ }
+
+ nl_added -= blk->nl;
+ block_free(blk);
+ return nl_added;
+}
+
+static size_t insert_bytes(BlockIter *cursor, const char *buf, size_t len)
+{
+ // Blocks must contain whole lines.
+ // Last char of buf might not be newline.
+ block_iter_normalize(cursor);
+
+ Block *blk = cursor->blk;
+ size_t new_size = blk->size + len;
+ if (new_size <= blk->alloc || new_size <= BLOCK_EDIT_SIZE) {
+ return insert_to_current(cursor, buf, len);
+ }
+
+ if (blk->nl <= 1 && !memchr(buf, '\n', len)) {
+ // Can't split this possibly very long line.
+ // insert_to_current() is much faster than split_and_insert().
+ return insert_to_current(cursor, buf, len);
+ }
+ return split_and_insert(cursor, buf, len);
+}
+
+void do_insert(View *view, const char *buf, size_t len)
+{
+ Buffer *buffer = view->buffer;
+ size_t nl = insert_bytes(&view->cursor, buf, len);
+ buffer->nl += nl;
+ sanity_check_blocks(view, true);
+
+ view_update_cursor_y(view);
+ buffer_mark_lines_changed(buffer, view->cy, nl ? LONG_MAX : view->cy);
+ if (buffer->syn) {
+ hl_insert(buffer, view->cy, nl);
+ }
+}
+
+static bool only_block(const Buffer *buffer, const Block *blk)
+{
+ return blk->node.prev == &buffer->blocks && blk->node.next == &buffer->blocks;
+}
+
+char *do_delete(View *view, size_t len, bool sanity_check_newlines)
+{
+ ListHead *saved_prev_node = NULL;
+ Block *blk = view->cursor.blk;
+ size_t offset = view->cursor.offset;
+ size_t pos = 0;
+ size_t deleted_nl = 0;
+
+ if (!len) {
+ return NULL;
+ }
+
+ if (!offset) {
+ // The block where cursor is can become empty and thereby may be deleted
+ saved_prev_node = blk->node.prev;
+ }
+
+ Buffer *buffer = view->buffer;
+ char *deleted = xmalloc(len);
+ while (pos < len) {
+ ListHead *next = blk->node.next;
+ size_t avail = blk->size - offset;
+ size_t count = MIN(len - pos, avail);
+ size_t nl = copy_count_nl(deleted + pos, blk->data + offset, count);
+ if (count < avail) {
+ memmove (
+ blk->data + offset,
+ blk->data + offset + count,
+ avail - count
+ );
+ }
+
+ deleted_nl += nl;
+ buffer->nl -= nl;
+ blk->nl -= nl;
+ blk->size -= count;
+ if (!blk->size && !only_block(buffer, blk)) {
+ block_free(blk);
+ }
+
+ offset = 0;
+ pos += count;
+ blk = BLOCK(next);
+
+ BUG_ON(pos < len && next == &buffer->blocks);
+ }
+
+ if (saved_prev_node) {
+ // Cursor was at beginning of a block that was possibly deleted
+ if (saved_prev_node->next == &buffer->blocks) {
+ view->cursor.blk = BLOCK(saved_prev_node);
+ view->cursor.offset = view->cursor.blk->size;
+ } else {
+ view->cursor.blk = BLOCK(saved_prev_node->next);
+ }
+ }
+
+ blk = view->cursor.blk;
+ if (
+ blk->size
+ && blk->data[blk->size - 1] != '\n'
+ && blk->node.next != &buffer->blocks
+ ) {
+ Block *next = BLOCK(blk->node.next);
+ size_t size = blk->size + next->size;
+
+ if (size > blk->alloc) {
+ blk->alloc = round_size_to_next_multiple(size, BLOCK_ALLOC_MULTIPLE);
+ xrenew(blk->data, blk->alloc);
+ }
+ memcpy(blk->data + blk->size, next->data, next->size);
+ blk->size = size;
+ blk->nl += next->nl;
+ block_free(next);
+ }
+
+ sanity_check_blocks(view, sanity_check_newlines);
+
+ view_update_cursor_y(view);
+ buffer_mark_lines_changed(buffer, view->cy, deleted_nl ? LONG_MAX : view->cy);
+ if (buffer->syn) {
+ hl_delete(buffer, view->cy, deleted_nl);
+ }
+ return deleted;
+}
+
+char *do_replace(View *view, size_t del, const char *buf, size_t ins)
+{
+ block_iter_normalize(&view->cursor);
+ Block *blk = view->cursor.blk;
+ size_t offset = view->cursor.offset;
+
+ size_t avail = blk->size - offset;
+ if (del >= avail) {
+ goto slow;
+ }
+
+ size_t new_size = blk->size + ins - del;
+ if (new_size > BLOCK_EDIT_SIZE) {
+ // Should split
+ if (blk->nl > 1 || memchr(buf, '\n', ins)) {
+ // Most likely can be split
+ goto slow;
+ }
+ }
+
+ if (new_size > blk->alloc) {
+ blk->alloc = round_size_to_next_multiple(new_size, BLOCK_ALLOC_MULTIPLE);
+ xrenew(blk->data, blk->alloc);
+ }
+
+ // Modification is limited to one block
+ Buffer *buffer = view->buffer;
+ char *ptr = blk->data + offset;
+ char *deleted = xmalloc(del);
+ size_t del_nl = copy_count_nl(deleted, ptr, del);
+ blk->nl -= del_nl;
+ buffer->nl -= del_nl;
+
+ if (del != ins) {
+ memmove(ptr + ins, ptr + del, avail - del);
+ }
+
+ size_t ins_nl = copy_count_nl(ptr, buf, ins);
+ blk->nl += ins_nl;
+ buffer->nl += ins_nl;
+ blk->size = new_size;
+ sanity_check_blocks(view, true);
+ view_update_cursor_y(view);
+
+ // If the number of inserted and removed bytes are the same, some
+ // line(s) changed but the lines after them didn't move up or down
+ long max = (del_nl == ins_nl) ? view->cy + del_nl : LONG_MAX;
+ buffer_mark_lines_changed(buffer, view->cy, max);
+
+ if (buffer->syn) {
+ hl_delete(buffer, view->cy, del_nl);
+ hl_insert(buffer, view->cy, ins_nl);
+ }
+
+ return deleted;
+
+slow:
+ // The "sanity_check_newlines" argument of do_delete() is false here
+ // because it may be removing a terminating newline that do_insert()
+ // is going to insert again at a different position:
+ deleted = do_delete(view, del, false);
+ do_insert(view, buf, ins);
+ return deleted;
+}
diff --git a/examples/dte/edit.h b/examples/dte/edit.h
new file mode 100644
index 0000000..2de58b8
--- /dev/null
+++ b/examples/dte/edit.h
@@ -0,0 +1,13 @@
+#ifndef EDIT_H
+#define EDIT_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+#include "view.h"
+
+void do_insert(View *view, const char *buf, size_t len) NONNULL_ARG(1);
+char *do_delete(View *view, size_t len, bool sanity_check_newlines) NONNULL_ARGS;
+char *do_replace(View *view, size_t del, const char *buf, size_t ins) NONNULL_ARGS_AND_RETURN;
+
+#endif
diff --git a/examples/dte/editor.c b/examples/dte/editor.c
new file mode 100644
index 0000000..aa88d6e
--- /dev/null
+++ b/examples/dte/editor.c
@@ -0,0 +1,321 @@
+#include "compat.h"
+#include <errno.h>
+#include <langinfo.h>
+#include <locale.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "editor.h"
+#include "bind.h"
+#include "bookmark.h"
+#include "command/macro.h"
+#include "commands.h"
+#include "compiler.h"
+#include "encoding.h"
+#include "error.h"
+#include "file-option.h"
+#include "filetype.h"
+#include "lock.h"
+#include "mode.h"
+#include "regexp.h"
+#include "screen.h"
+#include "search.h"
+#include "signals.h"
+#include "syntax/syntax.h"
+#include "tag.h"
+#include "terminal/input.h"
+#include "terminal/mode.h"
+#include "terminal/output.h"
+#include "terminal/style.h"
+#include "util/ascii.h"
+#include "util/debug.h"
+#include "util/exitcode.h"
+#include "util/intern.h"
+#include "util/log.h"
+#include "util/utf8.h"
+#include "util/xmalloc.h"
+#include "util/xstdio.h"
+#include "window.h"
+#include "../build/version.h"
+
+static void set_and_check_locale(void)
+{
+ const char *default_locale = setlocale(LC_CTYPE, "");
+ if (likely(default_locale)) {
+ const char *codeset = nl_langinfo(CODESET);
+ LOG_INFO("locale: %s (codeset: %s)", default_locale, codeset);
+ if (likely(lookup_encoding(codeset) == UTF8)) {
+ return;
+ }
+ } else {
+ LOG_ERROR("failed to set default locale");
+ }
+
+ static const char fallbacks[][12] = {"C.UTF-8", "en_US.UTF-8"};
+ const char *fallback = NULL;
+ for (size_t i = 0; i < ARRAYLEN(fallbacks) && !fallback; i++) {
+ fallback = setlocale(LC_CTYPE, fallbacks[i]);
+ }
+ if (fallback) {
+ LOG_INFO("using fallback locale for LC_CTYPE: %s", fallback);
+ return;
+ }
+
+ LOG_ERROR("no UTF-8 fallback locales found");
+ fputs("setlocale() failed\n", stderr);
+ exit(EX_CONFIG);
+}
+
+EditorState *init_editor_state(void)
+{
+ EditorState *e = xnew(EditorState, 1);
+ *e = (EditorState) {
+ .status = EDITOR_INITIALIZING,
+ .input_mode = INPUT_NORMAL,
+ .version = VERSION,
+ .command_history = {
+ .max_entries = 512,
+ },
+ .search_history = {
+ .max_entries = 128,
+ },
+ .cursor_styles = {
+ [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT},
+ [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
+ [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
+ [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
+ },
+ .modes = {
+ [INPUT_NORMAL] = {.cmds = &normal_commands},
+ [INPUT_COMMAND] = {.cmds = &cmd_mode_commands},
+ [INPUT_SEARCH] = {.cmds = &search_mode_commands},
+ },
+ .options = {
+ .auto_indent = true,
+ .detect_indent = 0,
+ .editorconfig = false,
+ .emulate_tab = false,
+ .expand_tab = false,
+ .file_history = true,
+ .indent_width = 8,
+ .overwrite = false,
+ .save_unmodified = SAVE_FULL,
+ .syntax = true,
+ .tab_width = 8,
+ .text_width = 72,
+ .ws_error = WSE_SPECIAL,
+
+ // Global-only options
+ .case_sensitive_search = CSS_TRUE,
+ .crlf_newlines = false,
+ .display_special = false,
+ .esc_timeout = 100,
+ .filesize_limit = 250,
+ .lock_files = true,
+ .optimize_true_color = true,
+ .scroll_margin = 0,
+ .select_cursor_char = true,
+ .set_window_title = false,
+ .show_line_numbers = false,
+ .statusline_left = str_intern(" %f%s%m%s%r%s%M"),
+ .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "),
+ .tab_bar = true,
+ .utf8_bom = false,
+ }
+ };
+
+ sanity_check_global_options(&e->options);
+
+ for (size_t i = 0; i < ARRAYLEN(e->modes); i++) {
+ const CommandSet *cmds = e->modes[i].cmds;
+ BUG_ON(!cmds);
+ BUG_ON(!cmds->lookup);
+ }
+
+ const char *home = getenv("HOME");
+ const char *dte_home = getenv("DTE_HOME");
+ e->home_dir = strview_intern(home ? home : "");
+ if (dte_home) {
+ e->user_config_dir = xstrdup(dte_home);
+ } else {
+ e->user_config_dir = xasprintf("%s/.dte", e->home_dir.data);
+ }
+
+ LOG_INFO("dte version: " VERSION);
+ LOG_INFO("features:%s", feature_string);
+
+ pid_t pid = getpid();
+ bool leader = pid == getsid(0);
+ e->session_leader = leader;
+ LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : "");
+
+ pid_t pgid = getpgrp();
+ if (pgid != pid) {
+ LOG_INFO("pgid: %jd", (intmax_t)pgid);
+ }
+
+ set_and_check_locale();
+ init_file_locks_context(e->user_config_dir, pid);
+
+ // Allow child processes to detect that they're running under dte
+ if (unlikely(setenv("DTE_VERSION", VERSION, true) != 0)) {
+ fatal_error("setenv", errno);
+ }
+
+ RegexpWordBoundaryTokens *wb = &e->regexp_word_tokens;
+ if (regexp_init_word_boundary_tokens(wb)) {
+ LOG_INFO("regex word boundary tokens detected: %s %s", wb->start, wb->end);
+ } else {
+ LOG_WARNING("no regex word boundary tokens detected");
+ }
+
+ term_input_init(&e->terminal.ibuf);
+ term_output_init(&e->terminal.obuf);
+ hashmap_init(&e->aliases, 32);
+ intmap_init(&e->modes[INPUT_NORMAL].key_bindings, 150);
+ intmap_init(&e->modes[INPUT_COMMAND].key_bindings, 40);
+ intmap_init(&e->modes[INPUT_SEARCH].key_bindings, 40);
+ return e;
+}
+
+void free_editor_state(EditorState *e)
+{
+ free(e->clipboard.buf);
+ free_file_options(&e->file_options);
+ free_filetypes(&e->filetypes);
+ free_syntaxes(&e->syntaxes);
+ file_history_free(&e->file_history);
+ history_free(&e->command_history);
+ history_free(&e->search_history);
+ search_free_regexp(&e->search);
+ term_output_free(&e->terminal.obuf);
+ term_input_free(&e->terminal.ibuf);
+ cmdline_free(&e->cmdline);
+ clear_messages(&e->messages);
+ free_macro(&e->macro);
+ tag_file_free(&e->tagfile);
+
+ ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free));
+ ptr_array_free_cb(&e->buffers, FREE_FUNC(free_buffer));
+ hashmap_free(&e->compilers, FREE_FUNC(free_compiler));
+ hashmap_free(&e->colors.other, free);
+ hashmap_free(&e->aliases, free);
+
+ for (size_t i = 0; i < ARRAYLEN(e->modes); i++) {
+ free_bindings(&e->modes[i].key_bindings);
+ }
+
+ free_interned_strings();
+ free_interned_regexps();
+
+ // TODO: intern this (so that it's freed by free_intern_pool())
+ free((void*)e->user_config_dir);
+
+ free(e);
+}
+
+static void sanity_check(const View *view)
+{
+#if DEBUG >= 1
+ const Block *blk;
+ block_for_each(blk, &view->buffer->blocks) {
+ if (blk == view->cursor.blk) {
+ BUG_ON(view->cursor.offset > view->cursor.blk->size);
+ return;
+ }
+ }
+ BUG("cursor not seen");
+#else
+ (void)view;
+#endif
+}
+
+void any_key(Terminal *term, unsigned int esc_timeout)
+{
+ KeyCode key;
+ xfputs("Press any key to continue\r\n", stderr);
+ while ((key = term_read_key(term, esc_timeout)) == KEY_NONE) {
+ ;
+ }
+ bool bracketed_paste = key == KEY_BRACKETED_PASTE;
+ if (bracketed_paste || key == KEY_DETECTED_PASTE) {
+ term_discard_paste(&term->ibuf, bracketed_paste);
+ }
+}
+
+NOINLINE
+void ui_resize(EditorState *e)
+{
+ if (e->status == EDITOR_INITIALIZING) {
+ return;
+ }
+ resized = 0;
+ update_screen_size(&e->terminal, e->root_frame);
+ normal_update(e);
+}
+
+void ui_start(EditorState *e)
+{
+ if (e->status == EDITOR_INITIALIZING) {
+ return;
+ }
+
+ // Note: the order of these calls is important - Kitty saves/restores
+ // some terminal state when switching buffers, so switching to the
+ // alternate screen buffer needs to happen before modes are enabled
+ term_use_alt_screen_buffer(&e->terminal);
+ term_enable_private_modes(&e->terminal);
+
+ ui_resize(e);
+}
+
+void ui_end(EditorState *e)
+{
+ if (e->status == EDITOR_INITIALIZING) {
+ return;
+ }
+ Terminal *term = &e->terminal;
+ TermOutputBuffer *obuf = &term->obuf;
+ term_clear_screen(obuf);
+ term_move_cursor(obuf, 0, term->height - 1);
+ term_restore_cursor_style(term);
+ term_show_cursor(term);
+ term_restore_private_modes(term);
+ term_use_normal_screen_buffer(term);
+ term_end_sync_update(term);
+ term_output_flush(obuf);
+ term_cooked();
+}
+
+int main_loop(EditorState *e)
+{
+ while (e->status == EDITOR_RUNNING) {
+ if (unlikely(resized)) {
+ LOG_INFO("SIGWINCH received");
+ ui_resize(e);
+ }
+
+ KeyCode key = term_read_key(&e->terminal, e->options.esc_timeout);
+ if (unlikely(key == KEY_NONE)) {
+ continue;
+ }
+
+ const ScreenState s = {
+ .is_modified = buffer_modified(e->buffer),
+ .id = e->buffer->id,
+ .cy = e->view->cy,
+ .vx = e->view->vx,
+ .vy = e->view->vy
+ };
+
+ clear_error();
+ handle_input(e, key);
+ sanity_check(e->view);
+ update_screen(e, &s);
+ }
+
+ BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX);
+ return e->status;
+}
diff --git a/examples/dte/editor.h b/examples/dte/editor.h
new file mode 100644
index 0000000..86a8103
--- /dev/null
+++ b/examples/dte/editor.h
@@ -0,0 +1,121 @@
+#ifndef EDITOR_H
+#define EDITOR_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "buffer.h"
+#include "cmdline.h"
+#include "command/macro.h"
+#include "command/run.h"
+#include "commands.h"
+#include "copy.h"
+#include "file-history.h"
+#include "frame.h"
+#include "history.h"
+#include "msg.h"
+#include "options.h"
+#include "regexp.h"
+#include "search.h"
+#include "syntax/color.h"
+#include "tag.h"
+#include "terminal/cursor.h"
+#include "terminal/terminal.h"
+#include "util/debug.h"
+#include "util/hashmap.h"
+#include "util/intmap.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "view.h"
+
+typedef enum {
+ EDITOR_INITIALIZING = -2,
+ EDITOR_RUNNING = -1,
+ // Values 0-125 are exit codes
+ EDITOR_EXIT_OK = 0,
+ EDITOR_EXIT_MAX = 125,
+} EditorStatus;
+
+typedef enum {
+ INPUT_NORMAL,
+ INPUT_COMMAND,
+ INPUT_SEARCH,
+} InputMode;
+
+typedef struct {
+ const CommandSet *cmds;
+ IntMap key_bindings;
+} ModeHandler;
+
+typedef struct EditorState {
+ EditorStatus status;
+ InputMode input_mode;
+ CommandLine cmdline;
+ SearchState search;
+ GlobalOptions options;
+ Terminal terminal;
+ StringView home_dir;
+ const char *user_config_dir;
+ bool child_controls_terminal;
+ bool everything_changed;
+ bool cursor_style_changed;
+ bool session_leader;
+ size_t cmdline_x;
+ ModeHandler modes[3];
+ Clipboard clipboard;
+ TagFile tagfile;
+ HashMap aliases;
+ HashMap compilers;
+ HashMap syntaxes;
+ ColorScheme colors;
+ CommandMacroState macro;
+ TermCursorStyle cursor_styles[NR_CURSOR_MODES];
+ Frame *root_frame;
+ struct Window *window;
+ View *view;
+ Buffer *buffer;
+ PointerArray buffers;
+ PointerArray filetypes;
+ PointerArray file_options;
+ PointerArray bookmarks;
+ MessageArray messages;
+ FileHistory file_history;
+ History search_history;
+ History command_history;
+ RegexpWordBoundaryTokens regexp_word_tokens;
+ const char *version;
+} EditorState;
+
+static inline void mark_everything_changed(EditorState *e)
+{
+ e->everything_changed = true;
+}
+
+static inline void set_input_mode(EditorState *e, InputMode mode)
+{
+ e->cursor_style_changed = true;
+ e->input_mode = mode;
+}
+
+static inline CommandRunner cmdrunner_for_mode(EditorState *e, InputMode mode, bool allow_recording)
+{
+ BUG_ON(mode >= ARRAYLEN(e->modes));
+ CommandRunner runner = {
+ .cmds = e->modes[mode].cmds,
+ .lookup_alias = (mode == INPUT_NORMAL) ? find_normal_alias : NULL,
+ .home_dir = &e->home_dir,
+ .allow_recording = allow_recording,
+ .userdata = e,
+ };
+ return runner;
+}
+
+EditorState *init_editor_state(void) RETURNS_NONNULL;
+void free_editor_state(EditorState *e) NONNULL_ARGS;
+void any_key(Terminal *term, unsigned int esc_timeout) NONNULL_ARGS;
+int main_loop(EditorState *e) NONNULL_ARGS WARN_UNUSED_RESULT;
+void ui_start(EditorState *e) NONNULL_ARGS;
+void ui_end(EditorState *e) NONNULL_ARGS;
+void ui_resize(EditorState *e) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/encoding.c b/examples/dte/encoding.c
new file mode 100644
index 0000000..3fb87db
--- /dev/null
+++ b/examples/dte/encoding.c
@@ -0,0 +1,132 @@
+#include "encoding.h"
+#include "util/ascii.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/str-util.h"
+
+typedef struct {
+ const char alias[8];
+ EncodingType encoding;
+} EncodingAlias;
+
+static const char encoding_names[][16] = {
+ [UTF8] = "UTF-8",
+ [UTF16BE] = "UTF-16BE",
+ [UTF16LE] = "UTF-16LE",
+ [UTF32BE] = "UTF-32BE",
+ [UTF32LE] = "UTF-32LE",
+};
+
+static const EncodingAlias encoding_aliases[] = {
+ {"UCS-2", UTF16BE},
+ {"UCS-2BE", UTF16BE},
+ {"UCS-2LE", UTF16LE},
+ {"UCS-4", UTF32BE},
+ {"UCS-4BE", UTF32BE},
+ {"UCS-4LE", UTF32LE},
+ {"UCS2", UTF16BE},
+ {"UCS4", UTF32BE},
+ {"UTF-16", UTF16BE},
+ {"UTF-32", UTF32BE},
+ {"UTF16", UTF16BE},
+ {"UTF16BE", UTF16BE},
+ {"UTF16LE", UTF16LE},
+ {"UTF32", UTF32BE},
+ {"UTF32BE", UTF32BE},
+ {"UTF32LE", UTF32LE},
+ {"UTF8", UTF8},
+};
+
+static const ByteOrderMark boms[NR_ENCODING_TYPES] = {
+ [UTF8] = {{0xef, 0xbb, 0xbf}, 3},
+ [UTF16BE] = {{0xfe, 0xff}, 2},
+ [UTF16LE] = {{0xff, 0xfe}, 2},
+ [UTF32BE] = {{0x00, 0x00, 0xfe, 0xff}, 4},
+ [UTF32LE] = {{0xff, 0xfe, 0x00, 0x00}, 4},
+};
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(encoding_aliases, alias, ascii_strcmp_icase);
+}
+
+static int enc_alias_cmp(const void *key, const void *elem)
+{
+ const EncodingAlias *a = key;
+ const char *name = elem;
+ return ascii_strcmp_icase(a->alias, name);
+}
+
+EncodingType lookup_encoding(const char *name)
+{
+ static_assert(ARRAYLEN(encoding_names) == NR_ENCODING_TYPES - 1);
+ for (size_t i = 0; i < ARRAYLEN(encoding_names); i++) {
+ if (ascii_streq_icase(name, encoding_names[i])) {
+ return (EncodingType) i;
+ }
+ }
+
+ const EncodingAlias *a = BSEARCH(name, encoding_aliases, enc_alias_cmp);
+ return a ? a->encoding : UNKNOWN_ENCODING;
+}
+
+static const char *encoding_type_to_string(EncodingType type)
+{
+ if (type < NR_ENCODING_TYPES && type != UNKNOWN_ENCODING) {
+ return str_intern(encoding_names[type]);
+ }
+ return NULL;
+}
+
+Encoding encoding_from_name(const char *name)
+{
+ const EncodingType type = lookup_encoding(name);
+ const char *normalized_name;
+ if (type == UNKNOWN_ENCODING) {
+ char upper[256];
+ size_t n;
+ for (n = 0; n < sizeof(upper) && name[n]; n++) {
+ upper[n] = ascii_toupper(name[n]);
+ }
+ normalized_name = mem_intern(upper, n);
+ } else {
+ normalized_name = encoding_type_to_string(type);
+ }
+ return (Encoding) {
+ .type = type,
+ .name = normalized_name
+ };
+}
+
+Encoding encoding_from_type(EncodingType type)
+{
+ return (Encoding) {
+ .type = type,
+ .name = encoding_type_to_string(type)
+ };
+}
+
+EncodingType detect_encoding_from_bom(const unsigned char *buf, size_t size)
+{
+ // Skip exhaustive checks if there's clearly no BOM
+ if (size < 2 || ((unsigned int)buf[0]) - 1 < 0xEE) {
+ return UNKNOWN_ENCODING;
+ }
+
+ // Iterate array backwards to ensure UTF32LE is checked before UTF16LE
+ for (int i = NR_ENCODING_TYPES - 1; i >= 0; i--) {
+ const unsigned int bom_len = boms[i].len;
+ if (bom_len > 0 && size >= bom_len && mem_equal(buf, boms[i].bytes, bom_len)) {
+ return (EncodingType) i;
+ }
+ }
+ return UNKNOWN_ENCODING;
+}
+
+const ByteOrderMark *get_bom_for_encoding(EncodingType encoding)
+{
+ static_assert(ARRAYLEN(boms) == NR_ENCODING_TYPES);
+ BUG_ON(encoding >= ARRAYLEN(boms));
+ const ByteOrderMark *bom = &boms[encoding];
+ return bom->len ? bom : NULL;
+}
diff --git a/examples/dte/encoding.h b/examples/dte/encoding.h
new file mode 100644
index 0000000..bb4cf67
--- /dev/null
+++ b/examples/dte/encoding.h
@@ -0,0 +1,46 @@
+#ifndef ENCODING_ENCODING_H
+#define ENCODING_ENCODING_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+
+typedef enum {
+ UTF8,
+ UTF16BE,
+ UTF16LE,
+ UTF32BE,
+ UTF32LE,
+ UNKNOWN_ENCODING,
+ NR_ENCODING_TYPES,
+
+ // This value is used by the "open" command to instruct other
+ // routines that no specific encoding was requested and that
+ // it should be detected instead. It is always replaced by
+ // some other value by the time a file is successfully opened.
+ ENCODING_AUTODETECT
+} EncodingType;
+
+typedef struct {
+ EncodingType type;
+ // An interned encoding name compatible with iconv_open(3)
+ const char *name;
+} Encoding;
+
+typedef struct {
+ const unsigned char bytes[4];
+ unsigned int len;
+} ByteOrderMark;
+
+static inline bool same_encoding(const Encoding *a, const Encoding *b)
+{
+ return a->type == b->type && a->name == b->name;
+}
+
+Encoding encoding_from_type(EncodingType type);
+Encoding encoding_from_name(const char *name) NONNULL_ARGS;
+EncodingType lookup_encoding(const char *name) NONNULL_ARGS;
+EncodingType detect_encoding_from_bom(const unsigned char *buf, size_t size);
+const ByteOrderMark *get_bom_for_encoding(EncodingType encoding);
+
+#endif
diff --git a/examples/dte/error.c b/examples/dte/error.c
new file mode 100644
index 0000000..87831c9
--- /dev/null
+++ b/examples/dte/error.c
@@ -0,0 +1,95 @@
+#include <errno.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <string.h>
+#include "error.h"
+#include "command/run.h"
+#include "config.h"
+#include "util/log.h"
+#include "util/xstdio.h"
+
+static char error_buf[512];
+static unsigned int nr_errors;
+static bool msg_is_error;
+static bool print_errors_to_stderr;
+
+void clear_error(void)
+{
+ error_buf[0] = '\0';
+}
+
+bool error_msg(const char *format, ...)
+{
+ const char *cmd = current_command ? current_command->name : NULL;
+ const char *file = current_config.file;
+ const unsigned int line = current_config.line;
+ const size_t size = sizeof(error_buf);
+ int pos = 0;
+
+ if (file && cmd) {
+ pos = snprintf(error_buf, size, "%s:%u: %s: ", file, line, cmd);
+ } else if (file) {
+ pos = snprintf(error_buf, size, "%s:%u: ", file, line);
+ } else if (cmd) {
+ pos = snprintf(error_buf, size, "%s: ", cmd);
+ }
+
+ if (unlikely(pos < 0)) {
+ // Note: POSIX snprintf(3) *does* set errno on failure (unlike ISO C)
+ LOG_ERRNO("snprintf");
+ pos = 0;
+ }
+
+ if (likely(pos < (size - 3))) {
+ va_list ap;
+ va_start(ap, format);
+ vsnprintf(error_buf + pos, size - pos, format, ap);
+ va_end(ap);
+ } else {
+ LOG_WARNING("no buffer space left for error message");
+ }
+
+ msg_is_error = true;
+ nr_errors++;
+
+ if (print_errors_to_stderr) {
+ xfputs(error_buf, stderr);
+ xfputc('\n', stderr);
+ }
+
+ LOG_INFO("%s", error_buf);
+
+ // Always return false, to allow tail-calling as `return error_msg(...);`
+ // from command handlers, instead of `error_msg(...); return false;`
+ return false;
+}
+
+bool error_msg_errno(const char *prefix)
+{
+ return error_msg("%s: %s", prefix, strerror(errno));
+}
+
+void info_msg(const char *format, ...)
+{
+ va_list ap;
+ va_start(ap, format);
+ vsnprintf(error_buf, sizeof(error_buf), format, ap);
+ va_end(ap);
+ msg_is_error = false;
+}
+
+const char *get_msg(bool *is_error)
+{
+ *is_error = msg_is_error;
+ return error_buf;
+}
+
+unsigned int get_nr_errors(void)
+{
+ return nr_errors;
+}
+
+void set_print_errors_to_stderr(bool enable)
+{
+ print_errors_to_stderr = enable;
+}
diff --git a/examples/dte/error.h b/examples/dte/error.h
new file mode 100644
index 0000000..bf19414
--- /dev/null
+++ b/examples/dte/error.h
@@ -0,0 +1,15 @@
+#ifndef ERROR_H
+#define ERROR_H
+
+#include <stdbool.h>
+#include "util/macros.h"
+
+bool error_msg(const char *format, ...) COLD PRINTF(1);
+bool error_msg_errno(const char *prefix) COLD NONNULL_ARGS;
+void info_msg(const char *format, ...) PRINTF(1);
+void clear_error(void);
+const char *get_msg(bool *is_error) NONNULL_ARGS;
+unsigned int get_nr_errors(void);
+void set_print_errors_to_stderr(bool enable);
+
+#endif
diff --git a/examples/dte/exec.c b/examples/dte/exec.c
new file mode 100644
index 0000000..416a2ef
--- /dev/null
+++ b/examples/dte/exec.c
@@ -0,0 +1,366 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "exec.h"
+#include "block-iter.h"
+#include "buffer.h"
+#include "change.h"
+#include "command/macro.h"
+#include "commands.h"
+#include "ctags.h"
+#include "error.h"
+#include "misc.h"
+#include "move.h"
+#include "msg.h"
+#include "selection.h"
+#include "show.h"
+#include "tag.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/numtostr.h"
+#include "util/ptr-array.h"
+#include "util/str-util.h"
+#include "util/string-view.h"
+#include "util/string.h"
+#include "util/strtonum.h"
+#include "util/xsnprintf.h"
+#include "view.h"
+#include "window.h"
+
+enum {
+ IN = 1 << 0,
+ OUT = 1 << 1,
+ ERR = 1 << 2,
+ ALL = IN | OUT | ERR,
+};
+
+static const struct {
+ char name[8];
+ uint8_t flags;
+} exec_map[] = {
+ [EXEC_BUFFER] = {"buffer", IN | OUT},
+ [EXEC_COMMAND] = {"command", IN},
+ [EXEC_ERRMSG] = {"errmsg", ERR},
+ [EXEC_EVAL] = {"eval", OUT},
+ [EXEC_LINE] = {"line", IN},
+ [EXEC_MSG] = {"msg", IN | OUT},
+ [EXEC_NULL] = {"null", ALL},
+ [EXEC_OPEN] = {"open", OUT},
+ [EXEC_SEARCH] = {"search", IN},
+ [EXEC_TAG] = {"tag", OUT},
+ [EXEC_TTY] = {"tty", ALL},
+ [EXEC_WORD] = {"word", IN},
+};
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(exec_map, name, strcmp);
+}
+
+ExecAction lookup_exec_action(const char *name, int fd)
+{
+ BUG_ON(fd < 0 || fd > 2);
+ ssize_t i = BSEARCH_IDX(name, exec_map, vstrcmp);
+ return (i >= 0 && (exec_map[i].flags & 1u << fd)) ? i : EXEC_INVALID;
+}
+
+static void open_files_from_string(EditorState *e, const String *str)
+{
+ PointerArray filenames = PTR_ARRAY_INIT;
+ for (size_t pos = 0, size = str->len; pos < size; ) {
+ char *filename = buf_next_line(str->buffer, &pos, size);
+ if (filename[0] != '\0') {
+ ptr_array_append(&filenames, filename);
+ }
+ }
+
+ if (filenames.count == 0) {
+ return;
+ }
+
+ ptr_array_append(&filenames, NULL);
+ window_open_files(e->window, (char**)filenames.ptrs, NULL);
+
+ // TODO: re-enable this when the todo in allow_macro_recording() is done
+ // macro_command_hook(&e->macro, "open", (char**)filenames.ptrs);
+
+ ptr_array_free_array(&filenames);
+}
+
+static void parse_and_activate_message(EditorState *e, const String *str)
+{
+ MessageArray *msgs = &e->messages;
+ size_t count = msgs->array.count;
+ size_t x;
+ if (!count || !buf_parse_size(str->buffer, str->len, &x) || !x) {
+ return;
+ }
+ msgs->pos = MIN(x - 1, count - 1);
+ activate_current_message(e);
+}
+
+static void parse_and_goto_tag(EditorState *e, const String *str)
+{
+ if (unlikely(str->len == 0)) {
+ error_msg("child produced no output");
+ return;
+ }
+
+ Tag tag;
+ size_t pos = 0;
+ StringView line = buf_slice_next_line(str->buffer, &pos, str->len);
+ if (pos == 0) {
+ return;
+ }
+
+ if (!parse_ctags_line(&tag, line.data, line.length)) {
+ // Treat line as simple tag name
+ tag_lookup(&e->tagfile, &line, e->buffer->abs_filename, &e->messages);
+ goto activate;
+ }
+
+ char buf[8192];
+ const char *cwd = getcwd(buf, sizeof buf);
+ if (unlikely(!cwd)) {
+ error_msg_errno("getcwd() failed");
+ return;
+ }
+
+ StringView dir = strview_from_cstring(cwd);
+ clear_messages(&e->messages);
+ add_message_for_tag(&e->messages, &tag, &dir);
+
+activate:
+ activate_current_message_save(e);
+}
+
+static const char **lines_and_columns_env(const Window *window)
+{
+ static char lines[DECIMAL_STR_MAX(window->edit_h)];
+ static char columns[DECIMAL_STR_MAX(window->edit_w)];
+ static const char *vars[] = {
+ "LINES", lines,
+ "COLUMNS", columns,
+ NULL,
+ };
+
+ buf_uint_to_str(window->edit_h, lines);
+ buf_uint_to_str(window->edit_w, columns);
+ return vars;
+}
+
+static void show_spawn_error_msg(const String *errstr, int err)
+{
+ if (err <= 0) {
+ return;
+ }
+
+ char msg[512];
+ msg[0] = '\0';
+ if (errstr->len) {
+ size_t pos = 0;
+ StringView line = buf_slice_next_line(errstr->buffer, &pos, errstr->len);
+ BUG_ON(pos == 0);
+ size_t len = MIN(line.length, sizeof(msg) - 8);
+ xsnprintf(msg, sizeof(msg), ": \"%.*s\"", (int)len, line.data);
+ }
+
+ if (err >= 256) {
+ int sig = err >> 8;
+ const char *str = strsignal(sig);
+ error_msg("Child received signal %d (%s)%s", sig, str ? str : "??", msg);
+ } else if (err) {
+ error_msg("Child returned %d%s", err, msg);
+ }
+}
+
+static SpawnAction spawn_action_from_exec_action(ExecAction action)
+{
+ BUG_ON(action == EXEC_INVALID);
+ if (action == EXEC_NULL) {
+ return SPAWN_NULL;
+ } else if (action == EXEC_TTY) {
+ return SPAWN_TTY;
+ } else {
+ return SPAWN_PIPE;
+ }
+}
+
+ssize_t handle_exec (
+ EditorState *e,
+ const char **argv,
+ ExecAction actions[3],
+ SpawnFlags spawn_flags,
+ bool strip_trailing_newline
+) {
+ View *view = e->view;
+ const BlockIter saved_cursor = view->cursor;
+ const ssize_t saved_sel_so = view->sel_so;
+ const ssize_t saved_sel_eo = view->sel_eo;
+ char *alloc = NULL;
+ bool output_to_buffer = (actions[STDOUT_FILENO] == EXEC_BUFFER);
+ bool replace_input = false;
+
+ SpawnContext ctx = {
+ .editor = e,
+ .argv = argv,
+ .outputs = {STRING_INIT, STRING_INIT},
+ .flags = spawn_flags,
+ .env = output_to_buffer ? lines_and_columns_env(e->window) : NULL,
+ .actions = {
+ spawn_action_from_exec_action(actions[0]),
+ spawn_action_from_exec_action(actions[1]),
+ spawn_action_from_exec_action(actions[2]),
+ },
+ };
+
+ switch (actions[STDIN_FILENO]) {
+ case EXEC_LINE:
+ if (view->selection) {
+ ctx.input.length = prepare_selection(view);
+ } else {
+ StringView line;
+ move_bol(view);
+ fill_line_ref(&view->cursor, &line);
+ ctx.input.length = line.length;
+ }
+ replace_input = true;
+ get_bytes:
+ alloc = block_iter_get_bytes(&view->cursor, ctx.input.length);
+ ctx.input.data = alloc;
+ break;
+ case EXEC_BUFFER:
+ if (view->selection) {
+ ctx.input.length = prepare_selection(view);
+ } else {
+ Block *blk;
+ block_for_each(blk, &view->buffer->blocks) {
+ ctx.input.length += blk->size;
+ }
+ move_bof(view);
+ }
+ replace_input = true;
+ goto get_bytes;
+ case EXEC_WORD:
+ if (view->selection) {
+ ctx.input.length = prepare_selection(view);
+ replace_input = true;
+ } else {
+ size_t offset;
+ StringView word = view_do_get_word_under_cursor(e->view, &offset);
+ if (word.length == 0) {
+ break;
+ }
+ // TODO: optimize this, so that the BlockIter moves by just the
+ // minimal word offset instead of iterating to a line offset
+ ctx.input.length = word.length;
+ move_bol(view);
+ view->cursor.offset += offset;
+ BUG_ON(view->cursor.offset >= view->cursor.blk->size);
+ }
+ goto get_bytes;
+ case EXEC_MSG: {
+ String messages = dump_messages(&e->messages);
+ ctx.input = strview_from_string(&messages),
+ alloc = messages.buffer;
+ break;
+ }
+ case EXEC_COMMAND: {
+ String hist = dump_command_history(e);
+ ctx.input = strview_from_string(&hist),
+ alloc = hist.buffer;
+ break;
+ }
+ case EXEC_SEARCH: {
+ String hist = dump_search_history(e);
+ ctx.input = strview_from_string(&hist),
+ alloc = hist.buffer;
+ break;
+ }
+ case EXEC_NULL:
+ case EXEC_TTY:
+ break;
+ // These can't be used as input actions and should be prevented by
+ // the validity checks in cmd_exec():
+ case EXEC_OPEN:
+ case EXEC_TAG:
+ case EXEC_EVAL:
+ case EXEC_ERRMSG:
+ case EXEC_INVALID:
+ default:
+ BUG("unhandled action");
+ return -1;
+ }
+
+ int err = spawn(&ctx);
+ free(alloc);
+ if (err != 0) {
+ show_spawn_error_msg(&ctx.outputs[1], err);
+ string_free(&ctx.outputs[0]);
+ string_free(&ctx.outputs[1]);
+ view->cursor = saved_cursor;
+ return -1;
+ }
+
+ string_free(&ctx.outputs[1]);
+ String *output = &ctx.outputs[0];
+ if (
+ strip_trailing_newline
+ && output_to_buffer
+ && output->len > 0
+ && output->buffer[output->len - 1] == '\n'
+ ) {
+ output->len--;
+ if (output->len > 0 && output->buffer[output->len - 1] == '\r') {
+ output->len--;
+ }
+ }
+
+ if (!output_to_buffer) {
+ view->cursor = saved_cursor;
+ view->sel_so = saved_sel_so;
+ view->sel_eo = saved_sel_eo;
+ mark_all_lines_changed(view->buffer);
+ }
+
+ switch (actions[STDOUT_FILENO]) {
+ case EXEC_BUFFER:
+ if (replace_input || view->selection) {
+ size_t del_count = replace_input ? ctx.input.length : prepare_selection(view);
+ buffer_replace_bytes(view, del_count, output->buffer, output->len);
+ unselect(view);
+ } else {
+ buffer_insert_bytes(view, output->buffer, output->len);
+ }
+ break;
+ case EXEC_MSG:
+ parse_and_activate_message(e, output);
+ break;
+ case EXEC_OPEN:
+ open_files_from_string(e, output);
+ break;
+ case EXEC_TAG:
+ parse_and_goto_tag(e, output);
+ break;
+ case EXEC_EVAL:
+ exec_normal_config(e, strview_from_string(output));
+ break;
+ case EXEC_NULL:
+ case EXEC_TTY:
+ break;
+ // These can't be used as output actions
+ case EXEC_COMMAND:
+ case EXEC_ERRMSG:
+ case EXEC_LINE:
+ case EXEC_SEARCH:
+ case EXEC_WORD:
+ case EXEC_INVALID:
+ default:
+ BUG("unhandled action");
+ return -1;
+ }
+
+ size_t output_len = output->len;
+ string_free(output);
+ return output_len;
+}
diff --git a/examples/dte/exec.h b/examples/dte/exec.h
new file mode 100644
index 0000000..e40a11f
--- /dev/null
+++ b/examples/dte/exec.h
@@ -0,0 +1,37 @@
+#ifndef EXEC_H
+#define EXEC_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include "editor.h"
+#include "spawn.h"
+#include "util/macros.h"
+
+typedef enum {
+ EXEC_INVALID = -1,
+ // Note: items below here need to be kept sorted
+ EXEC_BUFFER = 0,
+ EXEC_COMMAND,
+ EXEC_ERRMSG,
+ EXEC_EVAL,
+ EXEC_LINE,
+ EXEC_MSG,
+ EXEC_NULL,
+ EXEC_OPEN,
+ EXEC_SEARCH,
+ EXEC_TAG,
+ EXEC_TTY,
+ EXEC_WORD,
+} ExecAction;
+
+ssize_t handle_exec (
+ EditorState *e,
+ const char **argv,
+ ExecAction actions[3],
+ SpawnFlags spawn_flags,
+ bool strip_trailing_newline
+) NONNULL_ARGS;
+
+ExecAction lookup_exec_action(const char *name, int fd) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/file-history.c b/examples/dte/file-history.c
new file mode 100644
index 0000000..8066b94
--- /dev/null
+++ b/examples/dte/file-history.c
@@ -0,0 +1,153 @@
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include "file-history.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/readfile.h"
+#include "util/str-util.h"
+#include "util/string-view.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+#include "util/xstdio.h"
+
+enum {
+ MAX_ENTRIES = 512
+};
+
+void file_history_add(FileHistory *history, unsigned long row, unsigned long col, const char *filename)
+{
+ BUG_ON(row == 0);
+ BUG_ON(col == 0);
+ HashMap *map = &history->entries;
+ FileHistoryEntry *e = hashmap_get(map, filename);
+
+ if (e) {
+ if (e == history->last) {
+ e->row = row;
+ e->col = col;
+ return;
+ }
+ e->next->prev = e->prev;
+ if (unlikely(e == history->first)) {
+ history->first = e->next;
+ } else {
+ e->prev->next = e->next;
+ }
+ } else {
+ if (map->count == MAX_ENTRIES) {
+ // History is full; recycle the oldest entry
+ FileHistoryEntry *old_first = history->first;
+ FileHistoryEntry *new_first = old_first->next;
+ new_first->prev = NULL;
+ history->first = new_first;
+ e = hashmap_remove(map, old_first->filename);
+ BUG_ON(e != old_first);
+ } else {
+ e = xnew(FileHistoryEntry, 1);
+ }
+ e->filename = xstrdup(filename);
+ hashmap_insert(map, e->filename, e);
+ }
+
+ // Insert the entry at the end of the list
+ FileHistoryEntry *old_last = history->last;
+ e->next = NULL;
+ e->prev = old_last;
+ e->row = row;
+ e->col = col;
+ history->last = e;
+ if (likely(old_last)) {
+ old_last->next = e;
+ } else {
+ history->first = e;
+ }
+}
+
+static bool parse_ulong_field(StringView *sv, unsigned long *valp)
+{
+ size_t n = buf_parse_ulong(sv->data, sv->length, valp);
+ if (n == 0 || *valp == 0 || sv->data[n] != ' ') {
+ return false;
+ }
+ strview_remove_prefix(sv, n + 1);
+ return true;
+}
+
+void file_history_load(FileHistory *history, char *filename)
+{
+ BUG_ON(!history);
+ BUG_ON(!filename);
+ BUG_ON(history->filename);
+
+ hashmap_init(&history->entries, MAX_ENTRIES);
+ history->filename = filename;
+
+ char *buf;
+ const ssize_t ssize = read_file(filename, &buf);
+ if (ssize < 0) {
+ if (errno != ENOENT) {
+ error_msg("Error reading %s: %s", filename, strerror(errno));
+ }
+ return;
+ }
+
+ for (size_t pos = 0, size = ssize; pos < size; ) {
+ unsigned long row, col;
+ StringView line = buf_slice_next_line(buf, &pos, size);
+ if (unlikely(
+ !parse_ulong_field(&line, &row)
+ || !parse_ulong_field(&line, &col)
+ || line.length < 2
+ || line.data[0] != '/'
+ || buf[pos - 1] != '\n'
+ )) {
+ continue;
+ }
+ buf[pos - 1] = '\0'; // null-terminate line, by replacing '\n' with '\0'
+ file_history_add(history, row, col, line.data);
+ }
+
+ free(buf);
+}
+
+void file_history_save(const FileHistory *history)
+{
+ const char *filename = history->filename;
+ if (!filename) {
+ return;
+ }
+
+ FILE *f = xfopen(filename, "w", O_CLOEXEC, 0666);
+ if (!f) {
+ error_msg("Error creating %s: %s", filename, strerror(errno));
+ return;
+ }
+
+ for (const FileHistoryEntry *e = history->first; e; e = e->next) {
+ xfprintf(f, "%lu %lu %s\n", e->row, e->col, e->filename);
+ }
+
+ fclose(f);
+}
+
+bool file_history_find(const FileHistory *history, const char *filename, unsigned long *row, unsigned long *col)
+{
+ const FileHistoryEntry *e = hashmap_get(&history->entries, filename);
+ if (!e) {
+ return false;
+ }
+ *row = e->row;
+ *col = e->col;
+ return true;
+}
+
+void file_history_free(FileHistory *history)
+{
+ hashmap_free(&history->entries, free);
+ free(history->filename);
+ history->filename = NULL;
+ history->first = NULL;
+ history->last = NULL;
+}
diff --git a/examples/dte/file-history.h b/examples/dte/file-history.h
new file mode 100644
index 0000000..0a51891
--- /dev/null
+++ b/examples/dte/file-history.h
@@ -0,0 +1,29 @@
+#ifndef FILE_HISTORY_H
+#define FILE_HISTORY_H
+
+#include <stdbool.h>
+#include "util/hashmap.h"
+#include "util/macros.h"
+
+typedef struct FileHistoryEntry {
+ struct FileHistoryEntry *next;
+ struct FileHistoryEntry *prev;
+ char *filename;
+ unsigned long row;
+ unsigned long col;
+} FileHistoryEntry;
+
+typedef struct {
+ char *filename;
+ HashMap entries;
+ FileHistoryEntry *first;
+ FileHistoryEntry *last;
+} FileHistory;
+
+void file_history_add(FileHistory *hist, unsigned long row, unsigned long col, const char *filename);
+void file_history_load(FileHistory *hist, char *filename);
+void file_history_save(const FileHistory *hist);
+bool file_history_find(const FileHistory *hist, const char *filename, unsigned long *row, unsigned long *col) WARN_UNUSED_RESULT;
+void file_history_free(FileHistory *history);
+
+#endif
diff --git a/examples/dte/file-option.c b/examples/dte/file-option.c
new file mode 100644
index 0000000..df6af3d
--- /dev/null
+++ b/examples/dte/file-option.c
@@ -0,0 +1,193 @@
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "file-option.h"
+#include "command/serialize.h"
+#include "editor.h"
+#include "editorconfig/editorconfig.h"
+#include "error.h"
+#include "options.h"
+#include "regexp.h"
+#include "util/debug.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+
+typedef struct {
+ FileOptionType type;
+ char **strs;
+ union {
+ char *filetype;
+ CachedRegexp *filename;
+ } u;
+} FileOption;
+
+static void set_options(EditorState *e, char **args)
+{
+ for (size_t i = 0; args[i]; i += 2) {
+ set_option(e, args[i], args[i + 1], true, false);
+ }
+}
+
+void set_editorconfig_options(Buffer *buffer)
+{
+ LocalOptions *options = &buffer->options;
+ if (!options->editorconfig) {
+ return;
+ }
+
+ const char *path = buffer->abs_filename;
+ char cwd[8192];
+ if (!path) {
+ // For buffers with no associated filename, use a dummy path of
+ // "$PWD/__", to obtain generic settings for the working directory
+ // or the user's default settings
+ static const char suffix[] = "/__";
+ if (unlikely(!getcwd(cwd, sizeof(cwd) - sizeof(suffix)))) {
+ return;
+ }
+ memcpy(cwd + strlen(cwd), suffix, sizeof(suffix));
+ path = cwd;
+ }
+
+ EditorConfigOptions opts;
+ if (get_editorconfig_options(path, &opts) != 0) {
+ return;
+ }
+
+ switch (opts.indent_style) {
+ case INDENT_STYLE_SPACE:
+ options->expand_tab = true;
+ options->emulate_tab = true;
+ options->detect_indent = 0;
+ break;
+ case INDENT_STYLE_TAB:
+ options->expand_tab = false;
+ options->emulate_tab = false;
+ options->detect_indent = 0;
+ break;
+ case INDENT_STYLE_UNSPECIFIED:
+ break;
+ }
+
+ const unsigned int indent_size = opts.indent_size;
+ if (indent_size > 0 && indent_size <= INDENT_WIDTH_MAX) {
+ options->indent_width = indent_size;
+ options->detect_indent = 0;
+ }
+
+ const unsigned int tab_width = opts.tab_width;
+ if (tab_width > 0 && tab_width <= TAB_WIDTH_MAX) {
+ options->tab_width = tab_width;
+ }
+
+ const unsigned int max_line_length = opts.max_line_length;
+ if (max_line_length > 0 && max_line_length <= TEXT_WIDTH_MAX) {
+ options->text_width = max_line_length;
+ }
+}
+
+void set_file_options(EditorState *e, Buffer *buffer)
+{
+ for (size_t i = 0, n = e->file_options.count; i < n; i++) {
+ const FileOption *opt = e->file_options.ptrs[i];
+ if (opt->type == FOPTS_FILETYPE) {
+ if (streq(opt->u.filetype, buffer->options.filetype)) {
+ set_options(e, opt->strs);
+ }
+ continue;
+ }
+
+ BUG_ON(opt->type != FOPTS_FILENAME);
+ const char *filename = buffer->abs_filename;
+ if (!filename) {
+ continue;
+ }
+
+ const regex_t *re = &opt->u.filename->re;
+ regmatch_t m;
+ if (regexp_exec(re, filename, strlen(filename), 0, &m, 0)) {
+ set_options(e, opt->strs);
+ }
+ }
+}
+
+bool add_file_options(PointerArray *file_options, FileOptionType type, StringView str, char **strs, size_t nstrs)
+{
+ size_t len = str.length;
+ if (unlikely(len == 0)) {
+ const char *desc = (type == FOPTS_FILETYPE) ? "filetype" : "pattern";
+ return error_msg("can't add option with empty %s", desc);
+ }
+
+ FileOption *opt = xnew(FileOption, 1);
+ if (type == FOPTS_FILETYPE) {
+ opt->u.filetype = xstrcut(str.data, len);
+ goto append;
+ }
+
+ BUG_ON(type != FOPTS_FILENAME);
+ CachedRegexp *r = xmalloc(sizeof(*r) + len + 1);
+ memcpy(r->str, str.data, len);
+ r->str[len] = '\0';
+ opt->u.filename = r;
+
+ int err = regcomp(&r->re, r->str, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB);
+ if (unlikely(err)) {
+ regexp_error_msg(&r->re, r->str, err);
+ free(r);
+ free(opt);
+ return false;
+ }
+
+append:
+ opt->type = type;
+ opt->strs = copy_string_array(strs, nstrs);
+ ptr_array_append(file_options, opt);
+ return true;
+}
+
+void dump_file_options(const PointerArray *file_options, String *buf)
+{
+ for (size_t i = 0, n = file_options->count; i < n; i++) {
+ const FileOption *opt = file_options->ptrs[i];
+ const char *tp;
+ if (opt->type == FOPTS_FILENAME) {
+ tp = opt->u.filename->str;
+ } else {
+ tp = opt->u.filetype;
+ }
+ char **strs = opt->strs;
+ string_append_literal(buf, "option ");
+ if (opt->type == FOPTS_FILENAME) {
+ string_append_literal(buf, "-r ");
+ }
+ if (str_has_prefix(tp, "-") || string_array_contains_prefix(strs, "-")) {
+ string_append_literal(buf, "-- ");
+ }
+ string_append_escaped_arg(buf, tp, true);
+ for (size_t j = 0; strs[j]; j += 2) {
+ string_append_byte(buf, ' ');
+ string_append_cstring(buf, strs[j]);
+ string_append_byte(buf, ' ');
+ string_append_escaped_arg(buf, strs[j + 1], true);
+ }
+ string_append_byte(buf, '\n');
+ }
+}
+
+static void free_file_option(FileOption *opt)
+{
+ if (opt->type == FOPTS_FILENAME) {
+ free_cached_regexp(opt->u.filename);
+ } else {
+ BUG_ON(opt->type != FOPTS_FILETYPE);
+ free(opt->u.filetype);
+ }
+ free_string_array(opt->strs);
+ free(opt);
+}
+
+void free_file_options(PointerArray *file_options)
+{
+ ptr_array_free_cb(file_options, FREE_FUNC(free_file_option));
+}
diff --git a/examples/dte/file-option.h b/examples/dte/file-option.h
new file mode 100644
index 0000000..ab1e930
--- /dev/null
+++ b/examples/dte/file-option.h
@@ -0,0 +1,32 @@
+#ifndef FILE_OPTION_H
+#define FILE_OPTION_H
+
+#include <stdbool.h>
+#include "buffer.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "util/string.h"
+
+typedef enum {
+ FOPTS_FILENAME,
+ FOPTS_FILETYPE,
+} FileOptionType;
+
+struct EditorState;
+
+void set_file_options(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
+void set_editorconfig_options(Buffer *buffer) NONNULL_ARGS;
+void dump_file_options(const PointerArray *file_options, String *buf);
+void free_file_options(PointerArray *file_options);
+
+NONNULL_ARGS WARN_UNUSED_RESULT
+bool add_file_options (
+ PointerArray *file_options,
+ FileOptionType type,
+ StringView str,
+ char **strs,
+ size_t nstrs
+);
+
+#endif
diff --git a/examples/dte/filetype.c b/examples/dte/filetype.c
new file mode 100644
index 0000000..5a40f7c
--- /dev/null
+++ b/examples/dte/filetype.c
@@ -0,0 +1,333 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include "filetype.h"
+#include "command/serialize.h"
+#include "regexp.h"
+#include "util/array.h"
+#include "util/ascii.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+
+static int ft_compare(const void *key, const void *elem)
+{
+ const StringView *sv = key;
+ const char *ext = elem; // Cast to first member of struct
+ int res = memcmp(sv->data, ext, sv->length);
+ if (unlikely(res == 0 && ext[sv->length] != '\0')) {
+ res = -1;
+ }
+ return res;
+}
+
+// Built-in filetypes
+#include "filetype/names.c"
+#include "filetype/basenames.c"
+#include "filetype/directories.c"
+#include "filetype/extensions.c"
+#include "filetype/interpreters.c"
+#include "filetype/ignored-exts.c"
+#include "filetype/signatures.c"
+
+UNITTEST {
+ static_assert(NR_BUILTIN_FILETYPES < 256);
+ CHECK_BSEARCH_ARRAY(basenames, name, strcmp);
+ CHECK_BSEARCH_ARRAY(extensions, ext, strcmp);
+ CHECK_BSEARCH_ARRAY(interpreters, key, strcmp);
+ CHECK_BSEARCH_STR_ARRAY(ignored_extensions, strcmp);
+ CHECK_BSEARCH_STR_ARRAY(builtin_filetype_names, strcmp);
+
+ for (size_t i = 0; i < ARRAYLEN(builtin_filetype_names); i++) {
+ const char *name = builtin_filetype_names[i];
+ if (unlikely(!is_valid_filetype_name(name))) {
+ BUG("invalid name at builtin_filetype_names[%zu]: \"%s\"", i, name);
+ }
+ }
+}
+
+typedef struct {
+ unsigned int str_len;
+ char str[];
+} FlexArrayStr;
+
+// Filetypes dynamically added via the `ft` command.
+// Not grouped by name to make it possible to order them freely.
+typedef struct {
+ union {
+ FlexArrayStr *str;
+ CachedRegexp *regexp;
+ } u;
+ uint8_t type; // FileDetectionType
+ char name[];
+} UserFileTypeEntry;
+
+static bool ft_uses_regex(FileDetectionType type)
+{
+ return type == FT_CONTENT || type == FT_FILENAME;
+}
+
+bool add_filetype(PointerArray *filetypes, const char *name, const char *str, FileDetectionType type)
+{
+ BUG_ON(!is_valid_filetype_name(name));
+ regex_t re;
+ bool use_re = ft_uses_regex(type);
+ if (use_re) {
+ int err = regcomp(&re, str, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB);
+ if (unlikely(err)) {
+ return regexp_error_msg(&re, str, err);
+ }
+ }
+
+ size_t name_len = strlen(name);
+ size_t str_len = strlen(str);
+ UserFileTypeEntry *ft = xmalloc(sizeof(*ft) + name_len + 1);
+ ft->type = type;
+
+ char *str_dest;
+ if (use_re) {
+ CachedRegexp *r = xmalloc(sizeof(*r) + str_len + 1);
+ r->re = re;
+ ft->u.regexp = r;
+ str_dest = r->str;
+ } else {
+ FlexArrayStr *s = xmalloc(sizeof(*s) + str_len + 1);
+ s->str_len = str_len;
+ ft->u.str = s;
+ str_dest = s->str;
+ }
+
+ memcpy(ft->name, name, name_len + 1);
+ memcpy(str_dest, str, str_len + 1);
+ ptr_array_append(filetypes, ft);
+ return true;
+}
+
+static StringView path_extension(StringView filename)
+{
+ StringView ext = STRING_VIEW_INIT;
+ ext.data = strview_memrchr(&filename, '.');
+ if (!ext.data || ext.data == filename.data) {
+ return ext;
+ }
+ ext.data++;
+ ext.length = filename.length - (ext.data - filename.data);
+ return ext;
+}
+
+static StringView get_filename_extension(StringView filename)
+{
+ StringView ext = path_extension(filename);
+ if (is_ignored_extension(ext)) {
+ filename.length -= ext.length + 1;
+ ext = path_extension(filename);
+ }
+ if (strview_has_suffix(&ext, "~")) {
+ ext.length--;
+ }
+ return ext;
+}
+
+// Parse hashbang and return interpreter name, without version number.
+// For example, if line is "#!/usr/bin/env python2", "python" is returned.
+static StringView get_interpreter(StringView line)
+{
+ StringView sv = STRING_VIEW_INIT;
+ if (!strview_has_prefix(&line, "#!")) {
+ return sv;
+ }
+
+ strview_remove_prefix(&line, 2);
+ strview_trim_left(&line);
+ if (line.length < 2 || line.data[0] != '/') {
+ return sv;
+ }
+
+ size_t pos = 0;
+ sv = get_delim(line.data, &pos, line.length, ' ');
+ if (pos < line.length && strview_equal_cstring(&sv, "/usr/bin/env")) {
+ while (pos + 1 < line.length && line.data[pos] == ' ') {
+ pos++;
+ }
+ sv = get_delim(line.data, &pos, line.length, ' ');
+ }
+
+ ssize_t last_slash_idx = strview_memrchr_idx(&sv, '/');
+ if (last_slash_idx >= 0) {
+ strview_remove_prefix(&sv, last_slash_idx + 1);
+ }
+
+ while (sv.length && ascii_is_digit_or_dot(sv.data[sv.length - 1])) {
+ sv.length--;
+ }
+
+ return sv;
+}
+
+static bool ft_str_match(const UserFileTypeEntry *ft, const StringView sv)
+{
+ const char *str = ft->u.str->str;
+ const size_t len = ft->u.str->str_len;
+ return sv.length > 0 && strview_equal_strn(&sv, str, len);
+}
+
+static bool ft_regex_match(const UserFileTypeEntry *ft, const StringView sv)
+{
+ const regex_t *re = &ft->u.regexp->re;
+ regmatch_t m;
+ return sv.length > 0 && regexp_exec(re, sv.data, sv.length, 0, &m, 0);
+}
+
+static bool ft_match(const UserFileTypeEntry *ft, const StringView sv)
+{
+ if (ft_uses_regex(ft->type)) {
+ return ft_regex_match(ft, sv);
+ }
+ return ft_str_match(ft, sv);
+}
+
+const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line)
+{
+ const char *b = filename ? path_basename(filename) : NULL;
+ const StringView base = strview_from_cstring(b);
+ const StringView ext = get_filename_extension(base);
+ const StringView path = strview_from_cstring(filename);
+ const StringView interpreter = get_interpreter(line);
+ BUG_ON(path.length == 0 && (base.length != 0 || ext.length != 0));
+ BUG_ON(line.length == 0 && interpreter.length != 0);
+
+ // The order of elements in this array determines the order of
+ // precedence for the lookup() functions (but note that changing
+ // the initializer below makes no difference to the array order)
+ const struct {
+ StringView sv;
+ FileTypeEnum (*lookup)(const StringView sv);
+ } table[] = {
+ [FT_INTERPRETER] = {interpreter, filetype_from_interpreter},
+ [FT_BASENAME] = {base, filetype_from_basename},
+ [FT_CONTENT] = {line, filetype_from_signature},
+ [FT_EXTENSION] = {ext, filetype_from_extension},
+ [FT_FILENAME] = {path, filetype_from_dir_prefix},
+ };
+
+ // Search user `ft` entries
+ for (size_t i = 0, n = filetypes->count; i < n; i++) {
+ const UserFileTypeEntry *ft = filetypes->ptrs[i];
+ if (ft_match(ft, table[ft->type].sv)) {
+ return ft->name;
+ }
+ }
+
+ // Search built-in lookup tables
+ for (FileDetectionType i = 0; i < ARRAYLEN(table); i++) {
+ BUG_ON(!table[i].lookup);
+ FileTypeEnum ft = table[i].lookup(table[i].sv);
+ if (ft != NONE) {
+ return builtin_filetype_names[ft];
+ }
+ }
+
+ // Use "ini" filetype if first line looks like an ini [section]
+ strview_trim_right(&line);
+ if (line.length >= 4) {
+ const char *s = line.data;
+ const size_t n = line.length;
+ if (s[0] == '[' && s[n - 1] == ']' && is_word_byte(s[1])) {
+ if (!strview_contains_char_type(&line, ASCII_CNTRL)) {
+ return builtin_filetype_names[INI];
+ }
+ }
+ }
+
+ if (strview_equal_cstring(&ext, "conf")) {
+ if (strview_has_prefix(&path, "/etc/systemd/")) {
+ return builtin_filetype_names[INI];
+ }
+ BUG_ON(!filename);
+ const StringView dir = path_slice_dirname(filename);
+ if (
+ strview_has_prefix(&path, "/etc/")
+ || strview_has_prefix(&path, "/usr/share/")
+ || strview_has_prefix(&path, "/usr/local/share/")
+ || strview_has_suffix(&dir, "/tmpfiles.d")
+ ) {
+ return builtin_filetype_names[CONFIG];
+ }
+ }
+
+ return NULL;
+}
+
+bool is_ft(const PointerArray *filetypes, const char *name)
+{
+ if (BSEARCH(name, builtin_filetype_names, vstrcmp)) {
+ return true;
+ }
+
+ for (size_t i = 0, n = filetypes->count; i < n; i++) {
+ const UserFileTypeEntry *ft = filetypes->ptrs[i];
+ if (streq(ft->name, name)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix)
+{
+ COLLECT_STRINGS(builtin_filetype_names, a, prefix);
+ for (size_t i = 0, n = filetypes->count; i < n; i++) {
+ const UserFileTypeEntry *ft = filetypes->ptrs[i];
+ const char *name = ft->name;
+ if (str_has_prefix(name, prefix)) {
+ ptr_array_append(a, xstrdup(name));
+ }
+ }
+}
+
+static const char *ft_get_str(const UserFileTypeEntry *ft)
+{
+ return ft_uses_regex(ft->type) ? ft->u.regexp->str : ft->u.str->str;
+}
+
+String dump_filetypes(const PointerArray *filetypes)
+{
+ static const char flags[][4] = {
+ [FT_EXTENSION] = "",
+ [FT_FILENAME] = "-f ",
+ [FT_CONTENT] = "-c ",
+ [FT_INTERPRETER] = "-i ",
+ [FT_BASENAME] = "-b ",
+ };
+
+ String s = string_new(4096);
+ for (size_t i = 0, n = filetypes->count; i < n; i++) {
+ const UserFileTypeEntry *ft = filetypes->ptrs[i];
+ BUG_ON(ft->type >= ARRAYLEN(flags));
+ BUG_ON(ft->name[0] == '-');
+ string_append_literal(&s, "ft ");
+ string_append_cstring(&s, flags[ft->type]);
+ string_append_escaped_arg(&s, ft->name, true);
+ string_append_byte(&s, ' ');
+ string_append_escaped_arg(&s, ft_get_str(ft), true);
+ string_append_byte(&s, '\n');
+ }
+ return s;
+}
+
+static void free_filetype_entry(UserFileTypeEntry *ft)
+{
+ if (ft_uses_regex(ft->type)) {
+ free_cached_regexp(ft->u.regexp);
+ } else {
+ free(ft->u.str);
+ }
+ free(ft);
+}
+
+void free_filetypes(PointerArray *filetypes)
+{
+ ptr_array_free_cb(filetypes, FREE_FUNC(free_filetype_entry));
+}
diff --git a/examples/dte/filetype.h b/examples/dte/filetype.h
new file mode 100644
index 0000000..cec4d85
--- /dev/null
+++ b/examples/dte/filetype.h
@@ -0,0 +1,35 @@
+#ifndef FILETYPE_H
+#define FILETYPE_H
+
+#include <stdbool.h>
+#include <string.h>
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+#include "util/string.h"
+
+// Note: the order of these values changes the order of iteration
+// in find_ft()
+typedef enum {
+ FT_INTERPRETER,
+ FT_BASENAME,
+ FT_CONTENT,
+ FT_EXTENSION,
+ FT_FILENAME,
+} FileDetectionType;
+
+PURE
+static inline bool is_valid_filetype_name(const char *name)
+{
+ size_t n = strcspn(name, " \t/");
+ return n > 0 && n < 64 && name[n] == '\0' && name[0] != '-';
+}
+
+bool add_filetype(PointerArray *filetypes, const char *name, const char *str, FileDetectionType type) NONNULL_ARGS WARN_UNUSED_RESULT;
+bool is_ft(const PointerArray *filetypes, const char *name);
+const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line);
+void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix);
+String dump_filetypes(const PointerArray *filetypes);
+void free_filetypes(PointerArray *filetypes);
+
+#endif
diff --git a/examples/dte/frame.c b/examples/dte/frame.c
new file mode 100644
index 0000000..c06b2d8
--- /dev/null
+++ b/examples/dte/frame.c
@@ -0,0 +1,496 @@
+#include "frame.h"
+#include "editor.h"
+#include "util/debug.h"
+#include "util/xmalloc.h"
+#include "window.h"
+
+enum {
+ WINDOW_MIN_WIDTH = 8,
+ WINDOW_MIN_HEIGHT = 3,
+};
+
+static void sanity_check_frame(const Frame *frame)
+{
+ bool has_window = !!frame->window;
+ bool has_frames = frame->frames.count > 0;
+ if (has_window == has_frames) {
+ BUG("frames must contain a window or subframe(s), but never both");
+ }
+ BUG_ON(has_window && frame != frame->window->frame);
+}
+
+static int get_min_w(const Frame *frame)
+{
+ if (frame->window) {
+ return WINDOW_MIN_WIDTH;
+ }
+
+ const PointerArray *subframes = &frame->frames;
+ const size_t count = subframes->count;
+ if (!frame->vertical) {
+ int w = count - 1; // Separators
+ for (size_t i = 0; i < count; i++) {
+ w += get_min_w(subframes->ptrs[i]);
+ }
+ return w;
+ }
+
+ int max = 0;
+ for (size_t i = 0; i < count; i++) {
+ int w = get_min_w(subframes->ptrs[i]);
+ max = MAX(w, max);
+ }
+ return max;
+}
+
+static int get_min_h(const Frame *frame)
+{
+ if (frame->window) {
+ return WINDOW_MIN_HEIGHT;
+ }
+
+ const PointerArray *subframes = &frame->frames;
+ const size_t count = subframes->count;
+ if (frame->vertical) {
+ int h = 0;
+ for (size_t i = 0; i < count; i++) {
+ h += get_min_h(subframes->ptrs[i]);
+ }
+ return h;
+ }
+
+ int max = 0;
+ for (size_t i = 0; i < count; i++) {
+ int h = get_min_h(subframes->ptrs[i]);
+ max = MAX(h, max);
+ }
+ return max;
+}
+
+static int get_min(const Frame *frame)
+{
+ return frame->parent->vertical ? get_min_h(frame) : get_min_w(frame);
+}
+
+static int get_size(const Frame *frame)
+{
+ return frame->parent->vertical ? frame->h : frame->w;
+}
+
+static int get_container_size(const Frame *frame)
+{
+ return frame->vertical ? frame->h : frame->w;
+}
+
+static void set_size(Frame *frame, int size)
+{
+ bool vertical = frame->parent->vertical;
+ int w = vertical ? frame->parent->w : size;
+ int h = vertical ? size : frame->parent->h;
+ set_frame_size(frame, w, h);
+}
+
+static void divide_equally(const Frame *frame)
+{
+ size_t count = frame->frames.count;
+ BUG_ON(count == 0);
+
+ int *min = xnew(int, count);
+ for (size_t i = 0; i < count; i++) {
+ min[i] = get_min(frame->frames.ptrs[i]);
+ }
+
+ int *size = xnew0(int, count);
+ int s = get_container_size(frame);
+ int q, r, used;
+ size_t n = count;
+
+ // Consume q and r as equally as possible
+ do {
+ used = 0;
+ q = s / n;
+ r = s % n;
+ for (size_t i = 0; i < count; i++) {
+ if (size[i] == 0 && min[i] > q) {
+ size[i] = min[i];
+ used += min[i];
+ n--;
+ }
+ }
+ s -= used;
+ } while (used && n > 0);
+
+ for (size_t i = 0; i < count; i++) {
+ Frame *c = frame->frames.ptrs[i];
+ if (size[i] == 0) {
+ size[i] = q + (r-- > 0);
+ }
+ set_size(c, size[i]);
+ }
+
+ free(size);
+ free(min);
+}
+
+static void fix_size(const Frame *frame)
+{
+ size_t count = frame->frames.count;
+ int *size = xnew(int, count);
+ int *min = xnew(int, count);
+ int total = 0;
+ for (size_t i = 0; i < count; i++) {
+ const Frame *c = frame->frames.ptrs[i];
+ min[i] = get_min(c);
+ size[i] = MAX(get_size(c), min[i]);
+ total += size[i];
+ }
+
+ int s = get_container_size(frame);
+ if (total > s) {
+ int n = total - s;
+ for (ssize_t i = count - 1; n > 0 && i >= 0; i--) {
+ int new_size = MAX(size[i] - n, min[i]);
+ n -= size[i] - new_size;
+ size[i] = new_size;
+ }
+ } else {
+ size[count - 1] += s - total;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ set_size(frame->frames.ptrs[i], size[i]);
+ }
+
+ free(size);
+ free(min);
+}
+
+static void add_to_sibling_size(Frame *frame, int count)
+{
+ const Frame *parent = frame->parent;
+ size_t idx = ptr_array_idx(&parent->frames, frame);
+ BUG_ON(idx >= parent->frames.count);
+ if (idx == parent->frames.count - 1) {
+ frame = parent->frames.ptrs[idx - 1];
+ } else {
+ frame = parent->frames.ptrs[idx + 1];
+ }
+ set_size(frame, get_size(frame) + count);
+}
+
+static int sub(Frame *frame, int count)
+{
+ int min = get_min(frame);
+ int old = get_size(frame);
+ int new = MAX(min, old - count);
+ if (new != old) {
+ set_size(frame, new);
+ }
+ return count - (old - new);
+}
+
+static void subtract_from_sibling_size(const Frame *frame, int count)
+{
+ const Frame *parent = frame->parent;
+ size_t idx = ptr_array_idx(&parent->frames, frame);
+ BUG_ON(idx >= parent->frames.count);
+
+ for (size_t i = idx + 1, n = parent->frames.count; i < n; i++) {
+ count = sub(parent->frames.ptrs[i], count);
+ if (count == 0) {
+ return;
+ }
+ }
+
+ for (size_t i = idx; i > 0; i--) {
+ count = sub(parent->frames.ptrs[i - 1], count);
+ if (count == 0) {
+ return;
+ }
+ }
+}
+
+static void resize_to(Frame *frame, int size)
+{
+ const Frame *parent = frame->parent;
+ int total = parent->vertical ? parent->h : parent->w;
+ int count = parent->frames.count;
+ int min = get_min(frame);
+ int max = total - (count - 1) * min;
+ max = MAX(min, max);
+ size = CLAMP(size, min, max);
+
+ int change = size - get_size(frame);
+ if (change == 0) {
+ return;
+ }
+
+ set_size(frame, size);
+ if (change < 0) {
+ add_to_sibling_size(frame, -change);
+ } else {
+ subtract_from_sibling_size(frame, change);
+ }
+}
+
+static bool rightmost_frame(const Frame *frame)
+{
+ const Frame *parent = frame->parent;
+ if (!parent) {
+ return true;
+ }
+ if (!parent->vertical) {
+ if (frame != parent->frames.ptrs[parent->frames.count - 1]) {
+ return false;
+ }
+ }
+ return rightmost_frame(parent);
+}
+
+static Frame *new_frame(void)
+{
+ Frame *frame = xnew0(Frame, 1);
+ frame->equal_size = true;
+ return frame;
+}
+
+static Frame *add_frame(Frame *parent, Window *window, size_t idx)
+{
+ Frame *frame = new_frame();
+ frame->parent = parent;
+ frame->window = window;
+ window->frame = frame;
+ if (parent) {
+ BUG_ON(idx > parent->frames.count);
+ ptr_array_insert(&parent->frames, frame, idx);
+ parent->window = NULL;
+ }
+ return frame;
+}
+
+Frame *new_root_frame(Window *window)
+{
+ return add_frame(NULL, window, 0);
+}
+
+static Frame *find_resizable(Frame *frame, ResizeDirection dir)
+{
+ if (dir == RESIZE_DIRECTION_AUTO) {
+ return frame;
+ }
+
+ while (frame->parent) {
+ if (dir == RESIZE_DIRECTION_VERTICAL && frame->parent->vertical) {
+ return frame;
+ }
+ if (dir == RESIZE_DIRECTION_HORIZONTAL && !frame->parent->vertical) {
+ return frame;
+ }
+ frame = frame->parent;
+ }
+ return NULL;
+}
+
+void set_frame_size(Frame *frame, int w, int h)
+{
+ int min_w = get_min_w(frame);
+ int min_h = get_min_h(frame);
+ w = MAX(w, min_w);
+ h = MAX(h, min_h);
+ frame->w = w;
+ frame->h = h;
+
+ if (frame->window) {
+ w -= rightmost_frame(frame) ? 0 : 1; // Separator
+ set_window_size(frame->window, w, h);
+ return;
+ }
+
+ if (frame->equal_size) {
+ divide_equally(frame);
+ } else {
+ fix_size(frame);
+ }
+}
+
+void equalize_frame_sizes(Frame *parent)
+{
+ parent->equal_size = true;
+ divide_equally(parent);
+ update_window_coordinates(parent);
+}
+
+void resize_frame(Frame *frame, ResizeDirection dir, int size)
+{
+ frame = find_resizable(frame, dir);
+ if (!frame) {
+ return;
+ }
+
+ Frame *parent = frame->parent;
+ parent->equal_size = false;
+ resize_to(frame, size);
+ update_window_coordinates(parent);
+}
+
+void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount)
+{
+ resize_frame(frame, dir, get_size(frame) + amount);
+}
+
+static void update_frame_coordinates(const Frame *frame, int x, int y)
+{
+ if (frame->window) {
+ set_window_coordinates(frame->window, x, y);
+ return;
+ }
+
+ for (size_t i = 0, n = frame->frames.count; i < n; i++) {
+ const Frame *c = frame->frames.ptrs[i];
+ update_frame_coordinates(c, x, y);
+ if (frame->vertical) {
+ y += c->h;
+ } else {
+ x += c->w;
+ }
+ }
+}
+
+static Frame *get_root_frame(Frame *frame)
+{
+ BUG_ON(!frame);
+ while (frame->parent) {
+ frame = frame->parent;
+ }
+ return frame;
+}
+
+void update_window_coordinates(Frame *frame)
+{
+ update_frame_coordinates(get_root_frame(frame), 0, 0);
+}
+
+Frame *split_frame(Window *window, bool vertical, bool before)
+{
+ Frame *frame = window->frame;
+ Frame *parent = frame->parent;
+ if (!parent || parent->vertical != vertical) {
+ // Reparent window
+ frame->vertical = vertical;
+ add_frame(frame, window, 0);
+ parent = frame;
+ }
+
+ size_t idx = ptr_array_idx(&parent->frames, window->frame);
+ BUG_ON(idx >= parent->frames.count);
+ idx += before ? 0 : 1;
+ frame = add_frame(parent, new_window(window->editor), idx);
+ parent->equal_size = true;
+
+ // Recalculate
+ set_frame_size(parent, parent->w, parent->h);
+ update_window_coordinates(parent);
+ return frame;
+}
+
+// Doesn't really split root but adds new frame between root and its contents
+Frame *split_root_frame(EditorState *e, bool vertical, bool before)
+{
+ Frame *old_root = e->root_frame;
+ Frame *new_root = new_frame();
+ ptr_array_append(&new_root->frames, old_root);
+ old_root->parent = new_root;
+ new_root->vertical = vertical;
+ e->root_frame = new_root;
+
+ Frame *frame = add_frame(new_root, new_window(e), before ? 0 : 1);
+ set_frame_size(new_root, old_root->w, old_root->h);
+ update_window_coordinates(new_root);
+ return frame;
+}
+
+// NOTE: does not remove frame from frame->parent->frames
+static void free_frame(Frame *frame)
+{
+ frame->parent = NULL;
+ ptr_array_free_cb(&frame->frames, FREE_FUNC(free_frame));
+
+ if (frame->window) {
+ window_free(frame->window);
+ frame->window = NULL;
+ }
+
+ free(frame);
+}
+
+void remove_frame(EditorState *e, Frame *frame)
+{
+ Frame *parent = frame->parent;
+ if (!parent) {
+ free_frame(frame);
+ return;
+ }
+
+ ptr_array_remove(&parent->frames, frame);
+ free_frame(frame);
+
+ if (parent->frames.count == 1) {
+ // Replace parent with the only child frame
+ Frame *gp = parent->parent;
+ Frame *c = parent->frames.ptrs[0];
+ c->parent = gp;
+ c->w = parent->w;
+ c->h = parent->h;
+ if (gp) {
+ size_t idx = ptr_array_idx(&gp->frames, parent);
+ BUG_ON(idx >= gp->frames.count);
+ gp->frames.ptrs[idx] = c;
+ } else {
+ e->root_frame = c;
+ }
+ free(parent->frames.ptrs);
+ free(parent);
+ parent = c;
+ }
+
+ // Recalculate
+ set_frame_size(parent, parent->w, parent->h);
+ update_window_coordinates(parent);
+}
+
+void dump_frame(const Frame *frame, size_t level, String *str)
+{
+ sanity_check_frame(frame);
+ string_append_memset(str, ' ', level * 4);
+ string_sprintf(str, "%dx%d", frame->w, frame->h);
+
+ const Window *win = frame->window;
+ if (win) {
+ string_append_byte(str, '\n');
+ string_append_memset(str, ' ', (level + 1) * 4);
+ string_sprintf(str, "%d,%d %dx%d ", win->x, win->y, win->w, win->h);
+ string_append_cstring(str, buffer_filename(win->view->buffer));
+ string_append_byte(str, '\n');
+ return;
+ }
+
+ string_append_cstring(str, frame->vertical ? " V" : " H");
+ string_append_cstring(str, frame->equal_size ? "\n" : " !\n");
+
+ for (size_t i = 0, n = frame->frames.count; i < n; i++) {
+ const Frame *c = frame->frames.ptrs[i];
+ dump_frame(c, level + 1, str);
+ }
+}
+
+#if DEBUG >= 1
+void debug_frame(const Frame *frame)
+{
+ sanity_check_frame(frame);
+ for (size_t i = 0, n = frame->frames.count; i < n; i++) {
+ const Frame *c = frame->frames.ptrs[i];
+ BUG_ON(c->parent != frame);
+ debug_frame(c);
+ }
+}
+#endif
diff --git a/examples/dte/frame.h b/examples/dte/frame.h
new file mode 100644
index 0000000..022cf2b
--- /dev/null
+++ b/examples/dte/frame.h
@@ -0,0 +1,48 @@
+#ifndef FRAME_H
+#define FRAME_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string.h"
+
+// A container for other Frames or Windows. Frames and Windows form
+// a tree structure, wherein Windows are the terminal (leaf) nodes.
+typedef struct Frame {
+ struct Frame *parent;
+ // Every frame contains either one window or multiple subframes
+ PointerArray frames;
+ struct Window *window;
+ int w; // Width
+ int h; // Height
+ bool vertical;
+ bool equal_size;
+} Frame;
+
+typedef enum {
+ RESIZE_DIRECTION_AUTO,
+ RESIZE_DIRECTION_HORIZONTAL,
+ RESIZE_DIRECTION_VERTICAL,
+} ResizeDirection;
+
+struct EditorState;
+
+Frame *new_root_frame(struct Window *window);
+void set_frame_size(Frame *frame, int w, int h);
+void equalize_frame_sizes(Frame *parent);
+void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount);
+void resize_frame(Frame *frame, ResizeDirection dir, int size);
+void update_window_coordinates(Frame *frame);
+Frame *split_frame(struct Window *window, bool vertical, bool before);
+Frame *split_root_frame(struct EditorState *e, bool vertical, bool before);
+void remove_frame(struct EditorState *e, Frame *frame);
+void dump_frame(const Frame *frame, size_t level, String *str);
+
+#if DEBUG >= 1
+ void debug_frame(const Frame *frame);
+#else
+ static inline void debug_frame(const Frame* UNUSED_ARG(frame)) {}
+#endif
+
+#endif
diff --git a/examples/dte/history.c b/examples/dte/history.c
new file mode 100644
index 0000000..d03a6c2
--- /dev/null
+++ b/examples/dte/history.c
@@ -0,0 +1,146 @@
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include "history.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/readfile.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+#include "util/xstdio.h"
+
+void history_add(History *history, const char *text)
+{
+ BUG_ON(history->max_entries < 2);
+ if (text[0] == '\0') {
+ return;
+ }
+
+ HashMap *map = &history->entries;
+ HistoryEntry *e = hashmap_get(map, text);
+
+ if (e) {
+ if (e == history->last) {
+ // Existing entry already at end of list; nothing more to do
+ return;
+ }
+ // Remove existing entry from list
+ e->next->prev = e->prev;
+ if (unlikely(e == history->first)) {
+ history->first = e->next;
+ } else {
+ e->prev->next = e->next;
+ }
+ } else {
+ if (map->count == history->max_entries) {
+ // History is full; recycle oldest entry
+ HistoryEntry *old_first = history->first;
+ HistoryEntry *new_first = old_first->next;
+ new_first->prev = NULL;
+ history->first = new_first;
+ e = hashmap_remove(map, old_first->text);
+ BUG_ON(e != old_first);
+ } else {
+ e = xnew(HistoryEntry, 1);
+ }
+ e->text = xstrdup(text);
+ hashmap_insert(map, e->text, e);
+ }
+
+ // Insert entry at end of list
+ HistoryEntry *old_last = history->last;
+ e->next = NULL;
+ e->prev = old_last;
+ history->last = e;
+ if (likely(old_last)) {
+ old_last->next = e;
+ } else {
+ history->first = e;
+ }
+}
+
+bool history_search_forward (
+ const History *history,
+ const HistoryEntry **pos,
+ const char *text
+) {
+ const HistoryEntry *start = *pos ? (*pos)->prev : history->last;
+ for (const HistoryEntry *e = start; e; e = e->prev) {
+ if (str_has_prefix(e->text, text)) {
+ *pos = e;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool history_search_backward (
+ const History *history,
+ const HistoryEntry **pos,
+ const char *text
+) {
+ const HistoryEntry *start = *pos ? (*pos)->next : history->first;
+ for (const HistoryEntry *e = start; e; e = e->next) {
+ if (str_has_prefix(e->text, text)) {
+ *pos = e;
+ return true;
+ }
+ }
+ return false;
+}
+
+void history_load(History *history, char *filename)
+{
+ BUG_ON(!history);
+ BUG_ON(!filename);
+ BUG_ON(history->filename);
+ BUG_ON(history->max_entries < 2);
+
+ hashmap_init(&history->entries, history->max_entries);
+ history->filename = filename;
+
+ char *buf;
+ const ssize_t ssize = read_file(filename, &buf);
+ if (ssize < 0) {
+ if (errno != ENOENT) {
+ error_msg("Error reading %s: %s", filename, strerror(errno));
+ }
+ return;
+ }
+
+ for (size_t pos = 0, size = ssize; pos < size; ) {
+ history_add(history, buf_next_line(buf, &pos, size));
+ }
+
+ free(buf);
+}
+
+void history_save(const History *history)
+{
+ const char *filename = history->filename;
+ if (!filename) {
+ return;
+ }
+
+ FILE *f = xfopen(filename, "w", O_CLOEXEC, 0666);
+ if (!f) {
+ error_msg("Error creating %s: %s", filename, strerror(errno));
+ return;
+ }
+
+ for (const HistoryEntry *e = history->first; e; e = e->next) {
+ xfputs(e->text, f);
+ xfputc('\n', f);
+ }
+
+ fclose(f);
+}
+
+void history_free(History *history)
+{
+ hashmap_free(&history->entries, free);
+ free(history->filename);
+ history->filename = NULL;
+ history->first = NULL;
+ history->last = NULL;
+}
diff --git a/examples/dte/history.h b/examples/dte/history.h
new file mode 100644
index 0000000..12073f5
--- /dev/null
+++ b/examples/dte/history.h
@@ -0,0 +1,35 @@
+#ifndef HISTORY_H
+#define HISTORY_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/hashmap.h"
+#include "util/macros.h"
+
+typedef struct HistoryEntry {
+ struct HistoryEntry *next;
+ struct HistoryEntry *prev;
+ char *text;
+} HistoryEntry;
+
+// This is a HashMap with a doubly-linked list running through the
+// entries, in a way similar to the Java LinkedHashMap class. The
+// HashMap allows duplicates to be found and re-inserted at the end
+// of the list in O(1) time and the doubly-linked entries allow
+// ordered traversal.
+typedef struct {
+ char *filename;
+ HashMap entries;
+ HistoryEntry *first;
+ HistoryEntry *last;
+ size_t max_entries;
+} History;
+
+void history_add(History *history, const char *text);
+bool history_search_forward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT;
+bool history_search_backward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT;
+void history_load(History *history, char *filename);
+void history_save(const History *history);
+void history_free(History *history);
+
+#endif
diff --git a/examples/dte/indent.c b/examples/dte/indent.c
new file mode 100644
index 0000000..0d5d2ae
--- /dev/null
+++ b/examples/dte/indent.c
@@ -0,0 +1,193 @@
+#include <sys/types.h>
+#include "indent.h"
+#include "regexp.h"
+#include "util/xmalloc.h"
+
+char *make_indent(const LocalOptions *options, size_t width)
+{
+ if (width == 0) {
+ return NULL;
+ }
+
+ if (use_spaces_for_indent(options)) {
+ char *str = xmalloc(width + 1);
+ str[width] = '\0';
+ return memset(str, ' ', width);
+ }
+
+ size_t tw = options->tab_width;
+ size_t ntabs = indent_level(width, tw);
+ size_t nspaces = indent_remainder(width, tw);
+ size_t n = ntabs + nspaces;
+ char *str = xmalloc(n + 1);
+ memset(str + ntabs, ' ', nspaces);
+ str[n] = '\0';
+ return memset(str, '\t', ntabs);
+}
+
+static bool indent_inc(const LocalOptions *options, const StringView *line)
+{
+ static regex_t re1, re2;
+ static bool compiled;
+ if (!compiled) {
+ // TODO: Make these patterns configurable via a local option
+ static const char pat1[] = "\\{[\t ]*(//.*|/\\*.*\\*/[\t ]*)?$";
+ static const char pat2[] = "\\}[\t ]*(//.*|/\\*.*\\*/[\t ]*)?$";
+ regexp_compile_or_fatal_error(&re1, pat1, REG_NEWLINE | REG_NOSUB);
+ regexp_compile_or_fatal_error(&re2, pat2, REG_NEWLINE | REG_NOSUB);
+ compiled = true;
+ }
+
+ if (options->brace_indent) {
+ regmatch_t m;
+ if (regexp_exec(&re1, line->data, line->length, 0, &m, 0)) {
+ return true;
+ }
+ if (regexp_exec(&re2, line->data, line->length, 0, &m, 0)) {
+ return false;
+ }
+ }
+
+ const InternedRegexp *ir = options->indent_regex;
+ if (!ir) {
+ return false;
+ }
+
+ BUG_ON(ir->str[0] == '\0');
+ regmatch_t m;
+ return regexp_exec(&ir->re, line->data, line->length, 0, &m, 0);
+}
+
+char *get_indent_for_next_line(const LocalOptions *options, const StringView *line)
+{
+ size_t width = get_indent_width(options, line);
+ if (indent_inc(options, line)) {
+ width = next_indent_width(width, options->indent_width);
+ }
+ return make_indent(options, width);
+}
+
+IndentInfo get_indent_info(const LocalOptions *options, const StringView *line)
+{
+ const char *buf = line->data;
+ const size_t len = line->length;
+ const size_t tw = options->tab_width;
+ const size_t iw = options->indent_width;
+ const bool space_indent = use_spaces_for_indent(options);
+ IndentInfo info = {.sane = true};
+ size_t spaces = 0;
+ size_t tabs = 0;
+ size_t pos = 0;
+
+ for (; pos < len; pos++) {
+ if (buf[pos] == ' ') {
+ info.width++;
+ spaces++;
+ } else if (buf[pos] == '\t') {
+ info.width = next_indent_width(info.width, tw);
+ tabs++;
+ } else {
+ break;
+ }
+ if (indent_remainder(info.width, iw) == 0 && info.sane) {
+ info.sane = space_indent ? !tabs : !spaces;
+ }
+ }
+
+ info.level = indent_level(info.width, iw);
+ info.wsonly = (pos == len);
+ info.bytes = spaces + tabs;
+ return info;
+}
+
+size_t get_indent_width(const LocalOptions *options, const StringView *line)
+{
+ const char *buf = line->data;
+ size_t width = 0;
+ for (size_t i = 0, n = line->length, tw = options->tab_width; i < n; i++) {
+ if (buf[i] == ' ') {
+ width++;
+ } else if (buf[i] == '\t') {
+ width = next_indent_width(width, tw);
+ } else {
+ break;
+ }
+ }
+ return width;
+}
+
+static ssize_t get_current_indent_bytes(const LocalOptions *options, const char *buf, size_t cursor_offset)
+{
+ const size_t tw = options->tab_width;
+ const size_t iw = options->indent_width;
+ size_t ibytes = 0;
+ size_t iwidth = 0;
+
+ for (size_t i = 0; i < cursor_offset; i++) {
+ if (indent_remainder(iwidth, iw) == 0) {
+ ibytes = 0;
+ iwidth = 0;
+ }
+ switch (buf[i]) {
+ case '\t':
+ iwidth = next_indent_width(iwidth, tw);
+ break;
+ case ' ':
+ iwidth++;
+ break;
+ default:
+ // Cursor not at indentation
+ return -1;
+ }
+ ibytes++;
+ }
+
+ if (indent_remainder(iwidth, iw)) {
+ // Cursor at middle of indentation level
+ return -1;
+ }
+
+ return (ssize_t)ibytes;
+}
+
+size_t get_indent_level_bytes_left(const LocalOptions *options, BlockIter *cursor)
+{
+ StringView line;
+ size_t cursor_offset = fetch_this_line(cursor, &line);
+ if (!cursor_offset) {
+ return 0;
+ }
+ ssize_t ibytes = get_current_indent_bytes(options, line.data, cursor_offset);
+ return (ibytes < 0) ? 0 : (size_t)ibytes;
+}
+
+size_t get_indent_level_bytes_right(const LocalOptions *options, BlockIter *cursor)
+{
+ StringView line;
+ size_t cursor_offset = fetch_this_line(cursor, &line);
+ ssize_t ibytes = get_current_indent_bytes(options, line.data, cursor_offset);
+ if (ibytes < 0) {
+ return 0;
+ }
+
+ const size_t tw = options->tab_width;
+ const size_t iw = options->indent_width;
+ size_t iwidth = 0;
+ for (size_t i = cursor_offset, n = line.length; i < n; i++) {
+ switch (line.data[i]) {
+ case '\t':
+ iwidth = next_indent_width(iwidth, tw);
+ break;
+ case ' ':
+ iwidth++;
+ break;
+ default:
+ // No full indentation level at cursor position
+ return 0;
+ }
+ if (indent_remainder(iwidth, iw) == 0) {
+ return i - cursor_offset + 1;
+ }
+ }
+ return 0;
+}
diff --git a/examples/dte/indent.h b/examples/dte/indent.h
new file mode 100644
index 0000000..6198546
--- /dev/null
+++ b/examples/dte/indent.h
@@ -0,0 +1,58 @@
+#ifndef INDENT_H
+#define INDENT_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+#include <strings.h>
+#include "block-iter.h"
+#include "options.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/string-view.h"
+
+typedef struct {
+ size_t bytes; // Size in bytes
+ size_t width; // Width in columns
+ size_t level; // Number of whole `indent-width` levels
+ bool wsonly; // Empty or whitespace-only line
+
+ // Only spaces or tabs, depending on `use_spaces_for_indent()`.
+ // Note that a "sane" line can contain spaces after tabs for alignment.
+ bool sane;
+} IndentInfo;
+
+// Divide `x` by `d`, to obtain the number of whole indent levels.
+// If `d` is a power of 2, shift right by `ffs(d) - 1` instead, to
+// avoid the expensive divide operation. This optimization applies
+// to widths of 1, 2, 4 and 8, which covers all of the sensible ones.
+static inline size_t indent_level(size_t x, size_t d)
+{
+ BUG_ON(d - 1 > 7);
+ return likely(IS_POWER_OF_2(d)) ? x >> (ffs(d) - 1) : x / d;
+}
+
+static inline size_t indent_remainder(size_t x, size_t m)
+{
+ BUG_ON(m - 1 > 7);
+ return likely(IS_POWER_OF_2(m)) ? x & (m - 1) : x % m;
+}
+
+static inline size_t next_indent_width(size_t x, size_t mul)
+{
+ BUG_ON(mul - 1 > 7);
+ if (likely(IS_POWER_OF_2(mul))) {
+ size_t mask = ~(mul - 1);
+ return (x & mask) + mul;
+ }
+ return ((x + mul) / mul) * mul;
+}
+
+char *make_indent(const LocalOptions *options, size_t width);
+char *get_indent_for_next_line(const LocalOptions *options, const StringView *line);
+IndentInfo get_indent_info(const LocalOptions *options, const StringView *line);
+size_t get_indent_width(const LocalOptions *options, const StringView *line);
+size_t get_indent_level_bytes_left(const LocalOptions *options, BlockIter *cursor);
+size_t get_indent_level_bytes_right(const LocalOptions *options, BlockIter *cursor);
+
+#endif
diff --git a/examples/dte/load-save.c b/examples/dte/load-save.c
new file mode 100644
index 0000000..b3ea3fa
--- /dev/null
+++ b/examples/dte/load-save.c
@@ -0,0 +1,505 @@
+#include "compat.h"
+#include <errno.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "load-save.h"
+#include "block.h"
+#include "convert.h"
+#include "encoding.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/fd.h"
+#include "util/list.h"
+#include "util/log.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/time-util.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+
+static void add_block(Buffer *buffer, Block *blk)
+{
+ buffer->nl += blk->nl;
+ list_add_before(&blk->node, &buffer->blocks);
+}
+
+static Block *add_utf8_line (
+ Buffer *buffer,
+ Block *blk,
+ const unsigned char *line,
+ size_t len
+) {
+ size_t size = len + 1;
+ if (blk) {
+ size_t avail = blk->alloc - blk->size;
+ if (size <= avail) {
+ goto copy;
+ }
+ add_block(buffer, blk);
+ }
+ size = MAX(size, 8192);
+ blk = block_new(size);
+copy:
+ memcpy(blk->data + blk->size, line, len);
+ blk->size += len;
+ blk->data[blk->size++] = '\n';
+ blk->nl++;
+ return blk;
+}
+
+static bool decode_and_add_blocks(Buffer *buffer, const unsigned char *buf, size_t size, bool utf8_bom)
+{
+ EncodingType bom_type = detect_encoding_from_bom(buf, size);
+ EncodingType enc_type = buffer->encoding.type;
+ if (enc_type == ENCODING_AUTODETECT) {
+ if (bom_type != UNKNOWN_ENCODING) {
+ BUG_ON(buffer->encoding.name);
+ Encoding e = encoding_from_type(bom_type);
+ if (conversion_supported_by_iconv(e.name, "UTF-8")) {
+ buffer_set_encoding(buffer, e, utf8_bom);
+ } else {
+ buffer_set_encoding(buffer, encoding_from_type(UTF8), utf8_bom);
+ }
+ }
+ }
+
+ // Skip BOM only if it matches the specified file encoding
+ if (bom_type != UNKNOWN_ENCODING && bom_type == buffer->encoding.type) {
+ const ByteOrderMark *bom = get_bom_for_encoding(bom_type);
+ if (bom) {
+ const size_t bom_len = bom->len;
+ buf += bom_len;
+ size -= bom_len;
+ buffer->bom = true;
+ }
+ }
+
+ FileDecoder *dec = new_file_decoder(buffer->encoding.name, buf, size);
+ if (!dec) {
+ return false;
+ }
+
+ const char *line;
+ size_t len;
+ if (file_decoder_read_line(dec, &line, &len)) {
+ if (len && line[len - 1] == '\r') {
+ buffer->crlf_newlines = true;
+ len--;
+ }
+ Block *blk = add_utf8_line(buffer, NULL, line, len);
+ while (file_decoder_read_line(dec, &line, &len)) {
+ if (buffer->crlf_newlines && len && line[len - 1] == '\r') {
+ len--;
+ }
+ blk = add_utf8_line(buffer, blk, line, len);
+ }
+ if (blk) {
+ add_block(buffer, blk);
+ }
+ }
+
+ if (buffer->encoding.type == ENCODING_AUTODETECT) {
+ const char *enc = file_decoder_get_encoding(dec);
+ buffer_set_encoding(buffer, encoding_from_name(enc ? enc : "UTF-8"), utf8_bom);
+ }
+
+ free_file_decoder(dec);
+ return true;
+}
+
+static void fixup_blocks(Buffer *buffer)
+{
+ if (list_empty(&buffer->blocks)) {
+ Block *blk = block_new(1);
+ list_add_before(&blk->node, &buffer->blocks);
+ } else {
+ // Incomplete lines are not allowed because they are special cases
+ // and cause lots of trouble
+ Block *blk = BLOCK(buffer->blocks.prev);
+ if (blk->size && blk->data[blk->size - 1] != '\n') {
+ if (blk->size == blk->alloc) {
+ blk->alloc = round_size_to_next_multiple(blk->size + 1, 64);
+ xrenew(blk->data, blk->alloc);
+ }
+ blk->data[blk->size++] = '\n';
+ blk->nl++;
+ buffer->nl++;
+ }
+ }
+}
+
+static int xmadvise_sequential(void *addr, size_t len)
+{
+#if HAVE_POSIX_MADVISE
+ return posix_madvise(addr, len, POSIX_MADV_SEQUENTIAL);
+#else
+ // "The posix_madvise() function shall have no effect on the semantics
+ // of access to memory in the specified range, although it may affect
+ // the performance of access". Ergo, doing nothing is a valid fallback.
+ (void)addr;
+ (void)len;
+ return 0;
+#endif
+}
+
+static bool update_file_info(FileInfo *info, const struct stat *st)
+{
+ *info = (FileInfo) {
+ .size = st->st_size,
+ .mode = st->st_mode,
+ .gid = st->st_gid,
+ .uid = st->st_uid,
+ .dev = st->st_dev,
+ .ino = st->st_ino,
+ .mtime = *get_stat_mtime(st),
+ };
+ return true;
+}
+
+static bool buffer_stat(FileInfo *info, const char *filename)
+{
+ struct stat st;
+ return !stat(filename, &st) && update_file_info(info, &st);
+}
+
+static bool buffer_fstat(FileInfo *info, int fd)
+{
+ struct stat st;
+ return !fstat(fd, &st) && update_file_info(info, &st);
+}
+
+bool read_blocks(Buffer *buffer, int fd, bool utf8_bom)
+{
+ const size_t map_size = 64 * 1024;
+ size_t size = buffer->file.size;
+ unsigned char *buf = NULL;
+ bool mapped = false;
+ bool ret = false;
+
+ if (size >= map_size) {
+ // NOTE: size must be greater than 0
+ buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
+ if (buf != MAP_FAILED) {
+ xmadvise_sequential(buf, size);
+ mapped = true;
+ goto decode;
+ }
+ buf = NULL;
+ }
+
+ if (likely(size > 0)) {
+ buf = malloc(size);
+ if (unlikely(!buf)) {
+ goto error;
+ }
+ ssize_t rc = xread_all(fd, buf, size);
+ if (unlikely(rc < 0)) {
+ goto error;
+ }
+ size = rc;
+ } else {
+ // st_size is zero for some files in /proc
+ size_t alloc = map_size;
+ BUG_ON(!IS_POWER_OF_2(alloc));
+ buf = malloc(alloc);
+ if (unlikely(!buf)) {
+ goto error;
+ }
+ size_t pos = 0;
+ while (1) {
+ ssize_t rc = xread_all(fd, buf + pos, alloc - pos);
+ if (rc < 0) {
+ goto error;
+ }
+ if (rc == 0) {
+ break;
+ }
+ pos += rc;
+ if (pos == alloc) {
+ size_t new_alloc = alloc << 1;
+ if (unlikely(alloc >= new_alloc)) {
+ errno = EOVERFLOW;
+ goto error;
+ }
+ alloc = new_alloc;
+ char *new_buf = realloc(buf, alloc);
+ if (unlikely(!new_buf)) {
+ goto error;
+ }
+ buf = new_buf;
+ }
+ }
+ size = pos;
+ }
+
+decode:
+ ret = decode_and_add_blocks(buffer, buf, size, utf8_bom);
+
+error:
+ if (mapped) {
+ munmap(buf, size);
+ } else {
+ free(buf);
+ }
+
+ if (ret) {
+ fixup_blocks(buffer);
+ }
+
+ return ret;
+}
+
+bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist)
+{
+ int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0);
+
+ if (fd < 0) {
+ if (errno != ENOENT) {
+ return error_msg("Error opening %s: %s", filename, strerror(errno));
+ }
+ if (must_exist) {
+ return error_msg("File %s does not exist", filename);
+ }
+ fixup_blocks(buffer);
+ } else {
+ if (!buffer_fstat(&buffer->file, fd)) {
+ error_msg("fstat failed on %s: %s", filename, strerror(errno));
+ goto error;
+ }
+ if (!S_ISREG(buffer->file.mode)) {
+ error_msg("Not a regular file %s", filename);
+ goto error;
+ }
+ if (unlikely(buffer->file.size < 0)) {
+ error_msg("Invalid file size: %jd", (intmax_t)buffer->file.size);
+ goto error;
+ }
+ if (buffer->file.size / 1024 / 1024 > gopts->filesize_limit) {
+ error_msg (
+ "File size exceeds 'filesize-limit' option (%uMiB): %s",
+ gopts->filesize_limit, filename
+ );
+ goto error;
+ }
+ if (!read_blocks(buffer, fd, gopts->utf8_bom)) {
+ error_msg("Error reading %s: %s", filename, strerror(errno));
+ goto error;
+ }
+ xclose(fd);
+ }
+
+ if (buffer->encoding.type == ENCODING_AUTODETECT) {
+ Encoding enc = encoding_from_type(UTF8);
+ buffer_set_encoding(buffer, enc, gopts->utf8_bom);
+ }
+
+ return true;
+
+error:
+ xclose(fd);
+ return false;
+}
+
+static mode_t get_umask(void)
+{
+ // Wonderful get-and-set API
+ mode_t old = umask(0);
+ umask(old);
+ return old;
+}
+
+static bool write_buffer(Buffer *buffer, FileEncoder *enc, int fd, EncodingType bom_type)
+{
+ size_t size = 0;
+ const ByteOrderMark *bom = get_bom_for_encoding(bom_type);
+ if (bom) {
+ size = bom->len;
+ BUG_ON(size == 0);
+ if (xwrite_all(fd, bom->bytes, size) < 0) {
+ return error_msg_errno("write");
+ }
+ }
+
+ Block *blk;
+ block_for_each(blk, &buffer->blocks) {
+ ssize_t rc = file_encoder_write(enc, blk->data, blk->size);
+ if (rc < 0) {
+ return error_msg_errno("write");
+ }
+ size += rc;
+ }
+
+ size_t nr_errors = file_encoder_get_nr_errors(enc);
+ if (nr_errors > 0) {
+ // Any real error hides this message
+ error_msg (
+ "Warning: %zu non-reversible character conversion%s; file saved",
+ nr_errors,
+ (nr_errors > 1) ? "s" : ""
+ );
+ }
+
+ // Need to truncate if writing to existing file
+ if (xftruncate(fd, size)) {
+ return error_msg_errno("ftruncate");
+ }
+
+ return true;
+}
+
+static int tmp_file(const char *filename, const FileInfo *info, char *buf, size_t buflen)
+{
+ if (str_has_prefix(filename, "/tmp/")) {
+ // Don't use temporary file when saving file in /tmp because crontab
+ // command doesn't like the file to be replaced
+ return -1;
+ }
+
+ const char *base = path_basename(filename);
+ const StringView dir = path_slice_dirname(filename);
+ const int dlen = (int)dir.length;
+ int n = snprintf(buf, buflen, "%.*s/.tmp.%s.XXXXXX", dlen, dir.data, base);
+ if (unlikely(n <= 0 || n >= buflen)) {
+ buf[0] = '\0';
+ return -1;
+ }
+
+ int fd = mkstemp(buf);
+ if (fd < 0) {
+ // No write permission to the directory?
+ buf[0] = '\0';
+ return -1;
+ }
+
+ if (!info->mode) {
+ // New file
+ if (xfchmod(fd, 0666 & ~get_umask()) != 0) {
+ LOG_WARNING("failed to set file mode: %s", strerror(errno));
+ }
+ return fd;
+ }
+
+ // Preserve ownership and mode of the original file if possible
+ if (xfchown(fd, info->uid, info->gid) != 0) {
+ LOG_WARNING("failed to preserve file ownership: %s", strerror(errno));
+ }
+ if (xfchmod(fd, info->mode) != 0) {
+ LOG_WARNING("failed to preserve file mode: %s", strerror(errno));
+ }
+
+ return fd;
+}
+
+static int xfsync(int fd)
+{
+#if HAVE_FSYNC
+ retry:
+ if (fsync(fd) == 0) {
+ return 0;
+ }
+
+ switch (errno) {
+ // EINVAL is ignored because it just means "operation not possible
+ // on this descriptor" rather than indicating an actual error
+ case EINVAL:
+ case ENOTSUP:
+ case ENOSYS:
+ return 0;
+ case EINTR:
+ goto retry;
+ }
+
+ return -1;
+#else
+ (void)fd;
+ return 0;
+#endif
+}
+
+bool save_buffer (
+ Buffer *buffer,
+ const char *filename,
+ const Encoding *encoding,
+ bool crlf,
+ bool write_bom,
+ bool hardlinks
+) {
+ char tmp[8192];
+ tmp[0] = '\0';
+ int fd = -1;
+ if (hardlinks) {
+ LOG_INFO("target file has hard links; writing in-place");
+ } else {
+ // Try to use temporary file (safer)
+ fd = tmp_file(filename, &buffer->file, tmp, sizeof(tmp));
+ }
+
+ if (fd < 0) {
+ // Overwrite the original file directly (if it exists).
+ // Ownership is preserved automatically if the file exists.
+ mode_t mode = buffer->file.mode;
+ if (mode == 0) {
+ // New file
+ mode = 0666 & ~get_umask();
+ }
+ fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode);
+ if (fd < 0) {
+ return error_msg_errno("open");
+ }
+ }
+
+ FileEncoder *enc = new_file_encoder(encoding, crlf, fd);
+ if (unlikely(!enc)) {
+ // This should never happen because encoding is validated early
+ error_msg_errno("new_file_encoder");
+ goto error;
+ }
+
+ EncodingType bom_type = write_bom ? encoding->type : UNKNOWN_ENCODING;
+ if (!write_buffer(buffer, enc, fd, bom_type)) {
+ goto error;
+ }
+
+ if (buffer->options.fsync && xfsync(fd) != 0) {
+ error_msg_errno("fsync");
+ goto error;
+ }
+
+ int r = xclose(fd);
+ fd = -1;
+ if (r != 0) {
+ error_msg_errno("close");
+ goto error;
+ }
+
+ if (tmp[0] && rename(tmp, filename)) {
+ error_msg_errno("rename");
+ goto error;
+ }
+
+ free_file_encoder(enc);
+ buffer_stat(&buffer->file, filename);
+ return true;
+
+error:
+ if (fd >= 0) {
+ xclose(fd);
+ }
+ if (enc) {
+ free_file_encoder(enc);
+ }
+ if (tmp[0]) {
+ unlink(tmp);
+ } else {
+ // Not using temporary file, therefore mtime may have changed.
+ // Update stat to avoid "File has been modified by someone else"
+ // error later when saving the file again.
+ buffer_stat(&buffer->file, filename);
+ }
+ return false;
+}
diff --git a/examples/dte/load-save.h b/examples/dte/load-save.h
new file mode 100644
index 0000000..e8c0a46
--- /dev/null
+++ b/examples/dte/load-save.h
@@ -0,0 +1,14 @@
+#ifndef LOAD_SAVE_H
+#define LOAD_SAVE_H
+
+#include <stdbool.h>
+#include "buffer.h"
+#include "encoding.h"
+#include "options.h"
+#include "util/macros.h"
+
+bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist) WARN_UNUSED_RESULT;
+bool save_buffer(Buffer *buffer, const char *filename, const Encoding *encoding, bool crlf, bool write_bom, bool hardlinks) WARN_UNUSED_RESULT;
+bool read_blocks(Buffer *buffer, int fd, bool utf8_bom) WARN_UNUSED_RESULT;
+
+#endif
diff --git a/examples/dte/lock.c b/examples/dte/lock.c
new file mode 100644
index 0000000..74cf3a4
--- /dev/null
+++ b/examples/dte/lock.c
@@ -0,0 +1,201 @@
+#include <errno.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include "lock.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/log.h"
+#include "util/path.h"
+#include "util/readfile.h"
+#include "util/str-util.h"
+#include "util/string-view.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+#include "util/xsnprintf.h"
+
+// These are initialized during early startup and then never changed,
+// so they're deemed an "acceptable" use of globals:
+static const char *file_locks;
+static const char *file_locks_lock;
+static mode_t file_locks_mode = 0666;
+static pid_t editor_pid;
+
+void init_file_locks_context(const char *fallback_dir, pid_t pid)
+{
+ BUG_ON(file_locks);
+ const char *dir = xgetenv("XDG_RUNTIME_DIR");
+ if (!dir) {
+ LOG_INFO("$XDG_RUNTIME_DIR not set");
+ dir = fallback_dir;
+ } else if (unlikely(!path_is_absolute(dir))) {
+ LOG_WARNING("$XDG_RUNTIME_DIR invalid (not an absolute path)");
+ dir = fallback_dir;
+ } else {
+ // Set sticky bit (see XDG Base Directory Specification)
+ #ifdef S_ISVTX
+ file_locks_mode |= S_ISVTX;
+ #endif
+ }
+
+ file_locks = path_join(dir, "dte-locks");
+ file_locks_lock = path_join(dir, "dte-locks.lock");
+ editor_pid = pid;
+ LOG_INFO("locks file: %s", file_locks);
+}
+
+static bool process_exists(pid_t pid)
+{
+ return !kill(pid, 0);
+}
+
+static pid_t rewrite_lock_file(char *buf, size_t *sizep, const char *filename)
+{
+ const size_t filename_len = strlen(filename);
+ size_t size = *sizep;
+ pid_t other_pid = 0;
+
+ for (size_t pos = 0, bol = 0; pos < size; bol = pos) {
+ StringView line = buf_slice_next_line(buf, &pos, size);
+ uintmax_t num;
+ size_t numlen = buf_parse_uintmax(line.data, line.length, &num);
+ if (unlikely(numlen == 0 || num != (pid_t)num)) {
+ goto remove_line;
+ }
+
+ strview_remove_prefix(&line, numlen);
+ if (unlikely(!strview_has_prefix(&line, " /"))) {
+ goto remove_line;
+ }
+ strview_remove_prefix(&line, 1);
+
+ bool same = strview_equal_strn(&line, filename, filename_len);
+ pid_t pid = (pid_t)num;
+ if (pid == editor_pid) {
+ if (same) {
+ goto remove_line;
+ }
+ continue;
+ } else if (process_exists(pid)) {
+ if (same) {
+ other_pid = pid;
+ }
+ continue;
+ }
+
+ remove_line:
+ memmove(buf + bol, buf + pos, size - pos);
+ size -= pos - bol;
+ pos = bol;
+ }
+
+ *sizep = size;
+ return other_pid;
+}
+
+static bool lock_or_unlock(const char *filename, bool lock)
+{
+ BUG_ON(!file_locks);
+ if (streq(filename, file_locks) || streq(filename, file_locks_lock)) {
+ return true;
+ }
+
+ mode_t mode = file_locks_mode;
+ int tries = 0;
+ int wfd;
+ while (1) {
+ wfd = xopen(file_locks_lock, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode);
+ if (wfd >= 0) {
+ break;
+ }
+
+ if (errno != EEXIST) {
+ return error_msg("Error creating %s: %s", file_locks_lock, strerror(errno));
+ }
+ if (++tries == 3) {
+ if (unlink(file_locks_lock)) {
+ return error_msg (
+ "Error removing stale lock file %s: %s",
+ file_locks_lock,
+ strerror(errno)
+ );
+ }
+ error_msg("Stale lock file %s removed", file_locks_lock);
+ } else {
+ const struct timespec req = {
+ .tv_sec = 0,
+ .tv_nsec = 100 * 1000000,
+ };
+ nanosleep(&req, NULL);
+ }
+ }
+
+ char *buf = NULL;
+ ssize_t ssize = read_file(file_locks, &buf);
+ if (ssize < 0) {
+ if (errno != ENOENT) {
+ error_msg("Error reading %s: %s", file_locks, strerror(errno));
+ goto error;
+ }
+ ssize = 0;
+ }
+
+ size_t size = (size_t)ssize;
+ pid_t pid = rewrite_lock_file(buf, &size, filename);
+ if (lock) {
+ if (pid == 0) {
+ intmax_t p = (intmax_t)editor_pid;
+ size_t n = strlen(filename) + DECIMAL_STR_MAX(pid) + 4;
+ xrenew(buf, size + n);
+ size += xsnprintf(buf + size, n, "%jd %s\n", p, filename);
+ } else {
+ intmax_t p = (intmax_t)pid;
+ error_msg("File is locked (%s) by process %jd", file_locks, p);
+ }
+ }
+
+ if (xwrite_all(wfd, buf, size) < 0) {
+ error_msg("Error writing %s: %s", file_locks_lock, strerror(errno));
+ goto error;
+ }
+
+ int r = xclose(wfd);
+ wfd = -1;
+ if (r != 0) {
+ error_msg("Error closing %s: %s", file_locks_lock, strerror(errno));
+ goto error;
+ }
+
+ if (rename(file_locks_lock, file_locks)) {
+ const char *err = strerror(errno);
+ error_msg("Renaming %s to %s: %s", file_locks_lock, file_locks, err);
+ goto error;
+ }
+
+ free(buf);
+ return (pid == 0);
+
+error:
+ unlink(file_locks_lock);
+ free(buf);
+ if (wfd >= 0) {
+ xclose(wfd);
+ }
+ return false;
+}
+
+bool lock_file(const char *filename)
+{
+ return lock_or_unlock(filename, true);
+}
+
+void unlock_file(const char *filename)
+{
+ lock_or_unlock(filename, false);
+}
diff --git a/examples/dte/lock.h b/examples/dte/lock.h
new file mode 100644
index 0000000..9d2a90a
--- /dev/null
+++ b/examples/dte/lock.h
@@ -0,0 +1,12 @@
+#ifndef LOCK_H
+#define LOCK_H
+
+#include <stdbool.h>
+#include <sys/types.h>
+#include "util/macros.h"
+
+void init_file_locks_context(const char *fallback_dir, pid_t pid);
+bool lock_file(const char *filename) WARN_UNUSED_RESULT;
+void unlock_file(const char *filename);
+
+#endif
diff --git a/examples/dte/main.c b/examples/dte/main.c
new file mode 100644
index 0000000..11025af
--- /dev/null
+++ b/examples/dte/main.c
@@ -0,0 +1,575 @@
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+#include "block.h"
+#include "commands.h"
+#include "compiler.h"
+#include "config.h"
+#include "editor.h"
+#include "error.h"
+#include "file-history.h"
+#include "frame.h"
+#include "history.h"
+#include "load-save.h"
+#include "move.h"
+#include "screen.h"
+#include "search.h"
+#include "signals.h"
+#include "syntax/state.h"
+#include "syntax/syntax.h"
+#include "tag.h"
+#include "terminal/input.h"
+#include "terminal/key.h"
+#include "terminal/mode.h"
+#include "terminal/output.h"
+#include "terminal/terminal.h"
+#include "util/debug.h"
+#include "util/exitcode.h"
+#include "util/fd.h"
+#include "util/log.h"
+#include "util/macros.h"
+#include "util/path.h"
+#include "util/ptr-array.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+#include "util/xsnprintf.h"
+#include "view.h"
+#include "window.h"
+#include "../build/version.h"
+
+static void term_cleanup(EditorState *e)
+{
+ set_fatal_error_cleanup_handler(NULL, NULL);
+ if (!e->child_controls_terminal) {
+ ui_end(e);
+ }
+}
+
+static void cleanup_handler(void *userdata)
+{
+ term_cleanup(userdata);
+}
+
+static ExitCode write_stdout(const char *str, size_t len)
+{
+ if (xwrite_all(STDOUT_FILENO, str, len) < 0) {
+ perror("write");
+ return EX_IOERR;
+ }
+ return EX_OK;
+}
+
+static ExitCode list_builtin_configs(void)
+{
+ String str = dump_builtin_configs();
+ BUG_ON(!str.buffer);
+ ExitCode e = write_stdout(str.buffer, str.len);
+ string_free(&str);
+ return e;
+}
+
+static ExitCode dump_builtin_config(const char *name)
+{
+ const BuiltinConfig *cfg = get_builtin_config(name);
+ if (!cfg) {
+ fprintf(stderr, "Error: no built-in config with name '%s'\n", name);
+ return EX_USAGE;
+ }
+ return write_stdout(cfg->text.data, cfg->text.length);
+}
+
+static ExitCode lint_syntax(const char *filename)
+{
+ EditorState *e = init_editor_state();
+ int err;
+ BUG_ON(e->status != EDITOR_INITIALIZING);
+ const Syntax *s = load_syntax_file(e, filename, CFG_MUST_EXIST, &err);
+ if (s) {
+ const size_t n = s->states.count;
+ const char *plural = (n == 1) ? "" : "s";
+ printf("OK: loaded syntax '%s' with %zu state%s\n", s->name, n, plural);
+ } else if (err == EINVAL) {
+ error_msg("%s: no default syntax found", filename);
+ }
+ free_editor_state(e);
+ return get_nr_errors() ? EX_DATAERR : EX_OK;
+}
+
+static ExitCode showkey_loop(const char *term_name, const char *colorterm)
+{
+ if (unlikely(!term_raw())) {
+ perror("tcsetattr");
+ return EX_IOERR;
+ }
+
+ Terminal term;
+ TermOutputBuffer *obuf = &term.obuf;
+ TermInputBuffer *ibuf = &term.ibuf;
+ term_init(&term, term_name, colorterm);
+ term_input_init(ibuf);
+ term_output_init(obuf);
+ term_enable_private_modes(&term);
+ term_add_literal(obuf, "Press any key combination, or use Ctrl+D to exit\r\n");
+ term_output_flush(obuf);
+
+ char keystr[KEYCODE_STR_MAX];
+ for (bool loop = true; loop; ) {
+ KeyCode key = term_read_key(&term, 100);
+ switch (key) {
+ case KEY_NONE:
+ case KEY_IGNORE:
+ continue;
+ case KEY_BRACKETED_PASTE:
+ case KEY_DETECTED_PASTE:
+ term_discard_paste(ibuf, key == KEY_BRACKETED_PASTE);
+ continue;
+ case MOD_CTRL | 'd':
+ loop = false;
+ }
+ size_t keylen = keycode_to_string(key, keystr);
+ term_add_literal(obuf, " ");
+ term_add_bytes(obuf, keystr, keylen);
+ term_add_literal(obuf, "\r\n");
+ term_output_flush(obuf);
+ }
+
+ term_restore_private_modes(&term);
+ term_output_flush(obuf);
+ term_cooked();
+ term_input_free(ibuf);
+ term_output_free(obuf);
+ return EX_OK;
+}
+
+static ExitCode init_std_fds(int std_fds[2])
+{
+ FILE *streams[3] = {stdin, stdout, stderr};
+ for (int i = 0; i < ARRAYLEN(streams); i++) {
+ if (is_controlling_tty(i)) {
+ continue;
+ }
+
+ if (i < STDERR_FILENO) {
+ // Try to create a duplicate fd for redirected stdin/stdout; to
+ // allow reading/writing after freopen(3) closes the original
+ int fd = fcntl(i, F_DUPFD_CLOEXEC, 3);
+ if (fd == -1 && errno != EBADF) {
+ perror("fcntl");
+ return EX_OSERR;
+ }
+ std_fds[i] = fd;
+ }
+
+ // Ensure standard streams are connected to the terminal during
+ // editor operation, regardless of how they were redirected
+ if (unlikely(!freopen("/dev/tty", i ? "w" : "r", streams[i]))) {
+ const char *err = strerror(errno);
+ fprintf(stderr, "Failed to open tty for fd %d: %s\n", i, err);
+ return EX_IOERR;
+ }
+
+ int new_fd = fileno(streams[i]);
+ if (unlikely(new_fd != i)) {
+ // This should never happen in a single-threaded program.
+ // freopen() should call fclose() followed by open() and
+ // POSIX requires a successful call to open() to return the
+ // lowest available file descriptor.
+ fprintf(stderr, "freopen() changed fd from %d to %d\n", i, new_fd);
+ return EX_OSERR;
+ }
+
+ if (unlikely(!is_controlling_tty(new_fd))) {
+ perror("tcgetpgrp");
+ return EX_OSERR;
+ }
+ }
+
+ return EX_OK;
+}
+
+static Buffer *init_std_buffer(EditorState *e, int fds[2])
+{
+ const char *name = NULL;
+ Buffer *buffer = NULL;
+
+ if (fds[STDIN_FILENO] >= 3) {
+ Encoding enc = encoding_from_type(UTF8);
+ buffer = buffer_new(&e->buffers, &e->options, &enc);
+ if (read_blocks(buffer, fds[STDIN_FILENO], false)) {
+ name = "(stdin)";
+ buffer->temporary = true;
+ } else {
+ error_msg("Unable to read redirected stdin");
+ remove_and_free_buffer(&e->buffers, buffer);
+ buffer = NULL;
+ }
+ }
+
+ if (fds[STDOUT_FILENO] >= 3) {
+ if (!buffer) {
+ buffer = open_empty_buffer(&e->buffers, &e->options);
+ name = "(stdout)";
+ } else {
+ name = "(stdin|stdout)";
+ }
+ buffer->stdout_buffer = true;
+ buffer->temporary = true;
+ }
+
+ BUG_ON(!buffer != !name);
+ if (name) {
+ set_display_filename(buffer, xstrdup(name));
+ }
+
+ return buffer;
+}
+
+static ExitCode init_logging(const char *filename, const char *req_level_str)
+{
+ if (!filename || filename[0] == '\0') {
+ return EX_OK;
+ }
+
+ LogLevel req_level = log_level_from_str(req_level_str);
+ if (req_level == LOG_LEVEL_NONE) {
+ return EX_OK;
+ }
+ if (req_level == LOG_LEVEL_INVALID) {
+ fprintf(stderr, "Invalid $DTE_LOG_LEVEL value: '%s'\n", req_level_str);
+ return EX_USAGE;
+ }
+
+ // https://no-color.org/
+ const char *no_color = xgetenv("NO_COLOR");
+
+ LogLevel got_level = log_open(filename, req_level, !no_color);
+ if (got_level == LOG_LEVEL_NONE) {
+ const char *err = strerror(errno);
+ fprintf(stderr, "Failed to open $DTE_LOG (%s): %s\n", filename, err);
+ return EX_IOERR;
+ }
+
+ const char *got_level_str = log_level_to_str(got_level);
+ if (got_level != req_level) {
+ const char *r = req_level_str;
+ const char *g = got_level_str;
+ LOG_WARNING("log level '%s' unavailable; falling back to '%s'", r, g);
+ }
+
+ LOG_INFO("logging to '%s' (level: %s)", filename, got_level_str);
+
+ if (no_color) {
+ LOG_INFO("log colors disabled ($NO_COLOR)");
+ }
+
+ struct utsname u;
+ if (likely(uname(&u) >= 0)) {
+ LOG_INFO("system: %s/%s %s", u.sysname, u.machine, u.release);
+ } else {
+ LOG_ERRNO("uname");
+ }
+ return EX_OK;
+}
+
+static void log_config_counts(const EditorState *e)
+{
+ if (!log_level_enabled(LOG_LEVEL_INFO)) {
+ return;
+ }
+
+ size_t nbinds = 0;
+ for (size_t i = 0; i < ARRAYLEN(e->modes); i++) {
+ nbinds += e->modes[i].key_bindings.count;
+ }
+
+ size_t nerrorfmts = 0;
+ for (HashMapIter it = hashmap_iter(&e->compilers); hashmap_next(&it); ) {
+ const Compiler *compiler = it.entry->value;
+ nerrorfmts += compiler->error_formats.count;
+ }
+
+ LOG_INFO (
+ "binds=%zu aliases=%zu hi=%zu ft=%zu option=%zu errorfmt=%zu(%zu)",
+ nbinds,
+ e->aliases.count,
+ e->colors.other.count + NR_BC,
+ e->filetypes.count,
+ e->file_options.count,
+ e->compilers.count,
+ nerrorfmts
+ );
+}
+
+static const char copyright[] =
+ "dte " VERSION "\n"
+ "(C) 2013-2023 Craig Barnes\n"
+ "(C) 2010-2015 Timo Hirvonen\n"
+ "This program is free software; you can redistribute and/or modify\n"
+ "it under the terms of the GNU General Public License version 2\n"
+ "<https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.\n"
+ "There is NO WARRANTY, to the extent permitted by law.\n";
+
+static const char usage[] =
+ "Usage: %s [OPTIONS] [[+LINE] FILE]...\n\n"
+ "Options:\n"
+ " -c COMMAND Run COMMAND after editor starts\n"
+ " -t CTAG Jump to source location of CTAG\n"
+ " -r RCFILE Read user config from RCFILE instead of ~/.dte/rc\n"
+ " -s FILE Validate dte-syntax commands in FILE and exit\n"
+ " -b NAME Print built-in config matching NAME and exit\n"
+ " -B Print list of built-in config names and exit\n"
+ " -H Don't load or save history files\n"
+ " -R Don't read user config file\n"
+ " -K Start editor in \"showkey\" mode\n"
+ " -h Display help summary and exit\n"
+ " -V Display version number and exit\n"
+ "\n";
+
+int main(int argc, char *argv[])
+{
+ static const char optstring[] = "hBHKRVb:c:t:r:s:";
+ const char *tag = NULL;
+ const char *rc = NULL;
+ const char *commands[8];
+ size_t nr_commands = 0;
+ bool read_rc = true;
+ bool use_showkey = false;
+ bool load_and_save_history = true;
+ set_print_errors_to_stderr(true);
+
+ for (int ch; (ch = getopt(argc, argv, optstring)) != -1; ) {
+ switch (ch) {
+ case 'c':
+ if (unlikely(nr_commands >= ARRAYLEN(commands))) {
+ fputs("Error: too many -c options used\n", stderr);
+ return EX_USAGE;
+ }
+ commands[nr_commands++] = optarg;
+ break;
+ case 't':
+ tag = optarg;
+ break;
+ case 'r':
+ rc = optarg;
+ break;
+ case 's':
+ return lint_syntax(optarg);
+ case 'R':
+ read_rc = false;
+ break;
+ case 'b':
+ return dump_builtin_config(optarg);
+ case 'B':
+ return list_builtin_configs();
+ case 'H':
+ load_and_save_history = false;
+ break;
+ case 'K':
+ use_showkey = true;
+ goto loop_break;
+ case 'V':
+ return write_stdout(copyright, sizeof(copyright));
+ case 'h':
+ printf(usage, (argv[0] && argv[0][0]) ? argv[0] : "dte");
+ return EX_OK;
+ default:
+ return EX_USAGE;
+ }
+ }
+
+loop_break:;
+
+ const char *term_name = xgetenv("TERM");
+ if (!term_name) {
+ fputs("Error: $TERM not set\n", stderr);
+ // This is considered a "usage" error, because the program
+ // must be started from a properly configured terminal
+ return EX_USAGE;
+ }
+
+ // This must be done before calling init_logging(), otherwise an
+ // invocation like e.g. `DTE_LOG=/dev/pts/2 dte 0<&-` could
+ // cause the logging fd to be opened as STDIN_FILENO
+ int std_fds[2] = {-1, -1};
+ ExitCode r = init_std_fds(std_fds);
+ if (unlikely(r != EX_OK)) {
+ return r;
+ }
+
+ r = init_logging(getenv("DTE_LOG"), getenv("DTE_LOG_LEVEL"));
+ if (unlikely(r != EX_OK)) {
+ return r;
+ }
+
+ if (!term_mode_init()) {
+ perror("tcgetattr");
+ return EX_IOERR;
+ }
+
+ const char *colorterm = getenv("COLORTERM");
+ if (use_showkey) {
+ return showkey_loop(term_name, colorterm);
+ }
+
+ EditorState *e = init_editor_state();
+ Terminal *term = &e->terminal;
+ term_init(term, term_name, colorterm);
+
+ Buffer *std_buffer = init_std_buffer(e, std_fds);
+ bool have_stdout_buffer = std_buffer && std_buffer->stdout_buffer;
+
+ // Create this early (needed if "lock-files" is true)
+ const char *cfgdir = e->user_config_dir;
+ BUG_ON(!cfgdir);
+ if (mkdir(cfgdir, 0755) != 0 && errno != EEXIST) {
+ error_msg("Error creating %s: %s", cfgdir, strerror(errno));
+ load_and_save_history = false;
+ e->options.lock_files = false;
+ }
+
+ term_save_title(term);
+ exec_builtin_rc(e);
+
+ if (read_rc) {
+ ConfigFlags flags = CFG_NOFLAGS;
+ char buf[4096];
+ if (rc) {
+ flags |= CFG_MUST_EXIST;
+ } else {
+ xsnprintf(buf, sizeof buf, "%s/%s", cfgdir, "rc");
+ rc = buf;
+ }
+ LOG_INFO("loading configuration from %s", rc);
+ read_normal_config(e, rc, flags);
+ }
+
+ log_config_counts(e);
+ update_all_syntax_colors(&e->syntaxes, &e->colors);
+
+ Window *window = new_window(e);
+ e->window = window;
+ e->root_frame = new_root_frame(window);
+
+ set_signal_handlers();
+ set_fatal_error_cleanup_handler(cleanup_handler, e);
+
+ if (load_and_save_history) {
+ file_history_load(&e->file_history, path_join(cfgdir, "file-history"));
+ history_load(&e->command_history, path_join(cfgdir, "command-history"));
+ history_load(&e->search_history, path_join(cfgdir, "search-history"));
+ if (e->search_history.last) {
+ search_set_regexp(&e->search, e->search_history.last->text);
+ }
+ }
+
+ set_print_errors_to_stderr(false);
+
+ // Initialize terminal but don't update screen yet. Also display
+ // "Press any key to continue" prompt if there were any errors
+ // during reading configuration files.
+ if (!term_raw()) {
+ perror("tcsetattr");
+ return EX_IOERR;
+ }
+ if (get_nr_errors()) {
+ any_key(term, e->options.esc_timeout);
+ clear_error();
+ }
+
+ e->status = EDITOR_RUNNING;
+
+ for (size_t i = optind, line = 0, col = 0; i < argc; i++) {
+ const char *str = argv[i];
+ if (line == 0 && *str == '+' && str_to_filepos(str + 1, &line, &col)) {
+ continue;
+ }
+ View *view = window_open_buffer(window, str, false, NULL);
+ if (line == 0) {
+ continue;
+ }
+ set_view(view);
+ move_to_filepos(view, line, col);
+ line = 0;
+ }
+
+ if (std_buffer) {
+ window_add_buffer(window, std_buffer);
+ }
+
+ View *dview = NULL;
+ if (window->views.count == 0) {
+ // Open a default buffer, if none were opened for arguments
+ dview = window_open_empty_buffer(window);
+ BUG_ON(!dview);
+ BUG_ON(window->views.count != 1);
+ BUG_ON(dview != window->views.ptrs[0]);
+ }
+
+ set_view(window->views.ptrs[0]);
+ ui_start(e);
+
+ for (size_t i = 0; i < nr_commands; i++) {
+ handle_normal_command(e, commands[i], false);
+ }
+
+ if (tag) {
+ StringView tag_sv = strview_from_cstring(tag);
+ if (tag_lookup(&e->tagfile, &tag_sv, NULL, &e->messages)) {
+ activate_current_message(e);
+ if (dview && nr_commands == 0 && window->views.count > 1) {
+ // Close default/empty buffer, if `-t` jumped to a tag
+ // and no commands were executed via `-c`
+ remove_view(dview);
+ dview = NULL;
+ }
+ }
+ }
+
+ if (nr_commands > 0 || tag) {
+ normal_update(e);
+ }
+
+ int exit_code = main_loop(e);
+
+ term_restore_title(term);
+ ui_end(e);
+ term_output_flush(&term->obuf);
+ set_print_errors_to_stderr(true);
+
+ // Unlock files and add to file history
+ remove_frame(e, e->root_frame);
+
+ if (load_and_save_history) {
+ history_save(&e->command_history);
+ history_save(&e->search_history);
+ file_history_save(&e->file_history);
+ }
+
+ if (have_stdout_buffer) {
+ int fd = std_fds[STDOUT_FILENO];
+ Block *blk;
+ block_for_each(blk, &std_buffer->blocks) {
+ if (xwrite_all(fd, blk->data, blk->size) < 0) {
+ error_msg_errno("failed to write (stdout) buffer");
+ if (exit_code == EDITOR_EXIT_OK) {
+ exit_code = EX_IOERR;
+ }
+ break;
+ }
+ }
+ free_blocks(std_buffer);
+ free(std_buffer);
+ }
+
+ free_editor_state(e);
+ LOG_INFO("exiting with status %d", exit_code);
+ log_close();
+ return exit_code;
+}
diff --git a/examples/dte/misc.c b/examples/dte/misc.c
new file mode 100644
index 0000000..4ed640b
--- /dev/null
+++ b/examples/dte/misc.c
@@ -0,0 +1,764 @@
+#include <stdlib.h>
+#include <string.h>
+#include "misc.h"
+#include "buffer.h"
+#include "change.h"
+#include "indent.h"
+#include "move.h"
+#include "options.h"
+#include "regexp.h"
+#include "selection.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/string.h"
+#include "util/string-view.h"
+#include "util/utf8.h"
+
+typedef struct {
+ String buf;
+ char *indent;
+ size_t indent_len;
+ size_t indent_width;
+ size_t cur_width;
+ size_t text_width;
+} ParagraphFormatter;
+
+static bool line_has_opening_brace(StringView line)
+{
+ static regex_t re;
+ static bool compiled;
+ if (!compiled) {
+ // TODO: Reimplement without using regex
+ static const char pat[] = "\\{[ \t]*(//.*|/\\*.*\\*/[ \t]*)?$";
+ regexp_compile_or_fatal_error(&re, pat, REG_NEWLINE | REG_NOSUB);
+ compiled = true;
+ }
+
+ regmatch_t m;
+ return regexp_exec(&re, line.data, line.length, 0, &m, 0);
+}
+
+static bool line_has_closing_brace(StringView line)
+{
+ strview_trim_left(&line);
+ return line.length > 0 && line.data[0] == '}';
+}
+
+/*
+ * Stupid { ... } block selector.
+ *
+ * Because braces can be inside strings or comments and writing real
+ * parser for many programming languages does not make sense the rules
+ * for selecting a block are made very simple. Line that matches \{\s*$
+ * starts a block and line that matches ^\s*\} ends it.
+ */
+void select_block(View *view)
+{
+ BlockIter sbi, ebi, bi = view->cursor;
+ StringView line;
+ int level = 0;
+
+ // If current line does not match \{\s*$ but matches ^\s*\} then
+ // cursor is likely at end of the block you want to select
+ fetch_this_line(&bi, &line);
+ if (!line_has_opening_brace(line) && line_has_closing_brace(line)) {
+ block_iter_prev_line(&bi);
+ }
+
+ while (1) {
+ fetch_this_line(&bi, &line);
+ if (line_has_opening_brace(line)) {
+ if (level++ == 0) {
+ sbi = bi;
+ block_iter_next_line(&bi);
+ break;
+ }
+ }
+ if (line_has_closing_brace(line)) {
+ level--;
+ }
+
+ if (!block_iter_prev_line(&bi)) {
+ return;
+ }
+ }
+
+ while (1) {
+ fetch_this_line(&bi, &line);
+ if (line_has_closing_brace(line)) {
+ if (--level == 0) {
+ ebi = bi;
+ break;
+ }
+ }
+ if (line_has_opening_brace(line)) {
+ level++;
+ }
+
+ if (!block_iter_next_line(&bi)) {
+ return;
+ }
+ }
+
+ view->cursor = sbi;
+ view->sel_so = block_iter_get_offset(&ebi);
+ view->sel_eo = SEL_EO_RECALC;
+ view->selection = SELECT_LINES;
+
+ mark_all_lines_changed(view->buffer);
+}
+
+static int get_indent_of_matching_brace(const View *view)
+{
+ const LocalOptions *options = &view->buffer->options;
+ BlockIter bi = view->cursor;
+ StringView line;
+ int level = 0;
+
+ while (block_iter_prev_line(&bi)) {
+ fetch_this_line(&bi, &line);
+ if (line_has_opening_brace(line)) {
+ if (level++ == 0) {
+ return get_indent_width(options, &line);
+ }
+ }
+ if (line_has_closing_brace(line)) {
+ level--;
+ }
+ }
+
+ return -1;
+}
+
+void unselect(View *view)
+{
+ view->select_mode = SELECT_NONE;
+ if (view->selection) {
+ view->selection = SELECT_NONE;
+ mark_all_lines_changed(view->buffer);
+ }
+}
+
+void insert_text(View *view, const char *text, size_t size, bool move_after)
+{
+ size_t del_count = 0;
+ if (view->selection) {
+ del_count = prepare_selection(view);
+ unselect(view);
+ }
+ buffer_replace_bytes(view, del_count, text, size);
+ if (move_after) {
+ block_iter_skip_bytes(&view->cursor, size);
+ }
+}
+
+void delete_ch(View *view)
+{
+ size_t size = 0;
+ if (view->selection) {
+ size = prepare_selection(view);
+ unselect(view);
+ } else {
+ const LocalOptions *options = &view->buffer->options;
+ begin_change(CHANGE_MERGE_DELETE);
+ if (options->emulate_tab) {
+ size = get_indent_level_bytes_right(options, &view->cursor);
+ }
+ if (size == 0) {
+ BlockIter bi = view->cursor;
+ size = block_iter_next_column(&bi);
+ }
+ }
+ buffer_delete_bytes(view, size);
+}
+
+void erase(View *view)
+{
+ size_t size = 0;
+ if (view->selection) {
+ size = prepare_selection(view);
+ unselect(view);
+ } else {
+ const LocalOptions *options = &view->buffer->options;
+ begin_change(CHANGE_MERGE_ERASE);
+ if (options->emulate_tab) {
+ size = get_indent_level_bytes_left(options, &view->cursor);
+ block_iter_back_bytes(&view->cursor, size);
+ }
+ if (size == 0) {
+ CodePoint u;
+ size = block_iter_prev_char(&view->cursor, &u);
+ }
+ }
+ buffer_erase_bytes(view, size);
+}
+
+// Go to beginning of whitespace (tabs and spaces) under cursor and
+// return number of whitespace bytes after cursor after moving cursor
+static size_t goto_beginning_of_whitespace(View *view)
+{
+ BlockIter bi = view->cursor;
+ size_t count = 0;
+ CodePoint u;
+
+ // Count spaces and tabs at or after cursor
+ while (block_iter_next_char(&bi, &u)) {
+ if (u != '\t' && u != ' ') {
+ break;
+ }
+ count++;
+ }
+
+ // Count spaces and tabs before cursor
+ while (block_iter_prev_char(&view->cursor, &u)) {
+ if (u != '\t' && u != ' ') {
+ block_iter_next_char(&view->cursor, &u);
+ break;
+ }
+ count++;
+ }
+ return count;
+}
+
+static bool ws_only(const StringView *line)
+{
+ for (size_t i = 0, n = line->length; i < n; i++) {
+ char ch = line->data[i];
+ if (ch != ' ' && ch != '\t') {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Non-empty line can be used to determine size of indentation for the next line
+static bool find_non_empty_line_bwd(BlockIter *bi)
+{
+ block_iter_bol(bi);
+ do {
+ StringView line;
+ fill_line_ref(bi, &line);
+ if (!ws_only(&line)) {
+ return true;
+ }
+ } while (block_iter_prev_line(bi));
+ return false;
+}
+
+static void insert_nl(View *view)
+{
+ size_t del_count = 0;
+ size_t ins_count = 1;
+ char *ins = NULL;
+
+ // Prepare deleted text (selection or whitespace around cursor)
+ if (view->selection) {
+ del_count = prepare_selection(view);
+ unselect(view);
+ } else {
+ // Trim whitespace around cursor
+ del_count = goto_beginning_of_whitespace(view);
+ }
+
+ // Prepare inserted indentation
+ const LocalOptions *options = &view->buffer->options;
+ if (options->auto_indent) {
+ // Current line will be split at cursor position
+ BlockIter bi = view->cursor;
+ size_t len = block_iter_bol(&bi);
+ StringView line;
+ fill_line_ref(&bi, &line);
+ line.length = len;
+ if (ws_only(&line)) {
+ // This line is (or will become) white space only; find previous,
+ // non whitespace only line
+ if (block_iter_prev_line(&bi) && find_non_empty_line_bwd(&bi)) {
+ fill_line_ref(&bi, &line);
+ ins = get_indent_for_next_line(options, &line);
+ }
+ } else {
+ ins = get_indent_for_next_line(options, &line);
+ }
+ }
+
+ begin_change(CHANGE_MERGE_NONE);
+ if (ins) {
+ // Add newline before indent
+ ins_count = strlen(ins);
+ memmove(ins + 1, ins, ins_count);
+ ins[0] = '\n';
+ ins_count++;
+
+ buffer_replace_bytes(view, del_count, ins, ins_count);
+ free(ins);
+ } else {
+ buffer_replace_bytes(view, del_count, "\n", ins_count);
+ }
+ end_change();
+
+ // Move after inserted text
+ block_iter_skip_bytes(&view->cursor, ins_count);
+}
+
+void insert_ch(View *view, CodePoint ch)
+{
+ if (ch == '\n') {
+ insert_nl(view);
+ return;
+ }
+
+ const Buffer *buffer = view->buffer;
+ const LocalOptions *options = &buffer->options;
+ char buf[8];
+ char *ins = buf;
+ char *alloc = NULL;
+ size_t del_count = 0;
+ size_t ins_count = 0;
+
+ if (view->selection) {
+ // Prepare deleted text (selection)
+ del_count = prepare_selection(view);
+ unselect(view);
+ } else if (options->overwrite) {
+ // Delete character under cursor unless we're at end of line
+ BlockIter bi = view->cursor;
+ del_count = block_iter_is_eol(&bi) ? 0 : block_iter_next_column(&bi);
+ } else if (ch == '}' && options->auto_indent && options->brace_indent) {
+ BlockIter bi = view->cursor;
+ StringView curlr;
+ block_iter_bol(&bi);
+ fill_line_ref(&bi, &curlr);
+ if (ws_only(&curlr)) {
+ int width = get_indent_of_matching_brace(view);
+ if (width >= 0) {
+ // Replace current (ws only) line with some indent + '}'
+ block_iter_bol(&view->cursor);
+ del_count = curlr.length;
+ if (width) {
+ alloc = make_indent(options, width);
+ ins = alloc;
+ ins_count = strlen(ins);
+ // '}' will be replace the terminating NUL
+ }
+ }
+ }
+ }
+
+ // Prepare inserted text
+ if (ch == '\t' && options->expand_tab) {
+ ins_count = options->indent_width;
+ static_assert(sizeof(buf) >= INDENT_WIDTH_MAX);
+ memset(ins, ' ', ins_count);
+ } else {
+ u_set_char_raw(ins, &ins_count, ch);
+ }
+
+ // Record change
+ begin_change(del_count ? CHANGE_MERGE_NONE : CHANGE_MERGE_INSERT);
+ buffer_replace_bytes(view, del_count, ins, ins_count);
+ end_change();
+ free(alloc);
+
+ // Move after inserted text
+ block_iter_skip_bytes(&view->cursor, ins_count);
+}
+
+static void join_selection(View *view)
+{
+ size_t count = prepare_selection(view);
+ size_t len = 0, join = 0;
+ BlockIter bi;
+ CodePoint ch = 0;
+
+ unselect(view);
+ bi = view->cursor;
+
+ begin_change_chain();
+ while (count > 0) {
+ if (!len) {
+ view->cursor = bi;
+ }
+
+ count -= block_iter_next_char(&bi, &ch);
+ if (ch == '\t' || ch == ' ') {
+ len++;
+ } else if (ch == '\n') {
+ len++;
+ join++;
+ } else {
+ if (join) {
+ buffer_replace_bytes(view, len, " ", 1);
+ // Skip the space we inserted and the char we read last
+ block_iter_next_char(&view->cursor, &ch);
+ block_iter_next_char(&view->cursor, &ch);
+ bi = view->cursor;
+ }
+ len = 0;
+ join = 0;
+ }
+ }
+
+ // Don't replace last \n that is at end of the selection
+ if (join && ch == '\n') {
+ join--;
+ len--;
+ }
+
+ if (join) {
+ if (ch == '\n') {
+ // Don't add space to end of line
+ buffer_delete_bytes(view, len);
+ } else {
+ buffer_replace_bytes(view, len, " ", 1);
+ }
+ }
+ end_change_chain(view);
+}
+
+void join_lines(View *view)
+{
+ BlockIter bi = view->cursor;
+
+ if (view->selection) {
+ join_selection(view);
+ return;
+ }
+
+ if (!block_iter_next_line(&bi)) {
+ return;
+ }
+ if (block_iter_is_eof(&bi)) {
+ return;
+ }
+
+ BlockIter next = bi;
+ CodePoint u;
+ size_t count = 1;
+ block_iter_prev_char(&bi, &u);
+ while (block_iter_prev_char(&bi, &u)) {
+ if (u != '\t' && u != ' ') {
+ block_iter_next_char(&bi, &u);
+ break;
+ }
+ count++;
+ }
+ while (block_iter_next_char(&next, &u)) {
+ if (u != '\t' && u != ' ') {
+ break;
+ }
+ count++;
+ }
+
+ view->cursor = bi;
+ if (u == '\n') {
+ buffer_delete_bytes(view, count);
+ } else {
+ buffer_replace_bytes(view, count, " ", 1);
+ }
+}
+
+void clear_lines(View *view, bool auto_indent)
+{
+ char *indent = NULL;
+ if (auto_indent) {
+ BlockIter bi = view->cursor;
+ if (block_iter_prev_line(&bi) && find_non_empty_line_bwd(&bi)) {
+ StringView line;
+ fill_line_ref(&bi, &line);
+ indent = get_indent_for_next_line(&view->buffer->options, &line);
+ }
+ }
+
+ size_t del_count = 0;
+ if (view->selection) {
+ view->selection = SELECT_LINES;
+ del_count = prepare_selection(view);
+ unselect(view);
+ // Don't delete last newline
+ if (del_count) {
+ del_count--;
+ }
+ } else {
+ block_iter_eol(&view->cursor);
+ del_count = block_iter_bol(&view->cursor);
+ }
+
+ if (!indent && !del_count) {
+ return;
+ }
+
+ size_t ins_count = indent ? strlen(indent) : 0;
+ buffer_replace_bytes(view, del_count, indent, ins_count);
+ free(indent);
+ block_iter_skip_bytes(&view->cursor, ins_count);
+}
+
+void delete_lines(View *view)
+{
+ long x = view_get_preferred_x(view);
+ size_t del_count;
+ if (view->selection) {
+ view->selection = SELECT_LINES;
+ del_count = prepare_selection(view);
+ unselect(view);
+ } else {
+ block_iter_bol(&view->cursor);
+ BlockIter tmp = view->cursor;
+ del_count = block_iter_eat_line(&tmp);
+ }
+ buffer_delete_bytes(view, del_count);
+ move_to_preferred_x(view, x);
+}
+
+void new_line(View *view, bool above)
+{
+ if (above && block_iter_prev_line(&view->cursor) == 0) {
+ // Already on first line; insert newline at bof
+ block_iter_bol(&view->cursor);
+ buffer_insert_bytes(view, "\n", 1);
+ return;
+ }
+
+ const LocalOptions *options = &view->buffer->options;
+ char *ins = NULL;
+ block_iter_eol(&view->cursor);
+
+ if (options->auto_indent) {
+ BlockIter bi = view->cursor;
+ if (find_non_empty_line_bwd(&bi)) {
+ StringView line;
+ fill_line_ref(&bi, &line);
+ ins = get_indent_for_next_line(options, &line);
+ }
+ }
+
+ size_t ins_count;
+ if (ins) {
+ ins_count = strlen(ins);
+ memmove(ins + 1, ins, ins_count);
+ ins[0] = '\n';
+ ins_count++;
+ buffer_insert_bytes(view, ins, ins_count);
+ free(ins);
+ } else {
+ ins_count = 1;
+ buffer_insert_bytes(view, "\n", 1);
+ }
+
+ block_iter_skip_bytes(&view->cursor, ins_count);
+}
+
+static void add_word(ParagraphFormatter *pf, const char *word, size_t len)
+{
+ size_t i = 0;
+ size_t word_width = 0;
+ while (i < len) {
+ word_width += u_char_width(u_get_char(word, len, &i));
+ }
+
+ if (pf->cur_width && pf->cur_width + 1 + word_width > pf->text_width) {
+ string_append_byte(&pf->buf, '\n');
+ pf->cur_width = 0;
+ }
+
+ if (pf->cur_width == 0) {
+ if (pf->indent_len) {
+ string_append_buf(&pf->buf, pf->indent, pf->indent_len);
+ }
+ pf->cur_width = pf->indent_width;
+ } else {
+ string_append_byte(&pf->buf, ' ');
+ pf->cur_width++;
+ }
+
+ string_append_buf(&pf->buf, word, len);
+ pf->cur_width += word_width;
+}
+
+static bool is_paragraph_separator(const StringView *line)
+{
+ StringView trimmed = *line;
+ strview_trim(&trimmed);
+
+ return
+ trimmed.length == 0
+ // TODO: make this configurable
+ || strview_equal_cstring(&trimmed, "/*")
+ || strview_equal_cstring(&trimmed, "*/")
+ ;
+}
+
+static bool in_paragraph(const LocalOptions *options, const StringView *line, size_t indent_width)
+{
+ if (get_indent_width(options, line) != indent_width) {
+ return false;
+ }
+ return !is_paragraph_separator(line);
+}
+
+static size_t paragraph_size(View *view)
+{
+ const LocalOptions *options = &view->buffer->options;
+ BlockIter bi = view->cursor;
+ StringView line;
+ block_iter_bol(&bi);
+ fill_line_ref(&bi, &line);
+ if (is_paragraph_separator(&line)) {
+ // Not in paragraph
+ return 0;
+ }
+ size_t indent_width = get_indent_width(options, &line);
+
+ // Go to beginning of paragraph
+ while (block_iter_prev_line(&bi)) {
+ fill_line_ref(&bi, &line);
+ if (!in_paragraph(options, &line, indent_width)) {
+ block_iter_eat_line(&bi);
+ break;
+ }
+ }
+ view->cursor = bi;
+
+ // Get size of paragraph
+ size_t size = 0;
+ do {
+ size_t bytes = block_iter_eat_line(&bi);
+ if (!bytes) {
+ break;
+ }
+ size += bytes;
+ fill_line_ref(&bi, &line);
+ } while (in_paragraph(options, &line, indent_width));
+ return size;
+}
+
+void format_paragraph(View *view, size_t text_width)
+{
+ size_t len;
+ if (view->selection) {
+ view->selection = SELECT_LINES;
+ len = prepare_selection(view);
+ } else {
+ len = paragraph_size(view);
+ }
+ if (!len) {
+ return;
+ }
+
+ const LocalOptions *options = &view->buffer->options;
+ char *sel = block_iter_get_bytes(&view->cursor, len);
+ StringView sv = string_view(sel, len);
+ size_t indent_width = get_indent_width(options, &sv);
+ char *indent = make_indent(options, indent_width);
+
+ ParagraphFormatter pf = {
+ .buf = STRING_INIT,
+ .indent = indent,
+ .indent_len = indent ? strlen(indent) : 0,
+ .indent_width = indent_width,
+ .cur_width = 0,
+ .text_width = text_width
+ };
+
+ for (size_t i = 0; true; ) {
+ while (i < len) {
+ size_t tmp = i;
+ if (!u_is_breakable_whitespace(u_get_char(sel, len, &tmp))) {
+ break;
+ }
+ i = tmp;
+ }
+ if (i == len) {
+ break;
+ }
+
+ size_t start = i;
+ while (i < len) {
+ size_t tmp = i;
+ if (u_is_breakable_whitespace(u_get_char(sel, len, &tmp))) {
+ break;
+ }
+ i = tmp;
+ }
+
+ add_word(&pf, sel + start, i - start);
+ }
+
+ if (pf.buf.len) {
+ string_append_byte(&pf.buf, '\n');
+ }
+ buffer_replace_bytes(view, len, pf.buf.buffer, pf.buf.len);
+ if (pf.buf.len) {
+ block_iter_skip_bytes(&view->cursor, pf.buf.len - 1);
+ }
+ string_free(&pf.buf);
+ free(pf.indent);
+ free(sel);
+
+ unselect(view);
+}
+
+void change_case(View *view, char mode)
+{
+ bool was_selecting = false;
+ bool move = true;
+ size_t text_len;
+ if (view->selection) {
+ SelectionInfo info;
+ init_selection(view, &info);
+ view->cursor = info.si;
+ text_len = info.eo - info.so;
+ unselect(view);
+ was_selecting = true;
+ move = !info.swapped;
+ } else {
+ CodePoint u;
+ if (!block_iter_get_char(&view->cursor, &u)) {
+ return;
+ }
+ text_len = u_char_size(u);
+ }
+
+ String dst = string_new(text_len);
+ char *src = block_iter_get_bytes(&view->cursor, text_len);
+ size_t i = 0;
+ switch (mode) {
+ case 'l':
+ while (i < text_len) {
+ CodePoint u = u_to_lower(u_get_char(src, text_len, &i));
+ string_append_codepoint(&dst, u);
+ }
+ break;
+ case 'u':
+ while (i < text_len) {
+ CodePoint u = u_to_upper(u_get_char(src, text_len, &i));
+ string_append_codepoint(&dst, u);
+ }
+ break;
+ case 't':
+ while (i < text_len) {
+ CodePoint u = u_get_char(src, text_len, &i);
+ u = u_is_upper(u) ? u_to_lower(u) : u_to_upper(u);
+ string_append_codepoint(&dst, u);
+ }
+ break;
+ default:
+ BUG("unhandled case mode");
+ }
+
+ buffer_replace_bytes(view, text_len, dst.buffer, dst.len);
+ free(src);
+
+ if (move && dst.len > 0) {
+ if (was_selecting) {
+ // Move cursor back to where it was
+ size_t idx = dst.len;
+ u_prev_char(dst.buffer, &idx);
+ block_iter_skip_bytes(&view->cursor, idx);
+ } else {
+ block_iter_skip_bytes(&view->cursor, dst.len);
+ }
+ }
+
+ string_free(&dst);
+}
diff --git a/examples/dte/misc.h b/examples/dte/misc.h
new file mode 100644
index 0000000..055748f
--- /dev/null
+++ b/examples/dte/misc.h
@@ -0,0 +1,22 @@
+#ifndef MISC_H
+#define MISC_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/unicode.h"
+#include "view.h"
+
+void select_block(View *view);
+void unselect(View *view);
+void insert_text(View *view, const char *text, size_t size, bool move_after);
+void delete_ch(View *view);
+void erase(View *view);
+void insert_ch(View *view, CodePoint ch);
+void join_lines(View *view);
+void clear_lines(View *view, bool auto_indent);
+void delete_lines(View *view);
+void new_line(View *view, bool above);
+void format_paragraph(View *view, size_t text_width);
+void change_case(View *view, char mode);
+
+#endif
diff --git a/examples/dte/mode.c b/examples/dte/mode.c
new file mode 100644
index 0000000..fe90b6a
--- /dev/null
+++ b/examples/dte/mode.c
@@ -0,0 +1,74 @@
+#include "mode.h"
+#include "bind.h"
+#include "change.h"
+#include "cmdline.h"
+#include "command/macro.h"
+#include "completion.h"
+#include "misc.h"
+#include "shift.h"
+#include "terminal/input.h"
+#include "util/debug.h"
+#include "util/unicode.h"
+#include "view.h"
+
+static bool normal_mode_keypress(EditorState *e, KeyCode key)
+{
+ View *view = e->view;
+ KeyCode shift = key & MOD_SHIFT;
+ if ((key & ~shift) == KEY_TAB && view->selection == SELECT_LINES) {
+ // In line selections, Tab/S-Tab behave like `shift -- 1/-1`
+ shift_lines(view, shift ? -1 : 1);
+ return true;
+ }
+
+ if (u_is_unicode(key)) {
+ insert_ch(view, key);
+ macro_insert_char_hook(&e->macro, key);
+ return true;
+ }
+
+ return handle_binding(e, INPUT_NORMAL, key);
+}
+
+static bool insert_paste(EditorState *e, bool bracketed)
+{
+ String str = term_read_paste(&e->terminal.ibuf, bracketed);
+ if (e->input_mode == INPUT_NORMAL) {
+ begin_change(CHANGE_MERGE_NONE);
+ insert_text(e->view, str.buffer, str.len, true);
+ end_change();
+ macro_insert_text_hook(&e->macro, str.buffer, str.len);
+ } else {
+ CommandLine *c = &e->cmdline;
+ string_replace_byte(&str, '\n', ' ');
+ string_insert_buf(&c->buf, c->pos, str.buffer, str.len);
+ c->pos += str.len;
+ c->search_pos = NULL;
+ }
+ string_free(&str);
+ return true;
+}
+
+bool handle_input(EditorState *e, KeyCode key)
+{
+ if (key == KEY_DETECTED_PASTE || key == KEY_BRACKETED_PASTE) {
+ return insert_paste(e, key == KEY_BRACKETED_PASTE);
+ }
+
+ InputMode mode = e->input_mode;
+ if (mode == INPUT_NORMAL) {
+ return normal_mode_keypress(e, key);
+ }
+
+ BUG_ON(!(mode == INPUT_COMMAND || mode == INPUT_SEARCH));
+ if (!u_is_unicode(key) || key == KEY_TAB || key == KEY_ENTER) {
+ return handle_binding(e, mode, key);
+ }
+
+ CommandLine *c = &e->cmdline;
+ c->pos += string_insert_codepoint(&c->buf, c->pos, key);
+ if (mode == INPUT_COMMAND) {
+ reset_completion(c);
+ }
+ return true;
+}
diff --git a/examples/dte/mode.h b/examples/dte/mode.h
new file mode 100644
index 0000000..40d4a6b
--- /dev/null
+++ b/examples/dte/mode.h
@@ -0,0 +1,11 @@
+#ifndef MODE_H
+#define MODE_H
+
+#include <stdbool.h>
+#include "editor.h"
+#include "terminal/key.h"
+#include "util/macros.h"
+
+bool handle_input(EditorState *e, KeyCode key) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/move.c b/examples/dte/move.c
new file mode 100644
index 0000000..72414b3
--- /dev/null
+++ b/examples/dte/move.c
@@ -0,0 +1,311 @@
+#include "move.h"
+#include "buffer.h"
+#include "indent.h"
+#include "util/ascii.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/utf8.h"
+
+typedef enum {
+ CT_SPACE,
+ CT_NEWLINE,
+ CT_WORD,
+ CT_OTHER,
+} CharTypeEnum;
+
+void move_to_preferred_x(View *view, long preferred_x)
+{
+ const LocalOptions *options = &view->buffer->options;
+ StringView line;
+ view->preferred_x = preferred_x;
+ block_iter_bol(&view->cursor);
+ fill_line_ref(&view->cursor, &line);
+
+ if (options->emulate_tab && view->preferred_x < line.length) {
+ const size_t iw = options->indent_width;
+ const size_t ilevel = indent_level(view->preferred_x, iw);
+ for (size_t i = 0; i < line.length && line.data[i] == ' '; i++) {
+ if (i + 1 == (ilevel + 1) * iw) {
+ // Force cursor to beginning of the indentation level
+ view->cursor.offset += ilevel * iw;
+ return;
+ }
+ }
+ }
+
+ const unsigned int tw = options->tab_width;
+ unsigned long x = 0;
+ size_t i = 0;
+ while (x < view->preferred_x && i < line.length) {
+ CodePoint u = line.data[i++];
+ if (likely(u < 0x80)) {
+ if (likely(!ascii_iscntrl(u))) {
+ x++;
+ } else if (u == '\t') {
+ x = next_indent_width(x, tw);
+ } else if (u == '\n') {
+ break;
+ } else {
+ x += 2;
+ }
+ } else {
+ const size_t next = i;
+ i--;
+ u = u_get_nonascii(line.data, line.length, &i);
+ x += u_char_width(u);
+ if (x > view->preferred_x) {
+ i = next;
+ break;
+ }
+ }
+ }
+ if (x > view->preferred_x) {
+ i--;
+ }
+ view->cursor.offset += i;
+
+ // If cursor stopped on a zero-width char, move to the next spacing char
+ CodePoint u;
+ if (block_iter_get_char(&view->cursor, &u) && u_is_zero_width(u)) {
+ block_iter_next_column(&view->cursor);
+ }
+}
+
+void move_cursor_left(View *view)
+{
+ const LocalOptions *options = &view->buffer->options;
+ if (options->emulate_tab) {
+ size_t size = get_indent_level_bytes_left(options, &view->cursor);
+ if (size) {
+ block_iter_back_bytes(&view->cursor, size);
+ view_reset_preferred_x(view);
+ return;
+ }
+ }
+ block_iter_prev_column(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_cursor_right(View *view)
+{
+ const LocalOptions *options = &view->buffer->options;
+ if (options->emulate_tab) {
+ size_t size = get_indent_level_bytes_right(options, &view->cursor);
+ if (size) {
+ block_iter_skip_bytes(&view->cursor, size);
+ view_reset_preferred_x(view);
+ return;
+ }
+ }
+ block_iter_next_column(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_bol(View *view)
+{
+ block_iter_bol(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_bol_smart(View *view, SmartBolFlags flags)
+{
+ if (flags == 0) {
+ move_bol(view);
+ return;
+ }
+
+ BUG_ON(!(flags & BOL_SMART));
+ bool fwd = false;
+ StringView line;
+ size_t cursor_offset = fetch_this_line(&view->cursor, &line);
+
+ if (cursor_offset == 0) {
+ // Already at bol
+ if (!(flags & BOL_SMART_TOGGLE)) {
+ goto out;
+ }
+ fwd = true;
+ }
+
+ size_t indent = ascii_blank_prefix_length(line.data, line.length);
+ if (fwd) {
+ block_iter_skip_bytes(&view->cursor, indent);
+ } else {
+ size_t co = cursor_offset;
+ size_t move = (co > indent) ? co - indent : co;
+ block_iter_back_bytes(&view->cursor, move);
+ }
+
+out:
+ view_reset_preferred_x(view);
+}
+
+void move_eol(View *view)
+{
+ block_iter_eol(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_up(View *view, long count)
+{
+ const long x = view_get_preferred_x(view);
+ while (count > 0) {
+ if (!block_iter_prev_line(&view->cursor)) {
+ break;
+ }
+ count--;
+ }
+ move_to_preferred_x(view, x);
+}
+
+void move_down(View *view, long count)
+{
+ const long x = view_get_preferred_x(view);
+ while (count > 0) {
+ if (!block_iter_eat_line(&view->cursor)) {
+ break;
+ }
+ count--;
+ }
+ move_to_preferred_x(view, x);
+}
+
+void move_bof(View *view)
+{
+ block_iter_bof(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_eof(View *view)
+{
+ block_iter_eof(&view->cursor);
+ view_reset_preferred_x(view);
+}
+
+void move_to_line(View *view, size_t line)
+{
+ BUG_ON(line == 0);
+ view->center_on_scroll = true;
+ block_iter_goto_line(&view->cursor, line - 1);
+}
+
+void move_to_column(View *view, size_t column)
+{
+ BUG_ON(column == 0);
+ block_iter_bol(&view->cursor);
+ while (column-- > 1) {
+ CodePoint u;
+ if (!block_iter_next_char(&view->cursor, &u)) {
+ break;
+ }
+ if (u == '\n') {
+ block_iter_prev_char(&view->cursor, &u);
+ break;
+ }
+ }
+ view_reset_preferred_x(view);
+}
+
+void move_to_filepos(View *view, size_t line, size_t column)
+{
+ move_to_line(view, line);
+ BUG_ON(!block_iter_is_bol(&view->cursor));
+ if (column != 1) {
+ move_to_column(view, column);
+ }
+ view_reset_preferred_x(view);
+}
+
+static CharTypeEnum get_char_type(CodePoint u)
+{
+ if (u == '\n') {
+ return CT_NEWLINE;
+ }
+ if (u_is_breakable_whitespace(u)) {
+ return CT_SPACE;
+ }
+ if (u_is_word_char(u)) {
+ return CT_WORD;
+ }
+ return CT_OTHER;
+}
+
+static bool get_current_char_type(BlockIter *bi, CharTypeEnum *type)
+{
+ CodePoint u;
+ if (!block_iter_get_char(bi, &u)) {
+ return false;
+ }
+
+ *type = get_char_type(u);
+ return true;
+}
+
+static size_t skip_fwd_char_type(BlockIter *bi, CharTypeEnum type)
+{
+ size_t count = 0;
+ CodePoint u;
+ while (block_iter_next_char(bi, &u)) {
+ if (get_char_type(u) != type) {
+ block_iter_prev_char(bi, &u);
+ break;
+ }
+ count += u_char_size(u);
+ }
+ return count;
+}
+
+static size_t skip_bwd_char_type(BlockIter *bi, CharTypeEnum type)
+{
+ size_t count = 0;
+ CodePoint u;
+ while (block_iter_prev_char(bi, &u)) {
+ if (get_char_type(u) != type) {
+ block_iter_next_char(bi, &u);
+ break;
+ }
+ count += u_char_size(u);
+ }
+ return count;
+}
+
+size_t word_fwd(BlockIter *bi, bool skip_non_word)
+{
+ size_t count = 0;
+ CharTypeEnum type;
+
+ while (1) {
+ count += skip_fwd_char_type(bi, CT_SPACE);
+ if (!get_current_char_type(bi, &type)) {
+ return count;
+ }
+
+ if (
+ count
+ && (!skip_non_word || (type == CT_WORD || type == CT_NEWLINE))
+ ) {
+ return count;
+ }
+
+ count += skip_fwd_char_type(bi, type);
+ }
+}
+
+size_t word_bwd(BlockIter *bi, bool skip_non_word)
+{
+ size_t count = 0;
+ CharTypeEnum type;
+ CodePoint u;
+
+ do {
+ count += skip_bwd_char_type(bi, CT_SPACE);
+ if (!block_iter_prev_char(bi, &u)) {
+ return count;
+ }
+
+ type = get_char_type(u);
+ count += u_char_size(u);
+ count += skip_bwd_char_type(bi, type);
+ } while (skip_non_word && type != CT_WORD && type != CT_NEWLINE);
+ return count;
+}
diff --git a/examples/dte/move.h b/examples/dte/move.h
new file mode 100644
index 0000000..412a033
--- /dev/null
+++ b/examples/dte/move.h
@@ -0,0 +1,31 @@
+#ifndef MOVE_H
+#define MOVE_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "block-iter.h"
+#include "view.h"
+
+typedef enum {
+ BOL_SMART = 0x1, // Move to end of indent, before moving to bol (left moves only)
+ BOL_SMART_TOGGLE = 0x2, // Move to end of indent, if at bol (can move right)
+} SmartBolFlags;
+
+void move_to_preferred_x(View *view, long preferred_x);
+void move_cursor_left(View *view);
+void move_cursor_right(View *view);
+void move_bol(View *view);
+void move_bol_smart(View *view, SmartBolFlags flags);
+void move_eol(View *view);
+void move_up(View *view, long count);
+void move_down(View *view, long count);
+void move_bof(View *view);
+void move_eof(View *view);
+void move_to_line(View *view, size_t line);
+void move_to_column(View *view, size_t column);
+void move_to_filepos(View *view, size_t line, size_t column);
+
+size_t word_fwd(BlockIter *bi, bool skip_non_word);
+size_t word_bwd(BlockIter *bi, bool skip_non_word);
+
+#endif
diff --git a/examples/dte/msg.c b/examples/dte/msg.c
new file mode 100644
index 0000000..203347f
--- /dev/null
+++ b/examples/dte/msg.c
@@ -0,0 +1,139 @@
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "msg.h"
+#include "editor.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/numtostr.h"
+#include "util/path.h"
+#include "util/xmalloc.h"
+
+static void free_message(Message *m)
+{
+ if (m->loc) {
+ file_location_free(m->loc);
+ }
+ free(m);
+}
+
+Message *new_message(const char *msg, size_t len)
+{
+ Message *m = xmalloc(sizeof(*m) + len + 1);
+ m->loc = NULL;
+ if (len) {
+ memcpy(m->msg, msg, len);
+ }
+ m->msg[len] = '\0';
+ return m;
+}
+
+void add_message(MessageArray *msgs, Message *m)
+{
+ ptr_array_append(&msgs->array, m);
+}
+
+bool activate_current_message(EditorState *e)
+{
+ const MessageArray *msgs = &e->messages;
+ size_t count = msgs->array.count;
+ if (count == 0) {
+ return true;
+ }
+
+ size_t pos = msgs->pos;
+ BUG_ON(pos >= count);
+ const Message *m = msgs->array.ptrs[pos];
+ const FileLocation *loc = m->loc;
+ if (loc && loc->filename && !file_location_go(e->window, loc)) {
+ // Failed to jump to location; error message is visible
+ return false;
+ }
+
+ if (count == 1) {
+ info_msg("%s", m->msg);
+ } else {
+ info_msg("[%zu/%zu] %s", pos + 1, count, m->msg);
+ }
+
+ return true;
+}
+
+bool activate_current_message_save(EditorState *e)
+{
+ const View *view = e->view;
+ const BlockIter save = view->cursor;
+ FileLocation *loc = get_current_file_location(view);
+ bool ok = activate_current_message(e);
+
+ // Save position if file changed or cursor moved
+ view = e->view;
+ if (view->cursor.blk != save.blk || view->cursor.offset != save.offset) {
+ bookmark_push(&e->bookmarks, loc);
+ } else {
+ file_location_free(loc);
+ }
+
+ return ok;
+}
+
+void clear_messages(MessageArray *msgs)
+{
+ msgs->pos = 0;
+ ptr_array_free_cb(&msgs->array, FREE_FUNC(free_message));
+}
+
+String dump_messages(const MessageArray *messages)
+{
+ String buf = string_new(4096);
+ char cwd[8192];
+ if (unlikely(!getcwd(cwd, sizeof cwd))) {
+ return buf;
+ }
+
+ for (size_t i = 0, n = messages->array.count; i < n; i++) {
+ char *ptr = string_reserve_space(&buf, DECIMAL_STR_MAX(i));
+ buf.len += buf_umax_to_str(i + 1, ptr);
+ string_append_literal(&buf, ": ");
+
+ const Message *m = messages->array.ptrs[i];
+ const FileLocation *loc = m->loc;
+ if (!loc || !loc->filename) {
+ goto append_msg;
+ }
+
+ if (path_is_absolute(loc->filename)) {
+ char *rel = path_relative(loc->filename, cwd);
+ string_append_cstring(&buf, rel);
+ free(rel);
+ } else {
+ string_append_cstring(&buf, loc->filename);
+ }
+
+ string_append_byte(&buf, ':');
+
+ if (loc->pattern) {
+ string_append_literal(&buf, " /");
+ string_append_cstring(&buf, loc->pattern);
+ string_append_literal(&buf, "/\n");
+ continue;
+ }
+
+ if (loc->line != 0) {
+ string_append_cstring(&buf, ulong_to_str(loc->line));
+ string_append_byte(&buf, ':');
+ if (loc->column != 0) {
+ string_append_cstring(&buf, ulong_to_str(loc->column));
+ string_append_byte(&buf, ':');
+ }
+ }
+
+ string_append_literal(&buf, " ");
+
+ append_msg:
+ string_append_cstring(&buf, m->msg);
+ string_append_byte(&buf, '\n');
+ }
+
+ return buf;
+}
diff --git a/examples/dte/msg.h b/examples/dte/msg.h
new file mode 100644
index 0000000..b23fa25
--- /dev/null
+++ b/examples/dte/msg.h
@@ -0,0 +1,30 @@
+#ifndef MSG_H
+#define MSG_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "bookmark.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string.h"
+
+typedef struct {
+ FileLocation *loc;
+ char msg[];
+} Message;
+
+typedef struct {
+ PointerArray array;
+ size_t pos;
+} MessageArray;
+
+struct EditorState;
+
+Message *new_message(const char *msg, size_t len) RETURNS_NONNULL;
+void add_message(MessageArray *msgs, Message *m) NONNULL_ARGS;
+bool activate_current_message(struct EditorState *e) NONNULL_ARGS;
+bool activate_current_message_save(struct EditorState *e) NONNULL_ARGS;
+void clear_messages(MessageArray *msgs) NONNULL_ARGS;
+String dump_messages(const MessageArray *messages) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/options.c b/examples/dte/options.c
new file mode 100644
index 0000000..2fd4190
--- /dev/null
+++ b/examples/dte/options.c
@@ -0,0 +1,987 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "options.h"
+#include "buffer.h"
+#include "command/serialize.h"
+#include "editor.h"
+#include "error.h"
+#include "file-option.h"
+#include "filetype.h"
+#include "screen.h"
+#include "status.h"
+#include "terminal/output.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/numtostr.h"
+#include "util/str-util.h"
+#include "util/string-view.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+
+typedef enum {
+ OPT_STR,
+ OPT_UINT,
+ OPT_ENUM,
+ OPT_BOOL,
+ OPT_FLAG,
+ OPT_REGEX,
+} OptionType;
+
+typedef union {
+ const char *str_val; // OPT_STR, OPT_REGEX
+ unsigned int uint_val; // OPT_UINT, OPT_ENUM, OPT_FLAG
+ bool bool_val; // OPT_BOOL
+} OptionValue;
+
+typedef union {
+ struct {bool (*validate)(const char *value);} str_opt; // OPT_STR (optional)
+ struct {unsigned int min, max;} uint_opt; // OPT_UINT
+ struct {const char *const *values;} enum_opt; // OPT_ENUM, OPT_FLAG, OPT_BOOL
+} OptionConstraint;
+
+typedef struct {
+ const char name[22];
+ bool local;
+ bool global;
+ unsigned int offset;
+ OptionType type;
+ OptionConstraint u;
+ void (*on_change)(EditorState *e, bool global); // Optional
+} OptionDesc;
+
+#define STR_OPT(_name, OLG, _validate, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_STR, \
+ .u = {.str_opt = {.validate = _validate}}, \
+ .on_change = _on_change, \
+}
+
+#define UINT_OPT(_name, OLG, _min, _max, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_UINT, \
+ .u = {.uint_opt = { \
+ .min = _min, \
+ .max = _max, \
+ }}, \
+ .on_change = _on_change, \
+}
+
+#define ENUM_OPT(_name, OLG, _values, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_ENUM, \
+ .u = {.enum_opt = {.values = _values}}, \
+ .on_change = _on_change, \
+}
+
+#define FLAG_OPT(_name, OLG, _values, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_FLAG, \
+ .u = {.enum_opt = {.values = _values}}, \
+ .on_change = _on_change, \
+}
+
+#define BOOL_OPT(_name, OLG, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_BOOL, \
+ .u = {.enum_opt = {.values = bool_enum}}, \
+ .on_change = _on_change, \
+}
+
+#define REGEX_OPT(_name, OLG, _on_change) { \
+ OLG \
+ .name = _name, \
+ .type = OPT_REGEX, \
+ .on_change = _on_change, \
+}
+
+#define OLG(_offset, _local, _global) \
+ .offset = _offset, \
+ .local = _local, \
+ .global = _global, \
+
+#define L(member) OLG(offsetof(LocalOptions, member), true, false)
+#define G(member) OLG(offsetof(GlobalOptions, member), false, true)
+#define C(member) OLG(offsetof(CommonOptions, member), true, true)
+
+static void filetype_changed(EditorState *e, bool global)
+{
+ BUG_ON(!e->buffer);
+ BUG_ON(global);
+ set_file_options(e, e->buffer);
+ buffer_update_syntax(e, e->buffer);
+}
+
+static void set_window_title_changed(EditorState *e, bool global)
+{
+ BUG_ON(!global);
+ Terminal *term = &e->terminal;
+ if (e->options.set_window_title) {
+ if (e->status == EDITOR_RUNNING) {
+ update_term_title(term, e->buffer, e->options.set_window_title);
+ }
+ } else {
+ term_restore_title(term);
+ term_save_title(term);
+ }
+}
+
+static void syntax_changed(EditorState *e, bool global)
+{
+ if (e->buffer && !global) {
+ buffer_update_syntax(e, e->buffer);
+ }
+}
+
+static void overwrite_changed(EditorState *e, bool global)
+{
+ if (!global) {
+ e->cursor_style_changed = true;
+ }
+}
+
+static void redraw_buffer(EditorState *e, bool global)
+{
+ if (e->buffer && !global) {
+ mark_all_lines_changed(e->buffer);
+ }
+}
+
+static void redraw_screen(EditorState *e, bool global)
+{
+ BUG_ON(!global);
+ mark_everything_changed(e);
+}
+
+static bool validate_statusline_format(const char *value)
+{
+ size_t errpos = statusline_format_find_error(value);
+ if (likely(errpos == 0)) {
+ return true;
+ }
+ char ch = value[errpos];
+ if (ch == '\0') {
+ return error_msg("Format character expected after '%%'");
+ }
+ return error_msg("Invalid format character '%c'", ch);
+}
+
+static bool validate_filetype(const char *value)
+{
+ if (!is_valid_filetype_name(value)) {
+ return error_msg("Invalid filetype name '%s'", value);
+ }
+ return true;
+}
+
+static OptionValue str_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
+{
+ const char *const *strp = ptr;
+ return (OptionValue){.str_val = *strp};
+}
+
+static void str_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ const char **strp = ptr;
+ *strp = str_intern(v.str_val);
+}
+
+static bool str_parse(const OptionDesc *d, const char *str, OptionValue *v)
+{
+ bool valid = !d->u.str_opt.validate || d->u.str_opt.validate(str);
+ v->str_val = valid ? str : NULL;
+ return valid;
+}
+
+static const char *str_string(const OptionDesc* UNUSED_ARG(d), OptionValue v)
+{
+ const char *s = v.str_val;
+ return s ? s : "";
+}
+
+static bool str_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ const char **strp = ptr;
+ return xstreq(*strp, v.str_val);
+}
+
+static OptionValue re_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
+{
+ const InternedRegexp *const *irp = ptr;
+ return (OptionValue){.str_val = *irp ? (*irp)->str : NULL};
+}
+
+static void re_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ const InternedRegexp **irp = ptr;
+ *irp = v.str_val ? regexp_intern(v.str_val) : NULL;
+}
+
+static bool re_parse(const OptionDesc* UNUSED_ARG(d), const char *str, OptionValue *v)
+{
+ if (str[0] == '\0') {
+ v->str_val = NULL;
+ return true;
+ }
+
+ bool valid = regexp_is_interned(str) || regexp_is_valid(str, REG_NEWLINE);
+ v->str_val = valid ? str : NULL;
+ return valid;
+}
+
+static bool re_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ const InternedRegexp **irp = ptr;
+ return *irp ? xstreq((*irp)->str, v.str_val) : !v.str_val;
+}
+
+static OptionValue uint_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
+{
+ const unsigned int *valp = ptr;
+ return (OptionValue){.uint_val = *valp};
+}
+
+static void uint_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ unsigned int *valp = ptr;
+ *valp = v.uint_val;
+}
+
+static bool uint_parse(const OptionDesc *d, const char *str, OptionValue *v)
+{
+ unsigned int val;
+ if (!str_to_uint(str, &val)) {
+ return error_msg("Integer value for %s expected", d->name);
+ }
+
+ const unsigned int min = d->u.uint_opt.min;
+ const unsigned int max = d->u.uint_opt.max;
+ if (val < min || val > max) {
+ return error_msg("Value for %s must be in %u-%u range", d->name, min, max);
+ }
+
+ v->uint_val = val;
+ return true;
+}
+
+static const char *uint_string(const OptionDesc* UNUSED_ARG(desc), OptionValue value)
+{
+ return uint_to_str(value.uint_val);
+}
+
+static bool uint_equals(const OptionDesc* UNUSED_ARG(desc), void *ptr, OptionValue value)
+{
+ const unsigned int *valp = ptr;
+ return *valp == value.uint_val;
+}
+
+static OptionValue bool_get(const OptionDesc* UNUSED_ARG(d), void *ptr)
+{
+ const bool *valp = ptr;
+ return (OptionValue){.bool_val = *valp};
+}
+
+static void bool_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ bool *valp = ptr;
+ *valp = v.bool_val;
+}
+
+static bool bool_parse(const OptionDesc *d, const char *str, OptionValue *v)
+{
+ if (streq(str, "true")) {
+ v->bool_val = true;
+ return true;
+ } else if (streq(str, "false")) {
+ v->bool_val = false;
+ return true;
+ }
+ return error_msg("Invalid value for %s", d->name);
+}
+
+static const char *bool_string(const OptionDesc* UNUSED_ARG(d), OptionValue v)
+{
+ return v.bool_val ? "true" : "false";
+}
+
+static bool bool_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
+{
+ const bool *valp = ptr;
+ return *valp == v.bool_val;
+}
+
+static bool enum_parse(const OptionDesc *d, const char *str, OptionValue *v)
+{
+ const char *const *values = d->u.enum_opt.values;
+ unsigned int i;
+ for (i = 0; values[i]; i++) {
+ if (streq(values[i], str)) {
+ v->uint_val = i;
+ return true;
+ }
+ }
+
+ unsigned int val;
+ if (!str_to_uint(str, &val) || val >= i) {
+ return error_msg("Invalid value for %s", d->name);
+ }
+
+ v->uint_val = val;
+ return true;
+}
+
+static const char *enum_string(const OptionDesc *desc, OptionValue value)
+{
+ return desc->u.enum_opt.values[value.uint_val];
+}
+
+static bool flag_parse(const OptionDesc *d, const char *str, OptionValue *v)
+{
+ // "0" is allowed for compatibility and is the same as ""
+ if (str[0] == '0' && str[1] == '\0') {
+ v->uint_val = 0;
+ return true;
+ }
+
+ const char *const *values = d->u.enum_opt.values;
+ unsigned int flags = 0;
+
+ for (size_t pos = 0, len = strlen(str); pos < len; ) {
+ const StringView flag = get_delim(str, &pos, len, ',');
+ size_t i;
+ for (i = 0; values[i]; i++) {
+ if (strview_equal_cstring(&flag, values[i])) {
+ flags |= 1u << i;
+ break;
+ }
+ }
+ if (unlikely(!values[i])) {
+ int flen = (int)flag.length;
+ return error_msg("Invalid flag '%.*s' for %s", flen, flag.data, d->name);
+ }
+ }
+
+ v->uint_val = flags;
+ return true;
+}
+
+static const char *flag_string(const OptionDesc *desc, OptionValue value)
+{
+ static char buf[128];
+ unsigned int flags = value.uint_val;
+ if (!flags) {
+ buf[0] = '0';
+ buf[1] = '\0';
+ return buf;
+ }
+
+ char *ptr = buf;
+ const char *const *values = desc->u.enum_opt.values;
+ for (size_t i = 0, avail = sizeof(buf); values[i]; i++) {
+ if (flags & (1u << i)) {
+ char *next = memccpy(ptr, values[i], '\0', avail);
+ if (DEBUG >= 1) {
+ BUG_ON(!next);
+ avail -= (size_t)(next - ptr);
+ }
+ ptr = next;
+ ptr[-1] = ',';
+ }
+ }
+
+ BUG_ON(ptr == buf);
+ ptr[-1] = '\0';
+ return buf;
+}
+
+static const struct {
+ OptionValue (*get)(const OptionDesc *desc, void *ptr);
+ void (*set)(const OptionDesc *desc, void *ptr, OptionValue value);
+ bool (*parse)(const OptionDesc *desc, const char *str, OptionValue *value);
+ const char *(*string)(const OptionDesc *desc, OptionValue value);
+ bool (*equals)(const OptionDesc *desc, void *ptr, OptionValue value);
+} option_ops[] = {
+ [OPT_STR] = {str_get, str_set, str_parse, str_string, str_equals},
+ [OPT_UINT] = {uint_get, uint_set, uint_parse, uint_string, uint_equals},
+ [OPT_ENUM] = {uint_get, uint_set, enum_parse, enum_string, uint_equals},
+ [OPT_BOOL] = {bool_get, bool_set, bool_parse, bool_string, bool_equals},
+ [OPT_FLAG] = {uint_get, uint_set, flag_parse, flag_string, uint_equals},
+ [OPT_REGEX] = {re_get, re_set, re_parse, str_string, re_equals},
+};
+
+static const char *const bool_enum[] = {"false", "true", NULL};
+static const char *const newline_enum[] = {"unix", "dos", NULL};
+static const char *const tristate_enum[] = {"false", "true", "auto", NULL};
+static const char *const save_unmodified_enum[] = {"none", "touch", "full", NULL};
+
+static const char *const detect_indent_values[] = {
+ "1", "2", "3", "4", "5", "6", "7", "8",
+ NULL
+};
+
+// Note: this must be kept in sync with WhitespaceErrorFlags
+static const char *const ws_error_values[] = {
+ "space-indent",
+ "space-align",
+ "tab-indent",
+ "tab-after-indent",
+ "special",
+ "auto-indent",
+ "trailing",
+ "all-trailing",
+ NULL
+};
+
+static const OptionDesc option_desc[] = {
+ BOOL_OPT("auto-indent", C(auto_indent), NULL),
+ BOOL_OPT("brace-indent", L(brace_indent), NULL),
+ ENUM_OPT("case-sensitive-search", G(case_sensitive_search), tristate_enum, NULL),
+ FLAG_OPT("detect-indent", C(detect_indent), detect_indent_values, NULL),
+ BOOL_OPT("display-special", G(display_special), redraw_screen),
+ BOOL_OPT("editorconfig", C(editorconfig), NULL),
+ BOOL_OPT("emulate-tab", C(emulate_tab), NULL),
+ UINT_OPT("esc-timeout", G(esc_timeout), 0, 2000, NULL),
+ BOOL_OPT("expand-tab", C(expand_tab), redraw_buffer),
+ BOOL_OPT("file-history", C(file_history), NULL),
+ UINT_OPT("filesize-limit", G(filesize_limit), 0, 16000, NULL),
+ STR_OPT("filetype", L(filetype), validate_filetype, filetype_changed),
+ BOOL_OPT("fsync", C(fsync), NULL),
+ REGEX_OPT("indent-regex", L(indent_regex), NULL),
+ UINT_OPT("indent-width", C(indent_width), 1, INDENT_WIDTH_MAX, NULL),
+ BOOL_OPT("lock-files", G(lock_files), NULL),
+ ENUM_OPT("newline", G(crlf_newlines), newline_enum, NULL),
+ BOOL_OPT("optimize-true-color", G(optimize_true_color), redraw_screen),
+ BOOL_OPT("overwrite", C(overwrite), overwrite_changed),
+ ENUM_OPT("save-unmodified", C(save_unmodified), save_unmodified_enum, NULL),
+ UINT_OPT("scroll-margin", G(scroll_margin), 0, 100, redraw_screen),
+ BOOL_OPT("select-cursor-char", G(select_cursor_char), redraw_screen),
+ BOOL_OPT("set-window-title", G(set_window_title), set_window_title_changed),
+ BOOL_OPT("show-line-numbers", G(show_line_numbers), redraw_screen),
+ STR_OPT("statusline-left", G(statusline_left), validate_statusline_format, NULL),
+ STR_OPT("statusline-right", G(statusline_right), validate_statusline_format, NULL),
+ BOOL_OPT("syntax", C(syntax), syntax_changed),
+ BOOL_OPT("tab-bar", G(tab_bar), redraw_screen),
+ UINT_OPT("tab-width", C(tab_width), 1, TAB_WIDTH_MAX, redraw_buffer),
+ UINT_OPT("text-width", C(text_width), 1, TEXT_WIDTH_MAX, NULL),
+ BOOL_OPT("utf8-bom", G(utf8_bom), NULL),
+ FLAG_OPT("ws-error", C(ws_error), ws_error_values, redraw_buffer),
+};
+
+static char *local_ptr(const OptionDesc *desc, LocalOptions *opt)
+{
+ BUG_ON(!desc->local);
+ return (char*)opt + desc->offset;
+}
+
+static char *global_ptr(const OptionDesc *desc, GlobalOptions *opt)
+{
+ BUG_ON(!desc->global);
+ return (char*)opt + desc->offset;
+}
+
+static char *get_option_ptr(EditorState *e, const OptionDesc *d, bool global)
+{
+ return global ? global_ptr(d, &e->options) : local_ptr(d, &e->buffer->options);
+}
+
+static inline size_t count_enum_values(const OptionDesc *desc)
+{
+ OptionType type = desc->type;
+ BUG_ON(type != OPT_ENUM && type != OPT_FLAG && type != OPT_BOOL);
+ const char *const *values = desc->u.enum_opt.values;
+ BUG_ON(!values);
+ return string_array_length((char**)values);
+}
+
+UNITTEST {
+ static const struct {
+ size_t alignment;
+ size_t size;
+ } map[] = {
+ [OPT_STR] = {ALIGNOF(const char*), sizeof(const char*)},
+ [OPT_UINT] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
+ [OPT_ENUM] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
+ [OPT_BOOL] = {ALIGNOF(bool), sizeof(bool)},
+ [OPT_FLAG] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
+ [OPT_REGEX] = {ALIGNOF(const InternedRegexp*), sizeof(const InternedRegexp*)},
+ };
+
+ GlobalOptions gopts = {.tab_bar = true};
+ LocalOptions lopts = {.filetype = NULL};
+
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ const OptionType type = desc->type;
+ BUG_ON(type >= ARRAYLEN(map));
+ size_t alignment = map[type].alignment;
+ size_t end = desc->offset + map[type].size;
+ if (desc->global) {
+ uintptr_t ptr_val = (uintptr_t)global_ptr(desc, &gopts);
+ BUG_ON(ptr_val % alignment != 0);
+ BUG_ON(end > sizeof(GlobalOptions));
+ }
+ if (desc->local) {
+ uintptr_t ptr_val = (uintptr_t)local_ptr(desc, &lopts);
+ BUG_ON(ptr_val % alignment != 0);
+ BUG_ON(end > sizeof(LocalOptions));
+ }
+ if (desc->global && desc->local) {
+ BUG_ON(end > sizeof(CommonOptions));
+ }
+ if (type == OPT_UINT) {
+ BUG_ON(desc->u.uint_opt.max <= desc->u.uint_opt.min);
+ } else if (type == OPT_BOOL) {
+ BUG_ON(desc->u.enum_opt.values != bool_enum);
+ } else if (type == OPT_ENUM) {
+ BUG_ON(count_enum_values(desc) < 2);
+ } else if (type == OPT_FLAG) {
+ size_t nvals = count_enum_values(desc);
+ OptionValue val = {.uint_val = -1};
+ BUG_ON(nvals < 2);
+ BUG_ON(nvals >= BITSIZE(val.uint_val));
+ const char *str = flag_string(desc, val);
+ BUG_ON(!str);
+ BUG_ON(str[0] == '\0');
+ if (!flag_parse(desc, str, &val)) {
+ BUG("flag_parse() failed for string: %s", str);
+ }
+ unsigned int mask = (1u << nvals) - 1;
+ if (val.uint_val != mask) {
+ BUG("values not equal: %u, %u", val.uint_val, mask);
+ }
+ }
+ }
+
+ // Ensure option_desc[] is properly sorted
+ CHECK_BSEARCH_ARRAY(option_desc, name, strcmp);
+}
+
+static OptionValue desc_get(const OptionDesc *desc, void *ptr)
+{
+ return option_ops[desc->type].get(desc, ptr);
+}
+
+static void desc_set(EditorState *e, const OptionDesc *desc, void *ptr, bool global, OptionValue value)
+{
+ option_ops[desc->type].set(desc, ptr, value);
+ if (desc->on_change) {
+ desc->on_change(e, global);
+ }
+}
+
+static bool desc_parse(const OptionDesc *desc, const char *str, OptionValue *value)
+{
+ return option_ops[desc->type].parse(desc, str, value);
+}
+
+static const char *desc_string(const OptionDesc *desc, OptionValue value)
+{
+ return option_ops[desc->type].string(desc, value);
+}
+
+static bool desc_equals(const OptionDesc *desc, void *ptr, OptionValue value)
+{
+ return option_ops[desc->type].equals(desc, ptr, value);
+}
+
+static int option_cmp(const void *key, const void *elem)
+{
+ const char *name = key;
+ const OptionDesc *desc = elem;
+ return strcmp(name, desc->name);
+}
+
+static const OptionDesc *find_option(const char *name)
+{
+ return BSEARCH(name, option_desc, option_cmp);
+}
+
+static const OptionDesc *must_find_option(const char *name)
+{
+ const OptionDesc *desc = find_option(name);
+ if (!desc) {
+ error_msg("No such option %s", name);
+ }
+ return desc;
+}
+
+static const OptionDesc *must_find_global_option(const char *name)
+{
+ const OptionDesc *desc = must_find_option(name);
+ if (desc && !desc->global) {
+ error_msg("Option %s is not global", name);
+ return NULL;
+ }
+ return desc;
+}
+
+static bool do_set_option (
+ EditorState *e,
+ const OptionDesc *desc,
+ const char *value,
+ bool local,
+ bool global
+) {
+ if (local && !desc->local) {
+ return error_msg("Option %s is not local", desc->name);
+ }
+ if (global && !desc->global) {
+ return error_msg("Option %s is not global", desc->name);
+ }
+
+ OptionValue val;
+ if (!desc_parse(desc, value, &val)) {
+ return false;
+ }
+
+ if (!local && !global) {
+ // Set both by default
+ local = desc->local;
+ global = desc->global;
+ }
+
+ if (local) {
+ desc_set(e, desc, local_ptr(desc, &e->buffer->options), false, val);
+ }
+ if (global) {
+ desc_set(e, desc, global_ptr(desc, &e->options), true, val);
+ }
+
+ return true;
+}
+
+bool set_option(EditorState *e, const char *name, const char *value, bool local, bool global)
+{
+ const OptionDesc *desc = must_find_option(name);
+ if (!desc) {
+ return false;
+ }
+ return do_set_option(e, desc, value, local, global);
+}
+
+bool set_bool_option(EditorState *e, const char *name, bool local, bool global)
+{
+ const OptionDesc *desc = must_find_option(name);
+ if (!desc) {
+ return false;
+ }
+ if (desc->type != OPT_BOOL) {
+ return error_msg("Option %s is not boolean", desc->name);
+ }
+ return do_set_option(e, desc, "true", local, global);
+}
+
+static const OptionDesc *find_toggle_option(const char *name, bool *global)
+{
+ if (*global) {
+ return must_find_global_option(name);
+ }
+
+ // Toggle local value by default if option has both values
+ const OptionDesc *desc = must_find_option(name);
+ if (desc && !desc->local) {
+ *global = true;
+ }
+ return desc;
+}
+
+bool toggle_option(EditorState *e, const char *name, bool global, bool verbose)
+{
+ const OptionDesc *desc = find_toggle_option(name, &global);
+ if (!desc) {
+ return false;
+ }
+
+ char *ptr = get_option_ptr(e, desc, global);
+ OptionValue value = desc_get(desc, ptr);
+ OptionType type = desc->type;
+ if (type == OPT_ENUM) {
+ if (desc->u.enum_opt.values[value.uint_val + 1]) {
+ value.uint_val++;
+ } else {
+ value.uint_val = 0;
+ }
+ } else if (type == OPT_BOOL) {
+ value.bool_val = !value.bool_val;
+ } else {
+ return error_msg("Toggling %s requires arguments", name);
+ }
+
+ desc_set(e, desc, ptr, global, value);
+ if (verbose) {
+ const char *prefix = (global && desc->local) ? "[global] " : "";
+ const char *str = desc_string(desc, value);
+ info_msg("%s%s = %s", prefix, desc->name, str);
+ }
+
+ return true;
+}
+
+bool toggle_option_values (
+ EditorState *e,
+ const char *name,
+ bool global,
+ bool verbose,
+ char **values,
+ size_t count
+) {
+ const OptionDesc *desc = find_toggle_option(name, &global);
+ if (!desc) {
+ return false;
+ }
+
+ BUG_ON(count == 0);
+ size_t current = 0;
+ bool error = false;
+ char *ptr = get_option_ptr(e, desc, global);
+ OptionValue *parsed_values = xnew(OptionValue, count);
+
+ for (size_t i = 0; i < count; i++) {
+ if (desc_parse(desc, values[i], &parsed_values[i])) {
+ if (desc_equals(desc, ptr, parsed_values[i])) {
+ current = i + 1;
+ }
+ } else {
+ error = true;
+ }
+ }
+
+ if (!error) {
+ size_t i = current % count;
+ desc_set(e, desc, ptr, global, parsed_values[i]);
+ if (verbose) {
+ const char *prefix = (global && desc->local) ? "[global] " : "";
+ const char *str = desc_string(desc, parsed_values[i]);
+ info_msg("%s%s = %s", prefix, desc->name, str);
+ }
+ }
+
+ free(parsed_values);
+ return !error;
+}
+
+bool validate_local_options(char **strs)
+{
+ size_t invalid = 0;
+ for (size_t i = 0; strs[i]; i += 2) {
+ const char *name = strs[i];
+ const char *value = strs[i + 1];
+ const OptionDesc *desc = must_find_option(name);
+ if (unlikely(!desc)) {
+ invalid++;
+ } else if (unlikely(!desc->local)) {
+ error_msg("%s is not local", name);
+ invalid++;
+ } else if (unlikely(desc->on_change == filetype_changed)) {
+ error_msg("filetype cannot be set via option command");
+ invalid++;
+ } else {
+ OptionValue val;
+ if (unlikely(!desc_parse(desc, value, &val))) {
+ invalid++;
+ }
+ }
+ }
+ return !invalid;
+}
+
+#if DEBUG >= 1
+static void sanity_check_option_value(const OptionDesc *desc, OptionValue val)
+{
+ switch (desc->type) {
+ case OPT_STR:
+ BUG_ON(!val.str_val);
+ BUG_ON(val.str_val != str_intern(val.str_val));
+ if (desc->u.str_opt.validate) {
+ BUG_ON(!desc->u.str_opt.validate(val.str_val));
+ }
+ return;
+ case OPT_UINT:
+ BUG_ON(val.uint_val < desc->u.uint_opt.min);
+ BUG_ON(val.uint_val > desc->u.uint_opt.max);
+ return;
+ case OPT_ENUM:
+ BUG_ON(val.uint_val >= count_enum_values(desc));
+ return;
+ case OPT_FLAG: {
+ size_t nvals = count_enum_values(desc);
+ BUG_ON(nvals >= 32);
+ unsigned int mask = (1u << nvals) - 1;
+ unsigned int uint_val = val.uint_val;
+ BUG_ON((uint_val & mask) != uint_val);
+ }
+ return;
+ case OPT_REGEX:
+ BUG_ON(val.str_val && val.str_val[0] == '\0');
+ BUG_ON(val.str_val && !regexp_is_interned(val.str_val));
+ return;
+ case OPT_BOOL:
+ return;
+ }
+
+ BUG("unhandled option type");
+}
+
+static void sanity_check_options(const void *opts, bool global)
+{
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ BUG_ON(desc->type >= ARRAYLEN(option_ops));
+ if ((desc->global && desc->local) || global == desc->global) {
+ OptionValue val = desc_get(desc, (char*)opts + desc->offset);
+ sanity_check_option_value(desc, val);
+ }
+ }
+}
+
+void sanity_check_global_options(const GlobalOptions *gopts)
+{
+ sanity_check_options(gopts, true);
+}
+
+void sanity_check_local_options(const LocalOptions *lopts)
+{
+ sanity_check_options(lopts, false);
+}
+#endif
+
+void collect_options(PointerArray *a, const char *prefix, bool local, bool global)
+{
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ if ((local && !desc->local) || (global && !desc->global)) {
+ continue;
+ }
+ if (str_has_prefix(desc->name, prefix)) {
+ ptr_array_append(a, xstrdup(desc->name));
+ }
+ }
+}
+
+// Collect options that can be set via the "option" command
+void collect_auto_options(PointerArray *a, const char *prefix)
+{
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ if (!desc->local || desc->on_change == filetype_changed) {
+ continue;
+ }
+ if (str_has_prefix(desc->name, prefix)) {
+ ptr_array_append(a, xstrdup(desc->name));
+ }
+ }
+}
+
+void collect_toggleable_options(PointerArray *a, const char *prefix, bool global)
+{
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ if (global && !desc->global) {
+ continue;
+ }
+ OptionType type = desc->type;
+ bool toggleable = (type == OPT_ENUM || type == OPT_BOOL);
+ if (toggleable && str_has_prefix(desc->name, prefix)) {
+ ptr_array_append(a, xstrdup(desc->name));
+ }
+ }
+}
+
+void collect_option_values(EditorState *e, PointerArray *a, const char *option, const char *prefix)
+{
+ const OptionDesc *desc = find_option(option);
+ if (!desc) {
+ return;
+ }
+
+ if (prefix[0] == '\0') {
+ char *ptr = get_option_ptr(e, desc, !desc->local);
+ OptionValue value = desc_get(desc, ptr);
+ ptr_array_append(a, xstrdup(desc_string(desc, value)));
+ return;
+ }
+
+ OptionType type = desc->type;
+ if (type == OPT_STR || type == OPT_UINT || type == OPT_REGEX) {
+ return;
+ }
+
+ const char *const *values = desc->u.enum_opt.values;
+ if (type == OPT_ENUM || type == OPT_BOOL) {
+ for (size_t i = 0; values[i]; i++) {
+ if (str_has_prefix(values[i], prefix)) {
+ ptr_array_append(a, xstrdup(values[i]));
+ }
+ }
+ return;
+ }
+
+ BUG_ON(type != OPT_FLAG);
+ const char *comma = strrchr(prefix, ',');
+ size_t prefix_len = comma ? ++comma - prefix : 0;
+ for (size_t i = 0; values[i]; i++) {
+ const char *str = values[i];
+ if (str_has_prefix(str, prefix + prefix_len)) {
+ size_t str_len = strlen(str);
+ char *completion = xmalloc(prefix_len + str_len + 1);
+ memcpy(completion, prefix, prefix_len);
+ memcpy(completion + prefix_len, str, str_len + 1);
+ ptr_array_append(a, completion);
+ }
+ }
+}
+
+static void append_option(String *s, const OptionDesc *desc, void *ptr)
+{
+ const OptionValue value = desc_get(desc, ptr);
+ const char *value_str = desc_string(desc, value);
+ if (unlikely(value_str[0] == '-')) {
+ string_append_literal(s, "-- ");
+ }
+ string_append_cstring(s, desc->name);
+ string_append_byte(s, ' ');
+ string_append_escaped_arg(s, value_str, true);
+ string_append_byte(s, '\n');
+}
+
+String dump_options(GlobalOptions *gopts, LocalOptions *lopts)
+{
+ String buf = string_new(4096);
+ for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
+ const OptionDesc *desc = &option_desc[i];
+ void *local = desc->local ? local_ptr(desc, lopts) : NULL;
+ void *global = desc->global ? global_ptr(desc, gopts) : NULL;
+ if (local && global) {
+ const OptionValue global_value = desc_get(desc, global);
+ if (desc_equals(desc, local, global_value)) {
+ string_append_literal(&buf, "set ");
+ append_option(&buf, desc, local);
+ } else {
+ string_append_literal(&buf, "set -g ");
+ append_option(&buf, desc, global);
+ string_append_literal(&buf, "set -l ");
+ append_option(&buf, desc, local);
+ }
+ } else {
+ string_append_literal(&buf, "set ");
+ append_option(&buf, desc, local ? local : global);
+ }
+ }
+ return buf;
+}
+
+const char *get_option_value_string(EditorState *e, const char *name)
+{
+ const OptionDesc *desc = find_option(name);
+ if (!desc) {
+ return NULL;
+ }
+ char *ptr = get_option_ptr(e, desc, !desc->local);
+ return desc_string(desc, desc_get(desc, ptr));
+}
diff --git a/examples/dte/options.h b/examples/dte/options.h
new file mode 100644
index 0000000..1d0a129
--- /dev/null
+++ b/examples/dte/options.h
@@ -0,0 +1,114 @@
+#ifndef OPTIONS_H
+#define OPTIONS_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "regexp.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string.h"
+
+enum {
+ INDENT_WIDTH_MAX = 8,
+ TAB_WIDTH_MAX = 8,
+ TEXT_WIDTH_MAX = 1000,
+};
+
+// Note: this must be kept in sync with ws_error_values[]
+typedef enum {
+ WSE_SPACE_INDENT = 1 << 0, // Spaces in indent (except WSE_SPACE_ALIGN)
+ WSE_SPACE_ALIGN = 1 << 1, // Less than tab-width spaces at end of indent
+ WSE_TAB_INDENT = 1 << 2, // Tab in indent
+ WSE_TAB_AFTER_INDENT = 1 << 3, // Tab anywhere but indent
+ WSE_SPECIAL = 1 << 4, // Special whitespace characters
+ WSE_AUTO_INDENT = 1 << 5, // expand-tab ? WSE_TAB_AFTER_INDENT | WSE_TAB_INDENT : WSE_SPACE_INDENT
+ WSE_TRAILING = 1 << 6, // Trailing whitespace
+ WSE_ALL_TRAILING = 1 << 7, // Like WSE_TRAILING, but including around cursor
+} WhitespaceErrorFlags;
+
+// Note: this must be kept in sync with save_unmodified_enum[]
+typedef enum {
+ SAVE_NONE,
+ SAVE_TOUCH,
+ SAVE_FULL,
+} SaveUnmodifiedType;
+
+#define COMMON_OPTIONS \
+ unsigned int detect_indent; \
+ unsigned int indent_width; \
+ unsigned int save_unmodified; \
+ unsigned int tab_width; \
+ unsigned int text_width; \
+ unsigned int ws_error; \
+ bool auto_indent; \
+ bool editorconfig; \
+ bool emulate_tab; \
+ bool expand_tab; \
+ bool file_history; \
+ bool fsync; \
+ bool overwrite; \
+ bool syntax
+
+typedef struct {
+ COMMON_OPTIONS;
+} CommonOptions;
+
+// Note: all members should be initialized in buffer_new()
+typedef struct {
+ COMMON_OPTIONS;
+ // Only local
+ bool brace_indent;
+ const char *filetype;
+ const InternedRegexp *indent_regex;
+} LocalOptions;
+
+typedef struct {
+ COMMON_OPTIONS;
+ // Only global
+ bool display_special;
+ bool lock_files;
+ bool optimize_true_color;
+ bool select_cursor_char;
+ bool set_window_title;
+ bool show_line_numbers;
+ bool tab_bar;
+ bool utf8_bom; // Default value for new files
+ unsigned int esc_timeout;
+ unsigned int filesize_limit;
+ unsigned int scroll_margin;
+ unsigned int crlf_newlines; // Default value for new files
+ unsigned int case_sensitive_search; // SearchCaseSensitivity
+ const char *statusline_left;
+ const char *statusline_right;
+} GlobalOptions;
+
+#undef COMMON_OPTIONS
+
+static inline bool use_spaces_for_indent(const LocalOptions *opt)
+{
+ return opt->expand_tab || opt->indent_width != opt->tab_width;
+}
+
+struct EditorState;
+
+bool set_option(struct EditorState *e, const char *name, const char *value, bool local, bool global);
+bool set_bool_option(struct EditorState *e, const char *name, bool local, bool global);
+bool toggle_option(struct EditorState *e, const char *name, bool global, bool verbose);
+bool toggle_option_values(struct EditorState *e, const char *name, bool global, bool verbose, char **values, size_t count);
+bool validate_local_options(char **strs);
+void collect_options(PointerArray *a, const char *prefix, bool local, bool global);
+void collect_auto_options(PointerArray *a, const char *prefix);
+void collect_toggleable_options(PointerArray *a, const char *prefix, bool global);
+void collect_option_values(struct EditorState *e, PointerArray *a, const char *option, const char *prefix);
+String dump_options(GlobalOptions *gopts, LocalOptions *lopts);
+const char *get_option_value_string(struct EditorState *e, const char *name);
+
+#if DEBUG >= 1
+ void sanity_check_global_options(const GlobalOptions *opts);
+ void sanity_check_local_options(const LocalOptions *lopts);
+#else
+ static inline void sanity_check_global_options(const GlobalOptions* UNUSED_ARG(gopts)) {}
+ static inline void sanity_check_local_options(const LocalOptions* UNUSED_ARG(lopts)) {}
+#endif
+
+#endif
diff --git a/examples/dte/regexp.c b/examples/dte/regexp.c
new file mode 100644
index 0000000..dc4eb0f
--- /dev/null
+++ b/examples/dte/regexp.c
@@ -0,0 +1,151 @@
+#include <errno.h>
+#include <stdlib.h>
+#include "regexp.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/hashmap.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+#include "util/xsnprintf.h"
+
+static HashMap interned_regexps;
+
+bool regexp_error_msg(const regex_t *re, const char *pattern, int err)
+{
+ char msg[1024];
+ regerror(err, re, msg, sizeof(msg));
+ return error_msg("%s: %s", msg, pattern);
+}
+
+bool regexp_compile_internal(regex_t *re, const char *pattern, int flags)
+{
+ int err = regcomp(re, pattern, flags);
+ if (err) {
+ return regexp_error_msg(re, pattern, err);
+ }
+ return true;
+}
+
+void regexp_compile_or_fatal_error(regex_t *re, const char *pattern, int flags)
+{
+ // Note: DEFAULT_REGEX_FLAGS isn't used here because this function
+ // is only used for compiling built-in patterns, where we explicitly
+ // avoid using "enhanced" features
+ int err = regcomp(re, pattern, flags | REG_EXTENDED);
+ if (unlikely(err)) {
+ char msg[1024];
+ regerror(err, re, msg, sizeof(msg));
+ fatal_error(msg, EINVAL);
+ }
+}
+
+bool regexp_exec (
+ const regex_t *re,
+ const char *buf,
+ size_t size,
+ size_t nmatch,
+ regmatch_t *pmatch,
+ int flags
+) {
+ // "If REG_STARTEND is specified, pmatch must point to at least one
+ // regmatch_t (even if nmatch is 0 or REG_NOSUB was specified), to
+ // hold the input offsets for REG_STARTEND."
+ // -- https://man.openbsd.org/regex.3
+ BUG_ON(!pmatch);
+
+// ASan's __interceptor_regexec() doesn't support REG_STARTEND
+#if defined(REG_STARTEND) && !defined(ASAN_ENABLED) && !defined(MSAN_ENABLED)
+ pmatch[0].rm_so = 0;
+ pmatch[0].rm_eo = size;
+ return !regexec(re, buf, nmatch, pmatch, flags | REG_STARTEND);
+#else
+ // Buffer must be null-terminated if REG_STARTEND isn't supported
+ char *tmp = xstrcut(buf, size);
+ int ret = !regexec(re, tmp, nmatch, pmatch, flags);
+ free(tmp);
+ return ret;
+#endif
+}
+
+// Check which word boundary tokens are supported by regcomp(3)
+// (if any) and initialize `rwbt` with them for later use
+bool regexp_init_word_boundary_tokens(RegexpWordBoundaryTokens *rwbt)
+{
+ static const char text[] = "SSfooEE SSfoo fooEE foo SSfooEE";
+ const regoff_t match_start = 20, match_end = 23;
+ static const RegexpWordBoundaryTokens pairs[] = {
+ {"\\<", "\\>"},
+ {"[[:<:]]", "[[:>:]]"},
+ {"\\b", "\\b"},
+ };
+
+ BUG_ON(ARRAYLEN(text) <= match_end);
+ BUG_ON(!mem_equal(text + match_start - 1, " foo ", 5));
+
+ for (size_t i = 0; i < ARRAYLEN(pairs); i++) {
+ const char *start = pairs[i].start;
+ const char *end = pairs[i].end;
+ char patt[32];
+ xsnprintf(patt, sizeof(patt), "%s(foo)%s", start, end);
+ regex_t re;
+ if (regcomp(&re, patt, DEFAULT_REGEX_FLAGS) != 0) {
+ continue;
+ }
+ regmatch_t m[2];
+ bool match = !regexec(&re, text, ARRAYLEN(m), m, 0);
+ regfree(&re);
+ if (match && m[0].rm_so == match_start && m[0].rm_eo == match_end) {
+ *rwbt = pairs[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void free_cached_regexp(CachedRegexp *cr)
+{
+ regfree(&cr->re);
+ free(cr);
+}
+
+const InternedRegexp *regexp_intern(const char *pattern)
+{
+ if (pattern[0] == '\0') {
+ return NULL;
+ }
+
+ InternedRegexp *ir = hashmap_get(&interned_regexps, pattern);
+ if (ir) {
+ return ir;
+ }
+
+ ir = xnew(InternedRegexp, 1);
+ int err = regcomp(&ir->re, pattern, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB);
+ if (unlikely(err)) {
+ regexp_error_msg(&ir->re, pattern, err);
+ free(ir);
+ return NULL;
+ }
+
+ ir->str = xstrdup(pattern);
+ return hashmap_insert(&interned_regexps, ir->str, ir);
+}
+
+bool regexp_is_interned(const char *pattern)
+{
+ return !!hashmap_find(&interned_regexps, pattern);
+}
+
+// Note: this does NOT free InternedRegexp::str, because it points at the
+// same string as HashMapEntry::key and is already freed by hashmap_free()
+static void free_interned_regexp(InternedRegexp *ir)
+{
+ regfree(&ir->re);
+ free(ir);
+}
+
+void free_interned_regexps(void)
+{
+ hashmap_free(&interned_regexps, (FreeFunction)free_interned_regexp);
+}
diff --git a/examples/dte/regexp.h b/examples/dte/regexp.h
new file mode 100644
index 0000000..50fdabb
--- /dev/null
+++ b/examples/dte/regexp.h
@@ -0,0 +1,85 @@
+#ifndef REGEXP_H
+#define REGEXP_H
+
+#include <regex.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include "util/macros.h"
+
+enum {
+#ifdef REG_ENHANCED
+ // The REG_ENHANCED flag enables various extensions on macOS
+ // (see "enhanced features" in re_format(7)). Most of these
+ // extensions are enabled by default on Linux (in both glibc
+ // and musl) without the need for any extra flags.
+ DEFAULT_REGEX_FLAGS = REG_EXTENDED | REG_ENHANCED,
+#else
+ // POSIX Extended Regular Expressions (ERE) are used almost
+ // everywhere in this codebase, except where Basic Regular
+ // Expressions (BRE) are explicitly called for (most notably
+ // in search_tag(), which is used for ctags patterns).
+ DEFAULT_REGEX_FLAGS = REG_EXTENDED,
+#endif
+};
+
+typedef struct {
+ regex_t re;
+ char str[];
+} CachedRegexp;
+
+typedef struct {
+ char *str;
+ regex_t re;
+} InternedRegexp;
+
+// Platform-specific patterns for matching word boundaries, as detected
+// and initialized by regexp_init_word_boundary_tokens()
+typedef struct {
+ char start[8];
+ char end[8];
+} RegexpWordBoundaryTokens;
+
+bool regexp_compile_internal(regex_t *re, const char *pattern, int flags) WARN_UNUSED_RESULT;
+
+WARN_UNUSED_RESULT
+static inline bool regexp_compile(regex_t *re, const char *pattern, int flags)
+{
+ return regexp_compile_internal(re, pattern, flags | DEFAULT_REGEX_FLAGS);
+}
+
+WARN_UNUSED_RESULT
+static inline bool regexp_compile_basic(regex_t *re, const char *pattern, int flags)
+{
+ return regexp_compile_internal(re, pattern, flags);
+}
+
+WARN_UNUSED_RESULT
+static inline bool regexp_is_valid(const char *pattern, int flags)
+{
+ regex_t re;
+ if (!regexp_compile(&re, pattern, flags | REG_NOSUB)) {
+ return false;
+ }
+ regfree(&re);
+ return true;
+}
+
+void regexp_compile_or_fatal_error(regex_t *re, const char *pattern, int flags);
+bool regexp_init_word_boundary_tokens(RegexpWordBoundaryTokens *rwbt);
+bool regexp_error_msg(const regex_t *re, const char *pattern, int err);
+void free_cached_regexp(CachedRegexp *cr);
+
+const InternedRegexp *regexp_intern(const char *pattern);
+bool regexp_is_interned(const char *pattern);
+void free_interned_regexps(void);
+
+bool regexp_exec (
+ const regex_t *re,
+ const char *buf,
+ size_t size,
+ size_t nmatch,
+ regmatch_t *pmatch,
+ int flags
+) WARN_UNUSED_RESULT;
+
+#endif
diff --git a/examples/dte/replace.c b/examples/dte/replace.c
new file mode 100644
index 0000000..028d474
--- /dev/null
+++ b/examples/dte/replace.c
@@ -0,0 +1,256 @@
+#include <stdlib.h>
+#include "replace.h"
+#include "buffer.h"
+#include "change.h"
+#include "editor.h"
+#include "error.h"
+#include "regexp.h"
+#include "screen.h"
+#include "selection.h"
+#include "util/debug.h"
+#include "util/string.h"
+#include "util/xmalloc.h"
+#include "view.h"
+#include "window.h"
+
+static void build_replacement (
+ String *buf,
+ const char *line,
+ const char *format,
+ const regmatch_t *matches
+) {
+ for (size_t i = 0; format[i]; ) {
+ char ch = format[i++];
+ size_t match_idx;
+ if (ch == '\\') {
+ if (unlikely(format[i] == '\0')) {
+ break;
+ }
+ ch = format[i++];
+ if (ch < '1' || ch > '9') {
+ string_append_byte(buf, ch);
+ continue;
+ }
+ match_idx = ch - '0';
+ } else if (ch == '&') {
+ match_idx = 0;
+ } else {
+ string_append_byte(buf, ch);
+ continue;
+ }
+ const regmatch_t *match = &matches[match_idx];
+ regoff_t len = match->rm_eo - match->rm_so;
+ if (len > 0) {
+ string_append_buf(buf, line + match->rm_so, (size_t)len);
+ }
+ }
+}
+
+/*
+ * s/abc/x
+ *
+ * string to match against
+ * -------------------------------------------
+ * "foo abc bar abc baz" "foo abc bar abc baz"
+ * "foo x bar abc baz" " bar abc baz"
+ */
+static unsigned int replace_on_line (
+ View *view,
+ StringView *line,
+ regex_t *re,
+ const char *format,
+ BlockIter *bi,
+ ReplaceFlags *flagsp
+) {
+ const unsigned char *buf = line->data;
+ unsigned char *alloc = NULL;
+ EditorState *e = view->window->editor;
+ ReplaceFlags flags = *flagsp;
+ regmatch_t matches[32];
+ size_t pos = 0;
+ int eflags = 0;
+ unsigned int nr = 0;
+
+ while (regexp_exec (
+ re,
+ buf + pos,
+ line->length - pos,
+ ARRAYLEN(matches),
+ matches,
+ eflags
+ )) {
+ regoff_t match_len = matches[0].rm_eo - matches[0].rm_so;
+ bool skip = false;
+
+ // Move cursor to beginning of the text to replace
+ block_iter_skip_bytes(bi, matches[0].rm_so);
+ view->cursor = *bi;
+
+ if (flags & REPLACE_CONFIRM) {
+ switch (status_prompt(e, "Replace? [Y/n/a/q]", "ynaq")) {
+ case 'y':
+ break;
+ case 'n':
+ skip = true;
+ break;
+ case 'a':
+ flags &= ~REPLACE_CONFIRM;
+ *flagsp = flags;
+
+ // Record rest of the changes as one chain
+ begin_change_chain();
+ break;
+ case 'q':
+ case 0:
+ *flagsp = flags | REPLACE_CANCEL;
+ goto out;
+ }
+ }
+
+ if (skip) {
+ // Move cursor after the matched text
+ block_iter_skip_bytes(&view->cursor, match_len);
+ } else {
+ String b = STRING_INIT;
+ build_replacement(&b, buf + pos, format, matches);
+
+ // line ref is invalidated by modification
+ if (buf == line->data && line->length != 0) {
+ BUG_ON(alloc);
+ alloc = xmemdup(buf, line->length);
+ buf = alloc;
+ }
+
+ buffer_replace_bytes(view, match_len, b.buffer, b.len);
+ nr++;
+
+ // Update selection length
+ if (view->selection) {
+ view->sel_eo += b.len;
+ view->sel_eo -= match_len;
+ }
+
+ // Move cursor after the replaced text
+ block_iter_skip_bytes(&view->cursor, b.len);
+ string_free(&b);
+ }
+ *bi = view->cursor;
+
+ if (!match_len) {
+ break;
+ }
+
+ if (!(flags & REPLACE_GLOBAL)) {
+ break;
+ }
+
+ pos += matches[0].rm_so + match_len;
+
+ // Don't match beginning of line again
+ eflags = REG_NOTBOL;
+ }
+
+out:
+ free(alloc);
+ return nr;
+}
+
+bool reg_replace(View *view, const char *pattern, const char *format, ReplaceFlags flags)
+{
+ if (unlikely(pattern[0] == '\0')) {
+ return error_msg("Search pattern must contain at least 1 character");
+ }
+
+ int re_flags = REG_NEWLINE;
+ re_flags |= (flags & REPLACE_IGNORE_CASE) ? REG_ICASE : 0;
+ re_flags |= (flags & REPLACE_BASIC) ? 0 : DEFAULT_REGEX_FLAGS;
+
+ regex_t re;
+ if (unlikely(!regexp_compile_internal(&re, pattern, re_flags))) {
+ return false;
+ }
+
+ BlockIter bi = block_iter(view->buffer);
+ size_t nr_bytes;
+ bool swapped = false;
+ if (view->selection) {
+ SelectionInfo info;
+ init_selection(view, &info);
+ view->cursor = info.si;
+ view->sel_so = info.so;
+ view->sel_eo = info.eo;
+ swapped = info.swapped;
+ bi = view->cursor;
+ nr_bytes = info.eo - info.so;
+ } else {
+ BlockIter eof = bi;
+ block_iter_eof(&eof);
+ nr_bytes = block_iter_get_offset(&eof);
+ }
+
+ // Record multiple changes as one chain only when replacing all
+ if (!(flags & REPLACE_CONFIRM)) {
+ begin_change_chain();
+ }
+
+ unsigned int nr_substitutions = 0;
+ size_t nr_lines = 0;
+ while (1) {
+ StringView line;
+ fill_line_ref(&bi, &line);
+
+ // Number of bytes to process
+ size_t count = line.length;
+ if (line.length > nr_bytes) {
+ // End of selection is not full line
+ line.length = nr_bytes;
+ }
+
+ unsigned int nr = replace_on_line(view, &line, &re, format, &bi, &flags);
+ if (nr) {
+ nr_substitutions += nr;
+ nr_lines++;
+ }
+
+ if (flags & REPLACE_CANCEL || count + 1 >= nr_bytes) {
+ break;
+ }
+
+ nr_bytes -= count + 1;
+ block_iter_next_line(&bi);
+ }
+
+ if (!(flags & REPLACE_CONFIRM)) {
+ end_change_chain(view);
+ }
+
+ regfree(&re);
+
+ if (nr_substitutions) {
+ info_msg (
+ "%u substitution%s on %zu line%s",
+ nr_substitutions,
+ (nr_substitutions > 1) ? "s" : "",
+ nr_lines,
+ (nr_lines > 1) ? "s" : ""
+ );
+ } else if (!(flags & REPLACE_CANCEL)) {
+ error_msg("Pattern '%s' not found", pattern);
+ }
+
+ if (view->selection) {
+ // Undo what init_selection() did
+ if (view->sel_eo) {
+ view->sel_eo--;
+ }
+ if (swapped) {
+ ssize_t tmp = view->sel_so;
+ view->sel_so = view->sel_eo;
+ view->sel_eo = tmp;
+ }
+ block_iter_goto_offset(&view->cursor, view->sel_eo);
+ view->sel_eo = SEL_EO_RECALC;
+ }
+
+ return (nr_substitutions > 0);
+}
diff --git a/examples/dte/replace.h b/examples/dte/replace.h
new file mode 100644
index 0000000..783a46f
--- /dev/null
+++ b/examples/dte/replace.h
@@ -0,0 +1,18 @@
+#ifndef REPLACE_H
+#define REPLACE_H
+
+#include <stdbool.h>
+#include "util/macros.h"
+#include "view.h"
+
+typedef enum {
+ REPLACE_CONFIRM = 1 << 0,
+ REPLACE_GLOBAL = 1 << 1,
+ REPLACE_IGNORE_CASE = 1 << 2,
+ REPLACE_BASIC = 1 << 3,
+ REPLACE_CANCEL = 1 << 4,
+} ReplaceFlags;
+
+bool reg_replace(View *view, const char *pattern, const char *format, ReplaceFlags flags) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/screen-cmdline.c b/examples/dte/screen-cmdline.c
new file mode 100644
index 0000000..58d04c4
--- /dev/null
+++ b/examples/dte/screen-cmdline.c
@@ -0,0 +1,91 @@
+#include "screen.h"
+#include "error.h"
+#include "search.h"
+
+static void print_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error)
+{
+ BuiltinColorEnum c = BC_COMMANDLINE;
+ if (msg[0]) {
+ c = is_error ? BC_ERRORMSG : BC_INFOMSG;
+ }
+
+ TermOutputBuffer *obuf = &term->obuf;
+ set_builtin_color(term, colors, c);
+
+ for (size_t i = 0; msg[i]; ) {
+ CodePoint u = u_get_char(msg, i + 4, &i);
+ if (!term_put_char(obuf, u)) {
+ break;
+ }
+ }
+}
+
+void show_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error)
+{
+ term_output_reset(term, 0, term->width, 0);
+ term_move_cursor(&term->obuf, 0, term->height - 1);
+ print_message(term, colors, msg, is_error);
+ term_clear_eol(term);
+}
+
+static size_t print_command(Terminal *term, const ColorScheme *colors, const CommandLine *cmdline, char prefix)
+{
+ const String *buf = &cmdline->buf;
+ TermOutputBuffer *obuf = &term->obuf;
+
+ // Width of characters up to and including cursor position
+ size_t w = 1; // ":" (prefix)
+
+ for (size_t i = 0; i <= cmdline->pos && i < buf->len; ) {
+ CodePoint u = u_get_char(buf->buffer, buf->len, &i);
+ w += u_char_width(u);
+ }
+ if (cmdline->pos == buf->len) {
+ w++;
+ }
+ if (w > term->width) {
+ obuf->scroll_x = w - term->width;
+ }
+
+ set_builtin_color(term, colors, BC_COMMANDLINE);
+ term_put_char(obuf, prefix);
+
+ size_t x = obuf->x - obuf->scroll_x;
+ for (size_t i = 0; i < buf->len; ) {
+ BUG_ON(obuf->x > obuf->scroll_x + obuf->width);
+ CodePoint u = u_get_char(buf->buffer, buf->len, &i);
+ if (!term_put_char(obuf, u)) {
+ break;
+ }
+ if (i <= cmdline->pos) {
+ x = obuf->x - obuf->scroll_x;
+ }
+ }
+
+ return x;
+}
+
+void update_command_line(EditorState *e)
+{
+ Terminal *term = &e->terminal;
+ char prefix = ':';
+ term_output_reset(term, 0, term->width, 0);
+ term_move_cursor(&term->obuf, 0, term->height - 1);
+ switch (e->input_mode) {
+ case INPUT_NORMAL: {
+ bool msg_is_error;
+ const char *msg = get_msg(&msg_is_error);
+ print_message(term, &e->colors, msg, msg_is_error);
+ break;
+ }
+ case INPUT_SEARCH:
+ prefix = e->search.reverse ? '?' : '/';
+ // Fallthrough
+ case INPUT_COMMAND:
+ e->cmdline_x = print_command(term, &e->colors, &e->cmdline, prefix);
+ break;
+ default:
+ BUG("unhandled input mode");
+ }
+ term_clear_eol(term);
+}
diff --git a/examples/dte/screen-prompt.c b/examples/dte/screen-prompt.c
new file mode 100644
index 0000000..b5bbe7d
--- /dev/null
+++ b/examples/dte/screen-prompt.c
@@ -0,0 +1,134 @@
+#include "screen.h"
+#include "signals.h"
+#include "terminal/input.h"
+
+static char get_choice(Terminal *term, const char *choices, unsigned int esc_timeout)
+{
+ KeyCode key = term_read_key(term, esc_timeout);
+ if (key == KEY_NONE) {
+ return 0;
+ }
+
+ switch (key) {
+ case KEY_BRACKETED_PASTE:
+ case KEY_DETECTED_PASTE:
+ term_discard_paste(&term->ibuf, key == KEY_BRACKETED_PASTE);
+ return 0;
+ case MOD_CTRL | 'c':
+ case MOD_CTRL | 'g':
+ case MOD_CTRL | '[':
+ return 0x18; // Cancel
+ case KEY_ENTER:
+ return choices[0]; // Default
+ }
+
+ if (key < 128) {
+ char ch = ascii_tolower(key);
+ if (strchr(choices, ch)) {
+ return ch;
+ }
+ }
+ return 0;
+}
+
+static void show_dialog (
+ EditorState *e,
+ const TermColor *text_color,
+ const char *question
+) {
+ Terminal *term = &e->terminal;
+ unsigned int question_width = u_str_width(question);
+ unsigned int min_width = question_width + 2;
+ if (term->height < 12 || term->width < min_width) {
+ return;
+ }
+
+ unsigned int height = term->height / 4;
+ unsigned int mid = term->height / 2;
+ unsigned int top = mid - (height / 2);
+ unsigned int bot = top + height;
+ unsigned int width = MAX(term->width / 2, min_width);
+ unsigned int x = (term->width - width) / 2;
+
+ // The "underline" and "strikethrough" attributes should only apply
+ // to the text, not the whole dialog background:
+ TermColor dialog_color = *text_color;
+ TermOutputBuffer *obuf = &term->obuf;
+ dialog_color.attr &= ~(ATTR_UNDERLINE | ATTR_STRIKETHROUGH);
+ set_color(term, &e->colors, &dialog_color);
+
+ for (unsigned int y = top; y < bot; y++) {
+ term_output_reset(term, x, width, 0);
+ term_move_cursor(obuf, x, y);
+ if (y == mid) {
+ term_set_bytes(term, ' ', (width - question_width) / 2);
+ set_color(term, &e->colors, text_color);
+ term_add_str(obuf, question);
+ set_color(term, &e->colors, &dialog_color);
+ }
+ term_clear_eol(term);
+ }
+}
+
+char dialog_prompt(EditorState *e, const char *question, const char *choices)
+{
+ const TermColor *color = &e->colors.builtin[BC_DIALOG];
+ Terminal *term = &e->terminal;
+ TermOutputBuffer *obuf = &term->obuf;
+
+ normal_update(e);
+ term_hide_cursor(term);
+ show_dialog(e, color, question);
+ show_message(term, &e->colors, question, false);
+ term_output_flush(obuf);
+
+ unsigned int esc_timeout = e->options.esc_timeout;
+ char choice;
+ while ((choice = get_choice(term, choices, esc_timeout)) == 0) {
+ if (!resized) {
+ continue;
+ }
+ ui_resize(e);
+ term_hide_cursor(term);
+ show_dialog(e, color, question);
+ show_message(term, &e->colors, question, false);
+ term_output_flush(obuf);
+ }
+
+ mark_everything_changed(e);
+ return (choice >= 'a') ? choice : 0;
+}
+
+char status_prompt(EditorState *e, const char *question, const char *choices)
+{
+ // update_buffer_windows() assumes these have been called for current view
+ view_update_cursor_x(e->view);
+ view_update_cursor_y(e->view);
+ view_update(e->view, e->options.scroll_margin);
+
+ // Set changed_line_min and changed_line_max before calling update_range()
+ mark_all_lines_changed(e->buffer);
+
+ Terminal *term = &e->terminal;
+ start_update(term);
+ update_term_title(term, e->buffer, e->options.set_window_title);
+ update_buffer_windows(e, e->buffer);
+ show_message(term, &e->colors, question, false);
+ end_update(e);
+
+ unsigned int esc_timeout = e->options.esc_timeout;
+ char choice;
+ while ((choice = get_choice(term, choices, esc_timeout)) == 0) {
+ if (!resized) {
+ continue;
+ }
+ ui_resize(e);
+ term_hide_cursor(term);
+ show_message(term, &e->colors, question, false);
+ restore_cursor(e);
+ term_show_cursor(term);
+ term_output_flush(&term->obuf);
+ }
+
+ return (choice >= 'a') ? choice : 0;
+}
diff --git a/examples/dte/screen-status.c b/examples/dte/screen-status.c
new file mode 100644
index 0000000..c7ec837
--- /dev/null
+++ b/examples/dte/screen-status.c
@@ -0,0 +1,46 @@
+#include "screen.h"
+#include "status.h"
+
+void update_status_line(const Window *window)
+{
+ EditorState *e = window->editor;
+ const GlobalOptions *opts = &e->options;
+ InputMode mode = e->input_mode;
+ char lbuf[512], rbuf[512];
+ sf_format(window, opts, mode, lbuf, sizeof lbuf, opts->statusline_left);
+ sf_format(window, opts, mode, rbuf, sizeof rbuf, opts->statusline_right);
+
+ const ColorScheme *colors = &e->colors;
+ Terminal *term = &e->terminal;
+ TermOutputBuffer *obuf = &term->obuf;
+ size_t lw = u_str_width(lbuf);
+ size_t rw = u_str_width(rbuf);
+ int w = window->w;
+ static_assert_compatible_types(w, window->w);
+ term_output_reset(term, window->x, w, 0);
+ term_move_cursor(obuf, window->x, window->y + window->h - 1);
+ set_builtin_color(term, colors, BC_STATUSLINE);
+
+ if (lw + rw <= w) {
+ // Both fit
+ term_add_str(obuf, lbuf);
+ term_set_bytes(term, ' ', w - lw - rw);
+ term_add_str(obuf, rbuf);
+ } else if (lw <= w && rw <= w) {
+ // Both would fit separately, draw overlapping
+ term_add_str(obuf, lbuf);
+ obuf->x = w - rw;
+ term_move_cursor(obuf, window->x + w - rw, window->y + window->h - 1);
+ term_add_str(obuf, rbuf);
+ } else if (lw <= w) {
+ // Left fits
+ term_add_str(obuf, lbuf);
+ term_clear_eol(term);
+ } else if (rw <= w) {
+ // Right fits
+ term_set_bytes(term, ' ', w - rw);
+ term_add_str(obuf, rbuf);
+ } else {
+ term_clear_eol(term);
+ }
+}
diff --git a/examples/dte/screen-tabbar.c b/examples/dte/screen-tabbar.c
new file mode 100644
index 0000000..57de14e
--- /dev/null
+++ b/examples/dte/screen-tabbar.c
@@ -0,0 +1,176 @@
+#include "screen.h"
+#include "util/numtostr.h"
+#include "util/strtonum.h"
+
+static size_t tab_title_width(size_t tab_number, const char *filename)
+{
+ return 3 + size_str_width(tab_number) + u_str_width(filename);
+}
+
+static void update_tab_title_width(View *view, size_t tab_number)
+{
+ size_t w = tab_title_width(tab_number, buffer_filename(view->buffer));
+ view->tt_width = w;
+ view->tt_truncated_width = w;
+}
+
+static void update_first_tab_idx(Window *window)
+{
+ size_t max_first_idx = window->views.count;
+ for (size_t w = 0; max_first_idx > 0; max_first_idx--) {
+ const View *view = window->views.ptrs[max_first_idx - 1];
+ w += view->tt_truncated_width;
+ if (w > window->w) {
+ break;
+ }
+ }
+
+ size_t min_first_idx = window->views.count;
+ for (size_t w = 0; min_first_idx > 0; min_first_idx--) {
+ const View *view = window->views.ptrs[min_first_idx - 1];
+ if (w || view == window->view) {
+ w += view->tt_truncated_width;
+ }
+ if (w > window->w) {
+ break;
+ }
+ }
+
+ size_t idx = CLAMP(window->first_tab_idx, min_first_idx, max_first_idx);
+ window->first_tab_idx = idx;
+}
+
+static void calculate_tabbar(Window *window)
+{
+ int total_w = 0;
+ for (size_t i = 0, n = window->views.count; i < n; i++) {
+ View *view = window->views.ptrs[i];
+ if (view == window->view) {
+ // Make sure current tab is visible
+ window->first_tab_idx = MIN(i, window->first_tab_idx);
+ }
+ update_tab_title_width(view, i + 1);
+ total_w += view->tt_width;
+ }
+
+ if (total_w <= window->w) {
+ // All tabs fit without truncating
+ window->first_tab_idx = 0;
+ return;
+ }
+
+ // Truncate all wide tabs
+ total_w = 0;
+ int truncated_count = 0;
+ for (size_t i = 0, n = window->views.count; i < n; i++) {
+ View *view = window->views.ptrs[i];
+ int truncated_w = 20;
+ if (view->tt_width > truncated_w) {
+ view->tt_truncated_width = truncated_w;
+ total_w += truncated_w;
+ truncated_count++;
+ } else {
+ total_w += view->tt_width;
+ }
+ }
+
+ if (total_w > window->w) {
+ // Not all tabs fit even after truncating wide tabs
+ update_first_tab_idx(window);
+ return;
+ }
+
+ // All tabs fit after truncating wide tabs
+ int extra = window->w - total_w;
+
+ // Divide extra space between truncated tabs
+ while (extra > 0) {
+ BUG_ON(truncated_count == 0);
+ int extra_avg = extra / truncated_count;
+ int extra_mod = extra % truncated_count;
+
+ for (size_t i = 0, n = window->views.count; i < n; i++) {
+ View *view = window->views.ptrs[i];
+ int add = view->tt_width - view->tt_truncated_width;
+ if (add == 0) {
+ continue;
+ }
+
+ int avail = extra_avg;
+ if (extra_mod) {
+ // This is needed for equal divide
+ if (extra_avg == 0) {
+ avail++;
+ extra_mod--;
+ }
+ }
+ if (add > avail) {
+ add = avail;
+ } else {
+ truncated_count--;
+ }
+
+ view->tt_truncated_width += add;
+ extra -= add;
+ }
+ }
+
+ window->first_tab_idx = 0;
+}
+
+static void print_tab_title(Terminal *term, const ColorScheme *colors, const View *view, size_t idx)
+{
+ const char *filename = buffer_filename(view->buffer);
+ int skip = view->tt_width - view->tt_truncated_width;
+ if (skip > 0) {
+ filename += u_skip_chars(filename, &skip);
+ }
+
+ const char *tab_number = uint_to_str((unsigned int)idx + 1);
+ TermOutputBuffer *obuf = &term->obuf;
+ bool is_active_tab = (view == view->window->view);
+ bool is_modified = buffer_modified(view->buffer);
+ bool left_overflow = (obuf->x == 0 && idx > 0);
+
+ set_builtin_color(term, colors, is_active_tab ? BC_ACTIVETAB : BC_INACTIVETAB);
+ term_put_char(obuf, left_overflow ? '<' : ' ');
+ term_add_str(obuf, tab_number);
+ term_put_char(obuf, is_modified ? '+' : ':');
+ term_add_str(obuf, filename);
+
+ size_t ntabs = view->window->views.count;
+ bool right_overflow = (obuf->x == (obuf->width - 1) && idx < (ntabs - 1));
+ term_put_char(obuf, right_overflow ? '>' : ' ');
+}
+
+void print_tabbar(Terminal *term, const ColorScheme *colors, Window *window)
+{
+ TermOutputBuffer *obuf = &term->obuf;
+ term_output_reset(term, window->x, window->w, 0);
+ term_move_cursor(obuf, window->x, window->y);
+ calculate_tabbar(window);
+
+ size_t i = window->first_tab_idx;
+ size_t n = window->views.count;
+ for (; i < n; i++) {
+ const View *view = window->views.ptrs[i];
+ if (obuf->x + view->tt_truncated_width > window->w) {
+ break;
+ }
+ print_tab_title(term, colors, view, i);
+ }
+
+ set_builtin_color(term, colors, BC_TABBAR);
+
+ if (i == n) {
+ term_clear_eol(term);
+ return;
+ }
+
+ while (obuf->x < obuf->width - 1) {
+ term_put_char(obuf, ' ');
+ }
+ if (obuf->x == obuf->width - 1) {
+ term_put_char(obuf, '>');
+ }
+}
diff --git a/examples/dte/screen-view.c b/examples/dte/screen-view.c
new file mode 100644
index 0000000..0f8d72c
--- /dev/null
+++ b/examples/dte/screen-view.c
@@ -0,0 +1,427 @@
+#include "screen.h"
+#include "indent.h"
+#include "selection.h"
+#include "syntax/highlight.h"
+#include "util/ascii.h"
+#include "util/debug.h"
+#include "util/str-util.h"
+#include "util/utf8.h"
+
+typedef struct {
+ const View *view;
+ size_t line_nr;
+ size_t offset;
+ ssize_t sel_so;
+ ssize_t sel_eo;
+
+ const unsigned char *line;
+ size_t size;
+ size_t pos;
+ size_t indent_size;
+ size_t trailing_ws_offset;
+ const TermColor **colors;
+} LineInfo;
+
+// Like mask_color() but can change bg color only if it has not been changed yet
+static void mask_color2(const ColorScheme *colors, TermColor *color, const TermColor *over)
+{
+ int32_t default_bg = colors->builtin[BC_DEFAULT].bg;
+ if (over->bg != COLOR_KEEP && (color->bg == default_bg || color->bg < 0)) {
+ color->bg = over->bg;
+ }
+
+ if (over->fg != COLOR_KEEP) {
+ color->fg = over->fg;
+ }
+
+ if (!(over->attr & ATTR_KEEP)) {
+ color->attr = over->attr;
+ }
+}
+
+static void mask_selection_and_current_line (
+ const ColorScheme *colors,
+ const LineInfo *info,
+ TermColor *color
+) {
+ if (info->offset >= info->sel_so && info->offset < info->sel_eo) {
+ mask_color(color, &colors->builtin[BC_SELECTION]);
+ } else if (info->line_nr == info->view->cy) {
+ mask_color2(colors, color, &colors->builtin[BC_CURRENTLINE]);
+ }
+}
+
+static bool is_non_text(CodePoint u, bool display_special)
+{
+ if (u == '\t') {
+ return display_special;
+ }
+ return u < 0x20 || u == 0x7F || u_is_unprintable(u);
+}
+
+static WhitespaceErrorFlags get_ws_error(const LocalOptions *opts)
+{
+ WhitespaceErrorFlags taberrs = WSE_TAB_INDENT | WSE_TAB_AFTER_INDENT;
+ WhitespaceErrorFlags extra = opts->expand_tab ? taberrs : WSE_SPACE_INDENT;
+ return opts->ws_error | ((opts->ws_error & WSE_AUTO_INDENT) ? extra : 0);
+}
+
+static bool whitespace_error(const LineInfo *info, CodePoint u, size_t i)
+{
+ const View *view = info->view;
+ WhitespaceErrorFlags flags = get_ws_error(&view->buffer->options);
+ WhitespaceErrorFlags trailing = flags & (WSE_TRAILING | WSE_ALL_TRAILING);
+ if (i >= info->trailing_ws_offset && trailing) {
+ // Trailing whitespace
+ if (
+ // Cursor is not on this line
+ info->line_nr != view->cy
+ // or is positioned before any trailing whitespace
+ || view->cx < info->trailing_ws_offset
+ // or user explicitly wants trailing space under cursor highlighted
+ || flags & WSE_ALL_TRAILING
+ ) {
+ return true;
+ }
+ }
+
+ bool in_indent = (i < info->indent_size);
+ if (u == '\t') {
+ WhitespaceErrorFlags mask = in_indent ? WSE_TAB_INDENT : WSE_TAB_AFTER_INDENT;
+ return (flags & mask) != 0;
+ }
+ if (!in_indent) {
+ // All checks below here only apply to indentation
+ return false;
+ }
+
+ const char *line = info->line;
+ size_t pos = i;
+ size_t count = 0;
+ while (pos > 0 && line[pos - 1] == ' ') {
+ pos--;
+ }
+ while (pos < info->size && line[pos] == ' ') {
+ pos++;
+ count++;
+ }
+
+ WhitespaceErrorFlags mask;
+ if (count >= view->buffer->options.tab_width) {
+ // Spaces used instead of tab
+ mask = WSE_SPACE_INDENT;
+ } else if (pos < info->size && line[pos] == '\t') {
+ // Space before tab
+ mask = WSE_SPACE_INDENT;
+ } else {
+ // Less than tab width spaces at end of indentation
+ mask = WSE_SPACE_ALIGN;
+ }
+ return (flags & mask) != 0;
+}
+
+static CodePoint screen_next_char(EditorState *e, LineInfo *info)
+{
+ size_t count, pos = info->pos;
+ CodePoint u = info->line[pos];
+ TermColor color;
+ bool ws_error = false;
+
+ if (likely(u < 0x80)) {
+ info->pos++;
+ count = 1;
+ if (u == '\t' || u == ' ') {
+ ws_error = whitespace_error(info, u, pos);
+ }
+ } else {
+ u = u_get_nonascii(info->line, info->size, &info->pos);
+ count = info->pos - pos;
+
+ if (
+ u_is_special_whitespace(u) // Highly annoying no-break space etc.
+ && (info->view->buffer->options.ws_error & WSE_SPECIAL)
+ ) {
+ ws_error = true;
+ }
+ }
+
+ if (info->colors && info->colors[pos]) {
+ color = *info->colors[pos];
+ } else {
+ color = e->colors.builtin[BC_DEFAULT];
+ }
+ if (is_non_text(u, e->options.display_special)) {
+ mask_color(&color, &e->colors.builtin[BC_NONTEXT]);
+ }
+ if (ws_error) {
+ mask_color(&color, &e->colors.builtin[BC_WSERROR]);
+ }
+ mask_selection_and_current_line(&e->colors, info, &color);
+ set_color(&e->terminal, &e->colors, &color);
+
+ info->offset += count;
+ return u;
+}
+
+static void screen_skip_char(TermOutputBuffer *obuf, LineInfo *info)
+{
+ CodePoint u = info->line[info->pos++];
+ info->offset++;
+ if (likely(u < 0x80)) {
+ if (likely(!ascii_iscntrl(u))) {
+ obuf->x++;
+ } else if (u == '\t' && obuf->tab_mode != TAB_CONTROL) {
+ obuf->x = next_indent_width(obuf->x, obuf->tab_width);
+ } else {
+ // Control
+ obuf->x += 2;
+ }
+ } else {
+ size_t pos = info->pos;
+ info->pos--;
+ u = u_get_nonascii(info->line, info->size, &info->pos);
+ obuf->x += u_char_width(u);
+ info->offset += info->pos - pos;
+ }
+}
+
+static bool is_notice(const char *word, size_t len)
+{
+ switch (len) {
+ case 3: return mem_equal(word, "XXX", 3);
+ case 4: return mem_equal(word, "TODO", 4);
+ case 5: return mem_equal(word, "FIXME", 5);
+ }
+ return false;
+}
+
+// Highlight certain words inside comments
+static void hl_words(Terminal *term, const ColorScheme *colors, const LineInfo *info)
+{
+ const TermColor *cc = find_color(colors, "comment");
+ const TermColor *nc = find_color(colors, "notice");
+
+ if (!info->colors || !cc || !nc) {
+ return;
+ }
+
+ size_t i = info->pos;
+ if (i >= info->size) {
+ return;
+ }
+
+ // Go to beginning of partially visible word inside comment
+ while (i > 0 && info->colors[i] == cc && is_word_byte(info->line[i])) {
+ i--;
+ }
+
+ // This should be more than enough. I'm too lazy to iterate characters
+ // instead of bytes and calculate text width.
+ const size_t max = info->pos + term->width * 4 + 8;
+
+ size_t si;
+ while (i < info->size) {
+ if (info->colors[i] != cc || !is_word_byte(info->line[i])) {
+ if (i > max) {
+ break;
+ }
+ i++;
+ } else {
+ // Beginning of a word inside comment
+ si = i++;
+ while (
+ i < info->size && info->colors[i] == cc
+ && is_word_byte(info->line[i])
+ ) {
+ i++;
+ }
+ if (is_notice(info->line + si, i - si)) {
+ for (size_t j = si; j < i; j++) {
+ info->colors[j] = nc;
+ }
+ }
+ }
+ }
+}
+
+static void line_info_init (
+ LineInfo *info,
+ const View *view,
+ const BlockIter *bi,
+ size_t line_nr
+) {
+ *info = (LineInfo) {
+ .view = view,
+ .line_nr = line_nr,
+ .offset = block_iter_get_offset(bi),
+ };
+
+ if (!view->selection) {
+ info->sel_so = -1;
+ info->sel_eo = -1;
+ } else if (view->sel_eo != SEL_EO_RECALC) {
+ // Already calculated
+ info->sel_so = view->sel_so;
+ info->sel_eo = view->sel_eo;
+ BUG_ON(info->sel_so > info->sel_eo);
+ } else {
+ SelectionInfo sel;
+ init_selection(view, &sel);
+ info->sel_so = sel.so;
+ info->sel_eo = sel.eo;
+ }
+}
+
+static void line_info_set_line (
+ LineInfo *info,
+ const StringView *line,
+ const TermColor **colors
+) {
+ BUG_ON(line->length == 0);
+ BUG_ON(line->data[line->length - 1] != '\n');
+
+ info->line = line->data;
+ info->size = line->length - 1;
+ info->pos = 0;
+ info->colors = colors;
+
+ {
+ size_t i, n;
+ for (i = 0, n = info->size; i < n; i++) {
+ char ch = info->line[i];
+ if (ch != '\t' && ch != ' ') {
+ break;
+ }
+ }
+ info->indent_size = i;
+ }
+
+ static_assert_compatible_types(info->trailing_ws_offset, size_t);
+ info->trailing_ws_offset = SIZE_MAX;
+ for (ssize_t i = info->size - 1; i >= 0; i--) {
+ char ch = info->line[i];
+ if (ch != '\t' && ch != ' ') {
+ break;
+ }
+ info->trailing_ws_offset = i;
+ }
+}
+
+static void print_line(EditorState *e, LineInfo *info)
+{
+ // Screen might be scrolled horizontally. Skip most invisible
+ // characters using screen_skip_char(), which is much faster than
+ // buf_skip(screen_next_char(info)).
+ //
+ // There can be a wide character (tab, control code etc.) that is
+ // partially visible and can't be skipped using screen_skip_char().
+ Terminal *term = &e->terminal;
+ TermOutputBuffer *obuf = &term->obuf;
+ while (obuf->x + 8 < obuf->scroll_x && info->pos < info->size) {
+ screen_skip_char(obuf, info);
+ }
+
+ const ColorScheme *colors = &e->colors;
+ hl_words(term, colors, info);
+
+ while (info->pos < info->size) {
+ BUG_ON(obuf->x > obuf->scroll_x + obuf->width);
+ CodePoint u = screen_next_char(e, info);
+ if (!term_put_char(obuf, u)) {
+ // +1 for newline
+ info->offset += info->size - info->pos + 1;
+ return;
+ }
+ }
+
+ TermColor color;
+ if (e->options.display_special && obuf->x >= obuf->scroll_x) {
+ // Syntax highlighter highlights \n but use default color anyway
+ color = colors->builtin[BC_DEFAULT];
+ mask_color(&color, &colors->builtin[BC_NONTEXT]);
+ mask_selection_and_current_line(colors, info, &color);
+ set_color(term, colors, &color);
+ term_put_char(obuf, '$');
+ }
+
+ color = colors->builtin[BC_DEFAULT];
+ mask_selection_and_current_line(colors, info, &color);
+ set_color(term, colors, &color);
+ info->offset++;
+ term_clear_eol(term);
+}
+
+void update_range(EditorState *e, const View *view, long y1, long y2)
+{
+ const int edit_x = view->window->edit_x;
+ const int edit_y = view->window->edit_y;
+ const int edit_w = view->window->edit_w;
+ const int edit_h = view->window->edit_h;
+
+ Terminal *term = &e->terminal;
+ TermOutputBuffer *obuf = &term->obuf;
+ term_output_reset(term, edit_x, edit_w, view->vx);
+ obuf->tab_width = view->buffer->options.tab_width;
+ obuf->tab_mode = e->options.display_special ? TAB_SPECIAL : TAB_NORMAL;
+
+ BlockIter bi = view->cursor;
+ for (long i = 0, n = view->cy - y1; i < n; i++) {
+ block_iter_prev_line(&bi);
+ }
+ for (long i = 0, n = y1 - view->cy; i < n; i++) {
+ block_iter_eat_line(&bi);
+ }
+ block_iter_bol(&bi);
+
+ LineInfo info;
+ line_info_init(&info, view, &bi, y1);
+
+ y1 -= view->vy;
+ y2 -= view->vy;
+
+ bool got_line = !block_iter_is_eof(&bi);
+ hl_fill_start_states(view->buffer, &e->colors, info.line_nr);
+ long i;
+ for (i = y1; got_line && i < y2; i++) {
+ obuf->x = 0;
+ term_move_cursor(obuf, edit_x, edit_y + i);
+
+ StringView line;
+ fill_line_nl_ref(&bi, &line);
+ bool next_changed;
+ const TermColor **colors = hl_line(view->buffer, &e->colors, &line, info.line_nr, &next_changed);
+ line_info_set_line(&info, &line, colors);
+ print_line(e, &info);
+
+ got_line = !!block_iter_next_line(&bi);
+ info.line_nr++;
+
+ if (next_changed && i + 1 == y2 && y2 < edit_h) {
+ // More lines need to be updated not because their contents have
+ // changed but because their highlight state has
+ y2++;
+ }
+ }
+
+ if (i < y2 && info.line_nr == view->cy) {
+ // Dummy empty line is shown only if cursor is on it
+ TermColor color = e->colors.builtin[BC_DEFAULT];
+
+ obuf->x = 0;
+ mask_color2(&e->colors, &color, &e->colors.builtin[BC_CURRENTLINE]);
+ set_color(term, &e->colors, &color);
+
+ term_move_cursor(obuf, edit_x, edit_y + i++);
+ term_clear_eol(term);
+ }
+
+ if (i < y2) {
+ set_builtin_color(term, &e->colors, BC_NOLINE);
+ }
+ for (; i < y2; i++) {
+ obuf->x = 0;
+ term_move_cursor(obuf, edit_x, edit_y + i);
+ term_put_char(obuf, '~');
+ term_clear_eol(term);
+ }
+}
diff --git a/examples/dte/screen-window.c b/examples/dte/screen-window.c
new file mode 100644
index 0000000..aa3a96f
--- /dev/null
+++ b/examples/dte/screen-window.c
@@ -0,0 +1,130 @@
+#include "screen.h"
+
+static void print_separator(Window *window, void *ud)
+{
+ Terminal *term = ud;
+ TermOutputBuffer *obuf = &term->obuf;
+ if (window->x + window->w == term->width) {
+ return;
+ }
+ for (int y = 0, h = window->h; y < h; y++) {
+ term_move_cursor(obuf, window->x + window->w, window->y + y);
+ term_add_byte(obuf, '|');
+ }
+}
+
+static void update_separators(Terminal *term, const ColorScheme *colors, const Frame *frame)
+{
+ set_builtin_color(term, colors, BC_STATUSLINE);
+ frame_for_each_window(frame, print_separator, term);
+}
+
+static void update_line_numbers(Terminal *term, const ColorScheme *colors, Window *window, bool force)
+{
+ const View *view = window->view;
+ size_t lines = view->buffer->nl;
+ int x = window->x;
+
+ calculate_line_numbers(window);
+ long first = view->vy + 1;
+ long last = MIN(view->vy + window->edit_h, lines);
+
+ if (
+ !force
+ && window->line_numbers.first == first
+ && window->line_numbers.last == last
+ ) {
+ return;
+ }
+
+ window->line_numbers.first = first;
+ window->line_numbers.last = last;
+
+ TermOutputBuffer *obuf = &term->obuf;
+ char buf[DECIMAL_STR_MAX(unsigned long) + 1];
+ size_t width = window->line_numbers.width;
+ BUG_ON(width > sizeof(buf));
+ BUG_ON(width < LINE_NUMBERS_MIN_WIDTH);
+ term_output_reset(term, window->x, window->w, 0);
+ set_builtin_color(term, colors, BC_LINENUMBER);
+
+ for (int y = 0, h = window->edit_h, edit_y = window->edit_y; y < h; y++) {
+ unsigned long line = view->vy + y + 1;
+ memset(buf, ' ', width);
+ if (line <= lines) {
+ size_t i = width - 2;
+ do {
+ buf[i--] = (line % 10) + '0';
+ } while (line /= 10);
+ }
+ term_move_cursor(obuf, x, edit_y + y);
+ term_add_bytes(obuf, buf, width);
+ }
+}
+
+static void update_window_full(Window *window, void* UNUSED_ARG(data))
+{
+ EditorState *e = window->editor;
+ View *view = window->view;
+ view_update_cursor_x(view);
+ view_update_cursor_y(view);
+ view_update(view, e->options.scroll_margin);
+ if (e->options.tab_bar) {
+ print_tabbar(&e->terminal, &e->colors, window);
+ }
+ if (e->options.show_line_numbers) {
+ update_line_numbers(&e->terminal, &e->colors, window, true);
+ }
+ update_range(e, view, view->vy, view->vy + window->edit_h);
+ update_status_line(window);
+}
+
+void update_all_windows(EditorState *e)
+{
+ update_window_sizes(&e->terminal, e->root_frame);
+ frame_for_each_window(e->root_frame, update_window_full, NULL);
+ update_separators(&e->terminal, &e->colors, e->root_frame);
+}
+
+static void update_window(EditorState *e, Window *window)
+{
+ if (e->options.tab_bar && window->update_tabbar) {
+ print_tabbar(&e->terminal, &e->colors, window);
+ }
+
+ const View *view = window->view;
+ if (e->options.show_line_numbers) {
+ // Force updating lines numbers if all lines changed
+ bool force = (view->buffer->changed_line_max == LONG_MAX);
+ update_line_numbers(&e->terminal, &e->colors, window, force);
+ }
+
+ long y1 = MAX(view->buffer->changed_line_min, view->vy);
+ long y2 = MIN(view->buffer->changed_line_max, view->vy + window->edit_h - 1);
+ update_range(e, view, y1, y2 + 1);
+ update_status_line(window);
+}
+
+// Update all visible views containing this buffer
+void update_buffer_windows(EditorState *e, const Buffer *buffer)
+{
+ const View *current_view = e->view;
+ for (size_t i = 0, n = buffer->views.count; i < n; i++) {
+ View *view = buffer->views.ptrs[i];
+ if (view != view->window->view) {
+ // Not visible
+ continue;
+ }
+ if (view != current_view) {
+ // Restore cursor
+ view->cursor.blk = BLOCK(view->buffer->blocks.next);
+ block_iter_goto_offset(&view->cursor, view->saved_cursor_offset);
+
+ // These have already been updated for current view
+ view_update_cursor_x(view);
+ view_update_cursor_y(view);
+ view_update(view, e->options.scroll_margin);
+ }
+ update_window(e, view->window);
+ }
+}
diff --git a/examples/dte/screen.c b/examples/dte/screen.c
new file mode 100644
index 0000000..22bbf69
--- /dev/null
+++ b/examples/dte/screen.c
@@ -0,0 +1,211 @@
+#include <string.h>
+#include "screen.h"
+#include "frame.h"
+#include "terminal/cursor.h"
+#include "terminal/ioctl.h"
+#include "util/log.h"
+
+void set_color(Terminal *term, const ColorScheme *colors, const TermColor *color)
+{
+ TermColor tmp = *color;
+ // NOTE: -2 (keep) is treated as -1 (default)
+ if (tmp.fg < 0) {
+ tmp.fg = colors->builtin[BC_DEFAULT].fg;
+ }
+ if (tmp.bg < 0) {
+ tmp.bg = colors->builtin[BC_DEFAULT].bg;
+ }
+ if (same_color(&tmp, &term->obuf.color)) {
+ return;
+ }
+ term_set_color(term, &tmp);
+}
+
+void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c)
+{
+ set_color(term, colors, &colors->builtin[c]);
+}
+
+static void update_cursor_style(EditorState *e)
+{
+ CursorInputMode mode;
+ switch (e->input_mode) {
+ case INPUT_NORMAL:
+ if (e->buffer->options.overwrite) {
+ mode = CURSOR_MODE_OVERWRITE;
+ } else {
+ mode = CURSOR_MODE_INSERT;
+ }
+ break;
+ case INPUT_COMMAND:
+ case INPUT_SEARCH:
+ mode = CURSOR_MODE_CMDLINE;
+ break;
+ default:
+ BUG("unhandled input mode");
+ return;
+ }
+
+ TermCursorStyle style = e->cursor_styles[mode];
+ TermCursorStyle def = e->cursor_styles[CURSOR_MODE_DEFAULT];
+ if (style.type == CURSOR_KEEP) {
+ style.type = def.type;
+ }
+ if (style.color == COLOR_KEEP) {
+ style.color = def.color;
+ }
+
+ e->cursor_style_changed = false;
+ if (!same_cursor(&style, &e->terminal.obuf.cursor_style)) {
+ term_set_cursor_style(&e->terminal, style);
+ }
+}
+
+void update_term_title(Terminal *term, const Buffer *buffer, bool set_window_title)
+{
+ if (!set_window_title || !(term->features & TFLAG_SET_WINDOW_TITLE)) {
+ return;
+ }
+
+ // FIXME: title must not contain control characters
+ TermOutputBuffer *obuf = &term->obuf;
+ const char *filename = buffer_filename(buffer);
+ term_add_literal(obuf, "\033]2;");
+ term_add_bytes(obuf, filename, strlen(filename));
+ term_add_byte(obuf, ' ');
+ term_add_byte(obuf, buffer_modified(buffer) ? '+' : '-');
+ term_add_literal(obuf, " dte\033\\");
+}
+
+void mask_color(TermColor *color, const TermColor *over)
+{
+ if (over->fg != COLOR_KEEP) {
+ color->fg = over->fg;
+ }
+ if (over->bg != COLOR_KEEP) {
+ color->bg = over->bg;
+ }
+ if (!(over->attr & ATTR_KEEP)) {
+ color->attr = over->attr;
+ }
+}
+
+void restore_cursor(EditorState *e)
+{
+ unsigned int x, y;
+ switch (e->input_mode) {
+ case INPUT_NORMAL:
+ x = e->window->edit_x + e->view->cx_display - e->view->vx;
+ y = e->window->edit_y + e->view->cy - e->view->vy;
+ break;
+ case INPUT_COMMAND:
+ case INPUT_SEARCH:
+ x = e->cmdline_x;
+ y = e->terminal.height - 1;
+ break;
+ default:
+ BUG("unhandled input mode");
+ }
+ term_move_cursor(&e->terminal.obuf, x, y);
+}
+
+static void clear_update_tabbar(Window *window, void* UNUSED_ARG(data))
+{
+ window->update_tabbar = false;
+}
+
+void end_update(EditorState *e)
+{
+ Terminal *term = &e->terminal;
+ restore_cursor(e);
+ term_show_cursor(term);
+ term_end_sync_update(term);
+ term_output_flush(&term->obuf);
+
+ e->buffer->changed_line_min = LONG_MAX;
+ e->buffer->changed_line_max = -1;
+ frame_for_each_window(e->root_frame, clear_update_tabbar, NULL);
+}
+
+void start_update(Terminal *term)
+{
+ term_begin_sync_update(term);
+ term_hide_cursor(term);
+}
+
+void update_window_sizes(Terminal *term, Frame *frame)
+{
+ set_frame_size(frame, term->width, term->height - 1);
+ update_window_coordinates(frame);
+}
+
+void update_screen_size(Terminal *term, Frame *root_frame)
+{
+ unsigned int width, height;
+ if (!term_get_size(&width, &height)) {
+ return;
+ }
+
+ // TODO: remove minimum width/height and instead make update_screen()
+ // do something sensible when the terminal dimensions are tiny
+ term->width = MAX(width, 3);
+ term->height = MAX(height, 3);
+
+ update_window_sizes(term, root_frame);
+ LOG_INFO("terminal size: %ux%u", width, height);
+}
+
+NOINLINE
+void normal_update(EditorState *e)
+{
+ Terminal *term = &e->terminal;
+ start_update(term);
+ update_term_title(term, e->buffer, e->options.set_window_title);
+ update_all_windows(e);
+ update_command_line(e);
+ update_cursor_style(e);
+ end_update(e);
+}
+
+void update_screen(EditorState *e, const ScreenState *s)
+{
+ if (e->everything_changed) {
+ e->everything_changed = false;
+ normal_update(e);
+ return;
+ }
+
+ Buffer *buffer = e->buffer;
+ View *view = e->view;
+ view_update_cursor_x(view);
+ view_update_cursor_y(view);
+ view_update(view, e->options.scroll_margin);
+
+ if (s->id == buffer->id) {
+ if (s->vx != view->vx || s->vy != view->vy) {
+ mark_all_lines_changed(buffer);
+ } else {
+ // Because of trailing whitespace highlighting and highlighting
+ // current line in different color, the lines cy (old cursor y) and
+ // view->cy need to be updated. Always update at least current line.
+ buffer_mark_lines_changed(buffer, s->cy, view->cy);
+ }
+ if (s->is_modified != buffer_modified(buffer)) {
+ mark_buffer_tabbars_changed(buffer);
+ }
+ } else {
+ e->window->update_tabbar = true;
+ mark_all_lines_changed(buffer);
+ }
+
+ start_update(&e->terminal);
+ if (e->window->update_tabbar) {
+ update_term_title(&e->terminal, e->buffer, e->options.set_window_title);
+ }
+ update_buffer_windows(e, buffer);
+ update_command_line(e);
+ if (e->cursor_style_changed) {
+ update_cursor_style(e);
+ }
+ end_update(e);
+}
diff --git a/examples/dte/screen.h b/examples/dte/screen.h
new file mode 100644
index 0000000..a3041c0
--- /dev/null
+++ b/examples/dte/screen.h
@@ -0,0 +1,59 @@
+#ifndef SCREEN_H
+#define SCREEN_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "buffer.h"
+#include "editor.h"
+#include "syntax/color.h"
+#include "terminal/output.h"
+#include "terminal/terminal.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/utf8.h"
+#include "view.h"
+#include "window.h"
+
+typedef struct {
+ bool is_modified;
+ unsigned long id;
+ long cy;
+ long vx;
+ long vy;
+} ScreenState;
+
+// screen.c
+void update_screen(EditorState *e, const ScreenState *s);
+void update_term_title(Terminal *term, const Buffer *buffer, bool set_window_title);
+void update_window_sizes(Terminal *term, Frame *frame);
+void update_screen_size(Terminal *term, Frame *root_frame);
+void set_color(Terminal *term, const ColorScheme *colors, const TermColor *color);
+void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c);
+void mask_color(TermColor *color, const TermColor *over);
+void start_update(Terminal *term);
+void end_update(EditorState *e);
+void normal_update(EditorState *e);
+void restore_cursor(EditorState *e);
+
+// screen-cmdline.c
+void update_command_line(EditorState *e);
+void show_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error);
+
+// screen-tabbar.c
+void print_tabbar(Terminal *term, const ColorScheme *colors, Window *window);
+
+// screen-status.c
+void update_status_line(const Window *window);
+
+// screen-view.c
+void update_range(EditorState *e, const View *view, long y1, long y2);
+
+// screen-window.c
+void update_all_windows(EditorState *e);
+void update_buffer_windows(EditorState *e, const Buffer *buffer);
+
+// screen-prompt.c
+char status_prompt(EditorState *e, const char *question, const char *choices) NONNULL_ARGS;
+char dialog_prompt(EditorState *e, const char *question, const char *choices) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/search.c b/examples/dte/search.c
new file mode 100644
index 0000000..80ad5c3
--- /dev/null
+++ b/examples/dte/search.c
@@ -0,0 +1,244 @@
+#include <stdlib.h>
+#include "search.h"
+#include "block-iter.h"
+#include "buffer.h"
+#include "error.h"
+#include "regexp.h"
+#include "util/ascii.h"
+#include "util/xmalloc.h"
+
+static bool do_search_fwd(View *view, regex_t *regex, BlockIter *bi, bool skip)
+{
+ int flags = block_iter_is_bol(bi) ? 0 : REG_NOTBOL;
+
+ do {
+ if (block_iter_is_eof(bi)) {
+ return false;
+ }
+
+ regmatch_t match;
+ StringView line;
+ fill_line_ref(bi, &line);
+
+ // NOTE: If this is the first iteration then line.data contains
+ // partial line (text starting from the cursor position) and
+ // if match.rm_so is 0 then match is at beginning of the text
+ // which is same as the cursor position.
+ if (regexp_exec(regex, line.data, line.length, 1, &match, flags)) {
+ if (skip && match.rm_so == 0) {
+ // Ignore match at current cursor position
+ regoff_t count = match.rm_eo;
+ if (count == 0) {
+ // It is safe to skip one byte because every line
+ // has one extra byte (newline) that is not in line.data
+ count = 1;
+ }
+ block_iter_skip_bytes(bi, (size_t)count);
+ return do_search_fwd(view, regex, bi, false);
+ }
+
+ block_iter_skip_bytes(bi, match.rm_so);
+ view->cursor = *bi;
+ view->center_on_scroll = true;
+ view_reset_preferred_x(view);
+ return true;
+ }
+
+ skip = false; // Not at cursor position any more
+ flags = 0;
+ } while (block_iter_next_line(bi));
+
+ return false;
+}
+
+static bool do_search_bwd(View *view, regex_t *regex, BlockIter *bi, ssize_t cx, bool skip)
+{
+ if (block_iter_is_eof(bi)) {
+ goto next;
+ }
+
+ do {
+ regmatch_t match;
+ StringView line;
+ int flags = 0;
+ regoff_t offset = -1;
+ regoff_t pos = 0;
+
+ fill_line_ref(bi, &line);
+ while (
+ pos <= line.length
+ && regexp_exec(regex, line.data + pos, line.length - pos, 1, &match, flags)
+ ) {
+ flags = REG_NOTBOL;
+ if (cx >= 0) {
+ if (pos + match.rm_so >= cx) {
+ // Ignore match at or after cursor
+ break;
+ }
+ if (skip && pos + match.rm_eo > cx) {
+ // Search -rw should not find word under cursor
+ break;
+ }
+ }
+
+ // This might be what we want (last match before cursor)
+ offset = pos + match.rm_so;
+ pos += match.rm_eo;
+
+ if (match.rm_so == match.rm_eo) {
+ // Zero length match
+ break;
+ }
+ }
+
+ if (offset >= 0) {
+ block_iter_skip_bytes(bi, offset);
+ view->cursor = *bi;
+ view->center_on_scroll = true;
+ view_reset_preferred_x(view);
+ return true;
+ }
+
+ next:
+ cx = -1;
+ } while (block_iter_prev_line(bi));
+
+ return false;
+}
+
+bool search_tag(View *view, const char *pattern)
+{
+ regex_t regex;
+ if (!regexp_compile_basic(&regex, pattern, REG_NEWLINE)) {
+ return false;
+ }
+
+ BlockIter bi = block_iter(view->buffer);
+ bool found = do_search_fwd(view, &regex, &bi, false);
+ regfree(&regex);
+
+ if (!found) {
+ // Don't center view to cursor unnecessarily
+ view->force_center = false;
+ return error_msg("Tag not found");
+ }
+
+ view->center_on_scroll = true;
+ return true;
+}
+
+static void free_regex(SearchState *search)
+{
+ if (search->re_flags) {
+ regfree(&search->regex);
+ search->re_flags = 0;
+ }
+}
+
+static bool has_upper(const char *str)
+{
+ for (size_t i = 0; str[i]; i++) {
+ if (ascii_isupper(str[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+static bool update_regex(SearchState *search, SearchCaseSensitivity cs)
+{
+ int re_flags = REG_NEWLINE;
+ switch (cs) {
+ case CSS_TRUE:
+ break;
+ case CSS_FALSE:
+ re_flags |= REG_ICASE;
+ break;
+ case CSS_AUTO:
+ if (!has_upper(search->pattern)) {
+ re_flags |= REG_ICASE;
+ }
+ break;
+ default:
+ BUG("unhandled case sensitivity value");
+ }
+
+ if (re_flags == search->re_flags) {
+ return true;
+ }
+
+ free_regex(search);
+
+ search->re_flags = re_flags;
+ if (regexp_compile(&search->regex, search->pattern, search->re_flags)) {
+ return true;
+ }
+
+ free_regex(search);
+ return false;
+}
+
+void search_free_regexp(SearchState *search)
+{
+ free_regex(search);
+ free(search->pattern);
+}
+
+void search_set_regexp(SearchState *search, const char *pattern)
+{
+ search_free_regexp(search);
+ search->pattern = xstrdup(pattern);
+}
+
+static bool do_search_next(View *view, SearchState *search, SearchCaseSensitivity cs, bool skip)
+{
+ if (!search->pattern) {
+ return error_msg("No previous search pattern");
+ }
+ if (!update_regex(search, cs)) {
+ return false;
+ }
+
+ BlockIter bi = view->cursor;
+ regex_t *regex = &search->regex;
+ if (!search->reverse) {
+ if (do_search_fwd(view, regex, &bi, true)) {
+ return true;
+ }
+ block_iter_bof(&bi);
+ if (do_search_fwd(view, regex, &bi, false)) {
+ info_msg("Continuing at top");
+ return true;
+ }
+ } else {
+ size_t cursor_x = block_iter_bol(&bi);
+ if (do_search_bwd(view, regex, &bi, cursor_x, skip)) {
+ return true;
+ }
+ block_iter_eof(&bi);
+ if (do_search_bwd(view, regex, &bi, -1, false)) {
+ info_msg("Continuing at bottom");
+ return true;
+ }
+ }
+
+ return error_msg("Pattern '%s' not found", search->pattern);
+}
+
+bool search_prev(View *view, SearchState *search, SearchCaseSensitivity cs)
+{
+ toggle_search_direction(search);
+ bool r = search_next(view, search, cs);
+ toggle_search_direction(search);
+ return r;
+}
+
+bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs)
+{
+ return do_search_next(view, search, cs, false);
+}
+
+bool search_next_word(View *view, SearchState *search, SearchCaseSensitivity cs)
+{
+ return do_search_next(view, search, cs, true);
+}
diff --git a/examples/dte/search.h b/examples/dte/search.h
new file mode 100644
index 0000000..94d3a57
--- /dev/null
+++ b/examples/dte/search.h
@@ -0,0 +1,34 @@
+#ifndef SEARCH_H
+#define SEARCH_H
+
+#include <regex.h>
+#include <stdbool.h>
+#include "util/macros.h"
+#include "view.h"
+
+typedef enum {
+ CSS_FALSE,
+ CSS_TRUE,
+ CSS_AUTO,
+} SearchCaseSensitivity;
+
+typedef struct {
+ regex_t regex;
+ char *pattern;
+ int re_flags; // If zero, regex hasn't been compiled
+ bool reverse;
+} SearchState;
+
+static inline void toggle_search_direction(SearchState *search)
+{
+ search->reverse ^= 1;
+}
+
+bool search_tag(View *view, const char *pattern) NONNULL_ARGS WARN_UNUSED_RESULT;
+void search_set_regexp(SearchState *search, const char *pattern) NONNULL_ARGS;
+void search_free_regexp(SearchState *search) NONNULL_ARGS;
+bool search_prev(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT;
+bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT;
+bool search_next_word(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT;
+
+#endif
diff --git a/examples/dte/selection.c b/examples/dte/selection.c
new file mode 100644
index 0000000..5ddc67c
--- /dev/null
+++ b/examples/dte/selection.c
@@ -0,0 +1,110 @@
+#include "selection.h"
+#include "editor.h"
+#include "util/unicode.h"
+
+static bool include_cursor_char_in_selection(const View *view)
+{
+ const EditorState *e = view->window->editor;
+ if (!e->options.select_cursor_char) {
+ return false;
+ }
+
+ bool overwrite = view->buffer->options.overwrite;
+ CursorInputMode mode = overwrite ? CURSOR_MODE_OVERWRITE : CURSOR_MODE_INSERT;
+ TermCursorType type = e->cursor_styles[mode].type;
+ if (type == CURSOR_KEEP) {
+ type = e->cursor_styles[CURSOR_MODE_DEFAULT].type;
+ }
+
+ // If "select-cursor-char" option is true, include character under cursor
+ // in selections for any cursor type except bars (where it makes no sense
+ // to do so)
+ return !(type == CURSOR_STEADY_BAR || type == CURSOR_BLINKING_BAR);
+}
+
+void init_selection(const View *view, SelectionInfo *info)
+{
+ info->so = view->sel_so;
+ info->eo = block_iter_get_offset(&view->cursor);
+ info->si = view->cursor;
+ block_iter_goto_offset(&info->si, info->so);
+ info->swapped = false;
+ if (info->so > info->eo) {
+ size_t o = info->so;
+ info->so = info->eo;
+ info->eo = o;
+ info->si = view->cursor;
+ info->swapped = true;
+ }
+
+ BlockIter ei = info->si;
+ block_iter_skip_bytes(&ei, info->eo - info->so);
+ if (block_iter_is_eof(&ei)) {
+ if (info->so == info->eo) {
+ return;
+ }
+ CodePoint u;
+ info->eo -= block_iter_prev_char(&ei, &u);
+ }
+
+ if (view->selection == SELECT_LINES) {
+ info->so -= block_iter_bol(&info->si);
+ info->eo += block_iter_eat_line(&ei);
+ } else {
+ if (include_cursor_char_in_selection(view)) {
+ info->eo += block_iter_next_column(&ei);
+ }
+ }
+}
+
+size_t prepare_selection(View *view)
+{
+ SelectionInfo info;
+ init_selection(view, &info);
+ view->cursor = info.si;
+ return info.eo - info.so;
+}
+
+char *view_get_selection(View *view, size_t *size)
+{
+ if (view->selection == SELECT_NONE) {
+ *size = 0;
+ return NULL;
+ }
+
+ BlockIter save = view->cursor;
+ *size = prepare_selection(view);
+ char *buf = block_iter_get_bytes(&view->cursor, *size);
+ view->cursor = save;
+ return buf;
+}
+
+size_t get_nr_selected_lines(const SelectionInfo *info)
+{
+ BlockIter bi = info->si;
+ size_t pos = info->so;
+ CodePoint u = 0;
+ size_t nr_lines = 1;
+
+ while (pos < info->eo) {
+ if (u == '\n') {
+ nr_lines++;
+ }
+ pos += block_iter_next_char(&bi, &u);
+ }
+ return nr_lines;
+}
+
+size_t get_nr_selected_chars(const SelectionInfo *info)
+{
+ BlockIter bi = info->si;
+ size_t pos = info->so;
+ CodePoint u;
+ size_t nr_chars = 0;
+
+ while (pos < info->eo) {
+ nr_chars++;
+ pos += block_iter_next_char(&bi, &u);
+ }
+ return nr_chars;
+}
diff --git a/examples/dte/selection.h b/examples/dte/selection.h
new file mode 100644
index 0000000..ddd60c5
--- /dev/null
+++ b/examples/dte/selection.h
@@ -0,0 +1,22 @@
+#ifndef SELECTION_H
+#define SELECTION_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "block-iter.h"
+#include "view.h"
+
+typedef struct {
+ BlockIter si;
+ size_t so;
+ size_t eo;
+ bool swapped;
+} SelectionInfo;
+
+void init_selection(const View *view, SelectionInfo *info);
+size_t prepare_selection(View *view);
+char *view_get_selection(View *view, size_t *size);
+size_t get_nr_selected_lines(const SelectionInfo *info);
+size_t get_nr_selected_chars(const SelectionInfo *info);
+
+#endif
diff --git a/examples/dte/shift.c b/examples/dte/shift.c
new file mode 100644
index 0000000..6276d3c
--- /dev/null
+++ b/examples/dte/shift.c
@@ -0,0 +1,147 @@
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include "shift.h"
+#include "block-iter.h"
+#include "buffer.h"
+#include "change.h"
+#include "indent.h"
+#include "move.h"
+#include "options.h"
+#include "selection.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/xmalloc.h"
+
+static char *alloc_indent(const LocalOptions *options, size_t count, size_t *sizep)
+{
+ bool use_spaces = use_spaces_for_indent(options);
+ size_t size = use_spaces ? count * options->indent_width : count;
+ *sizep = size;
+ return memset(xmalloc(size), use_spaces ? ' ' : '\t', size);
+}
+
+static void shift_right(View *view, size_t nr_lines, size_t count)
+{
+ const LocalOptions *options = &view->buffer->options;
+ size_t indent_size;
+ char *indent = alloc_indent(options, count, &indent_size);
+
+ for (size_t i = 0; true; ) {
+ StringView line;
+ fetch_this_line(&view->cursor, &line);
+ IndentInfo info = get_indent_info(options, &line);
+ if (info.wsonly) {
+ if (info.bytes) {
+ // Remove indentation
+ buffer_delete_bytes(view, info.bytes);
+ }
+ } else if (info.sane) {
+ // Insert whitespace
+ buffer_insert_bytes(view, indent, indent_size);
+ } else {
+ // Replace whole indentation with sane one
+ size_t size;
+ char *buf = alloc_indent(options, info.level + count, &size);
+ buffer_replace_bytes(view, info.bytes, buf, size);
+ free(buf);
+ }
+ if (++i == nr_lines) {
+ break;
+ }
+ block_iter_eat_line(&view->cursor);
+ }
+
+ free(indent);
+}
+
+static void shift_left(View *view, size_t nr_lines, size_t count)
+{
+ const LocalOptions *options = &view->buffer->options;
+ const size_t indent_width = options->indent_width;
+ const bool space_indent = use_spaces_for_indent(options);
+
+ for (size_t i = 0; true; ) {
+ StringView line;
+ fetch_this_line(&view->cursor, &line);
+ IndentInfo info = get_indent_info(options, &line);
+ if (info.wsonly) {
+ if (info.bytes) {
+ // Remove indentation
+ buffer_delete_bytes(view, info.bytes);
+ }
+ } else if (info.level && info.sane) {
+ size_t n = MIN(count, info.level);
+ if (space_indent) {
+ n *= indent_width;
+ }
+ buffer_delete_bytes(view, n);
+ } else if (info.bytes) {
+ // Replace whole indentation with sane one
+ if (info.level > count) {
+ size_t size;
+ char *buf = alloc_indent(options, info.level - count, &size);
+ buffer_replace_bytes(view, info.bytes, buf, size);
+ free(buf);
+ } else {
+ buffer_delete_bytes(view, info.bytes);
+ }
+ }
+ if (++i == nr_lines) {
+ break;
+ }
+ block_iter_eat_line(&view->cursor);
+ }
+}
+
+static void do_shift_lines(View *view, int count, size_t nr_lines)
+{
+ begin_change_chain();
+ block_iter_bol(&view->cursor);
+ if (count > 0) {
+ shift_right(view, nr_lines, count);
+ } else {
+ shift_left(view, nr_lines, -count);
+ }
+ end_change_chain(view);
+}
+
+void shift_lines(View *view, int count)
+{
+ unsigned int width = view->buffer->options.indent_width;
+ BUG_ON(width > INDENT_WIDTH_MAX);
+ BUG_ON(count == 0);
+
+ long x = view_get_preferred_x(view) + (count * width);
+ x = MAX(x, 0);
+
+ if (view->selection == SELECT_NONE) {
+ do_shift_lines(view, count, 1);
+ goto out;
+ }
+
+ SelectionInfo info;
+ view->selection = SELECT_LINES;
+ init_selection(view, &info);
+ view->cursor = info.si;
+ size_t nr_lines = get_nr_selected_lines(&info);
+ do_shift_lines(view, count, nr_lines);
+ if (info.swapped) {
+ // Cursor should be at beginning of selection
+ block_iter_bol(&view->cursor);
+ view->sel_so = block_iter_get_offset(&view->cursor);
+ while (--nr_lines) {
+ block_iter_prev_line(&view->cursor);
+ }
+ } else {
+ BlockIter save = view->cursor;
+ while (--nr_lines) {
+ block_iter_prev_line(&view->cursor);
+ }
+ view->sel_so = block_iter_get_offset(&view->cursor);
+ view->cursor = save;
+ }
+
+out:
+ move_to_preferred_x(view, x);
+}
diff --git a/examples/dte/shift.h b/examples/dte/shift.h
new file mode 100644
index 0000000..92da552
--- /dev/null
+++ b/examples/dte/shift.h
@@ -0,0 +1,8 @@
+#ifndef SHIFT_H
+#define SHIFT_H
+
+#include "view.h"
+
+void shift_lines(View *view, int count);
+
+#endif
diff --git a/examples/dte/show.c b/examples/dte/show.c
new file mode 100644
index 0000000..69a6acf
--- /dev/null
+++ b/examples/dte/show.c
@@ -0,0 +1,558 @@
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include "show.h"
+#include "bind.h"
+#include "buffer.h"
+#include "change.h"
+#include "cmdline.h"
+#include "command/alias.h"
+#include "command/macro.h"
+#include "command/serialize.h"
+#include "commands.h"
+#include "compiler.h"
+#include "completion.h"
+#include "config.h"
+#include "edit.h"
+#include "encoding.h"
+#include "error.h"
+#include "file-option.h"
+#include "filetype.h"
+#include "frame.h"
+#include "msg.h"
+#include "options.h"
+#include "syntax/color.h"
+#include "terminal/cursor.h"
+#include "terminal/key.h"
+#include "terminal/style.h"
+#include "util/array.h"
+#include "util/bsearch.h"
+#include "util/debug.h"
+#include "util/intern.h"
+#include "util/str-util.h"
+#include "util/unicode.h"
+#include "util/xmalloc.h"
+#include "util/xsnprintf.h"
+#include "view.h"
+#include "window.h"
+
+extern char **environ;
+
+typedef enum {
+ DTERC = 0x1, // Use "dte" filetype (and syntax highlighter)
+ LASTLINE = 0x2, // Move cursor to last line (e.g. most recent history entry)
+ MSGLINE = 0x4, // Move cursor to line containing current message
+} ShowHandlerFlags;
+
+typedef struct {
+ const char name[11];
+ uint8_t flags; // ShowHandlerFlags
+ String (*dump)(EditorState *e);
+ bool (*show)(EditorState *e, const char *name, bool cmdline);
+ void (*complete_arg)(EditorState *e, PointerArray *a, const char *prefix);
+} ShowHandler;
+
+static void open_temporary_buffer (
+ EditorState *e,
+ const char *text,
+ size_t text_len,
+ const char *cmd,
+ const char *cmd_arg,
+ ShowHandlerFlags flags
+) {
+ View *view = window_open_new_file(e->window);
+ Buffer *buffer = view->buffer;
+ buffer->temporary = true;
+ do_insert(view, text, text_len);
+ set_display_filename(buffer, xasprintf("(%s %s)", cmd, cmd_arg));
+ buffer_set_encoding(buffer, encoding_from_type(UTF8), e->options.utf8_bom);
+
+ if (flags & LASTLINE) {
+ block_iter_eof(&view->cursor);
+ block_iter_prev_line(&view->cursor);
+ } else if ((flags & MSGLINE) && e->messages.array.count > 0) {
+ block_iter_goto_line(&view->cursor, e->messages.pos);
+ }
+
+ if (flags & DTERC) {
+ buffer->options.filetype = str_intern("dte");
+ set_file_options(e, buffer);
+ buffer_update_syntax(e, buffer);
+ }
+}
+
+static bool show_normal_alias(EditorState *e, const char *alias_name, bool cflag)
+{
+ const char *cmd_str = find_alias(&e->aliases, alias_name);
+ if (!cmd_str) {
+ if (find_normal_command(alias_name)) {
+ info_msg("%s is a built-in command, not an alias", alias_name);
+ } else {
+ info_msg("%s is not a known alias", alias_name);
+ }
+ return true;
+ }
+
+ if (cflag) {
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, cmd_str);
+ } else {
+ info_msg("%s is aliased to: %s", alias_name, cmd_str);
+ }
+
+ return true;
+}
+
+static bool show_binding(EditorState *e, const char *keystr, bool cflag)
+{
+ KeyCode key;
+ if (!parse_key_string(&key, keystr)) {
+ return error_msg("invalid key string: %s", keystr);
+ }
+
+ // Use canonical key string in printed messages
+ char buf[KEYCODE_STR_MAX];
+ size_t len = keycode_to_string(key, buf);
+ BUG_ON(len == 0);
+ keystr = buf;
+
+ if (u_is_unicode(key)) {
+ return error_msg("%s is not a bindable key", keystr);
+ }
+
+ const CachedCommand *b = lookup_binding(&e->modes[INPUT_NORMAL].key_bindings, key);
+ if (!b) {
+ info_msg("%s is not bound to a command", keystr);
+ return true;
+ }
+
+ if (cflag) {
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, b->cmd_str);
+ } else {
+ info_msg("%s is bound to: %s", keystr, b->cmd_str);
+ }
+
+ return true;
+}
+
+static bool show_color(EditorState *e, const char *color_name, bool cflag)
+{
+ const TermColor *hl = find_color(&e->colors, color_name);
+ if (!hl) {
+ info_msg("no color entry with name '%s'", color_name);
+ return true;
+ }
+
+ if (cflag) {
+ CommandLine *c = &e->cmdline;
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_clear(c);
+ string_append_hl_color(&c->buf, color_name, hl);
+ c->pos = c->buf.len;
+ } else {
+ const char *color_str = term_color_to_string(hl);
+ info_msg("color '%s' is set to: %s", color_name, color_str);
+ }
+
+ return true;
+}
+
+static bool show_cursor(EditorState *e, const char *mode_str, bool cflag)
+{
+ CursorInputMode mode = cursor_mode_from_str(mode_str);
+ if (mode >= NR_CURSOR_MODES) {
+ return error_msg("no cursor entry for '%s'", mode_str);
+ }
+
+ TermCursorStyle style = e->cursor_styles[mode];
+ const char *type = cursor_type_to_str(style.type);
+ const char *color = cursor_color_to_str(style.color);
+ if (cflag) {
+ char buf[64];
+ xsnprintf(buf, sizeof buf, "cursor %s %s %s", mode_str, type, color);
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, buf);
+ } else {
+ info_msg("cursor '%s' is set to: %s %s", mode_str, type, color);
+ }
+
+ return true;
+}
+
+static bool show_env(EditorState *e, const char *name, bool cflag)
+{
+ const char *value = getenv(name);
+ if (!value) {
+ info_msg("no environment variable with name '%s'", name);
+ return true;
+ }
+
+ if (cflag) {
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, value);
+ } else {
+ info_msg("$%s is set to: %s", name, value);
+ }
+
+ return true;
+}
+
+static String dump_env(EditorState* UNUSED_ARG(e))
+{
+ String buf = string_new(4096);
+ for (size_t i = 0; environ[i]; i++) {
+ string_append_cstring(&buf, environ[i]);
+ string_append_byte(&buf, '\n');
+ }
+ return buf;
+}
+
+static String dump_setenv(EditorState* UNUSED_ARG(e))
+{
+ String buf = string_new(4096);
+ for (size_t i = 0; environ[i]; i++) {
+ const char *str = environ[i];
+ const char *delim = strchr(str, '=');
+ if (unlikely(!delim || delim == str)) {
+ continue;
+ }
+ string_append_literal(&buf, "setenv ");
+ if (unlikely(str[0] == '-' || delim[1] == '-')) {
+ string_append_literal(&buf, "-- ");
+ }
+ const StringView name = string_view(str, delim - str);
+ string_append_escaped_arg_sv(&buf, name, true);
+ string_append_byte(&buf, ' ');
+ string_append_escaped_arg(&buf, delim + 1, true);
+ string_append_byte(&buf, '\n');
+ }
+ return buf;
+}
+
+static bool show_builtin(EditorState *e, const char *name, bool cflag)
+{
+ const BuiltinConfig *cfg = get_builtin_config(name);
+ if (!cfg) {
+ return error_msg("no built-in config with name '%s'", name);
+ }
+
+ const StringView sv = cfg->text;
+ if (cflag) {
+ buffer_insert_bytes(e->view, sv.data, sv.length);
+ } else {
+ open_temporary_buffer(e, sv.data, sv.length, "builtin", name, DTERC);
+ }
+
+ return true;
+}
+
+static bool show_compiler(EditorState *e, const char *name, bool cflag)
+{
+ const Compiler *compiler = find_compiler(&e->compilers, name);
+ if (!compiler) {
+ info_msg("no errorfmt entry found for '%s'", name);
+ return true;
+ }
+
+ String str = string_new(512);
+ dump_compiler(compiler, name, &str);
+ if (cflag) {
+ buffer_insert_bytes(e->view, str.buffer, str.len);
+ } else {
+ open_temporary_buffer(e, str.buffer, str.len, "errorfmt", name, DTERC);
+ }
+
+ string_free(&str);
+ return true;
+}
+
+static bool show_option(EditorState *e, const char *name, bool cflag)
+{
+ const char *value = get_option_value_string(e, name);
+ if (!value) {
+ return error_msg("invalid option name: %s", name);
+ }
+
+ if (cflag) {
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, value);
+ } else {
+ info_msg("%s is set to: %s", name, value);
+ }
+
+ return true;
+}
+
+static void collect_all_options(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
+{
+ collect_options(a, prefix, false, false);
+}
+
+static void do_collect_cursor_modes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
+{
+ collect_cursor_modes(a, prefix);
+}
+
+static void do_collect_builtin_configs(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
+{
+ collect_builtin_configs(a, prefix);
+}
+
+static void do_collect_builtin_includes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
+{
+ collect_builtin_includes(a, prefix);
+}
+
+static bool show_wsplit(EditorState *e, const char *name, bool cflag)
+{
+ if (!streq(name, "this")) {
+ return error_msg("invalid window: %s", name);
+ }
+
+ const Window *w = e->window;
+ char buf[(4 * DECIMAL_STR_MAX(w->x)) + 4];
+ xsnprintf(buf, sizeof buf, "%d,%d %dx%d", w->x, w->y, w->w, w->h);
+
+ if (cflag) {
+ set_input_mode(e, INPUT_COMMAND);
+ cmdline_set_text(&e->cmdline, buf);
+ } else {
+ info_msg("current window dimensions: %s", buf);
+ }
+
+ return true;
+}
+
+static String do_history_dump(const History *history)
+{
+ const size_t nr_entries = history->entries.count;
+ const size_t size = round_size_to_next_multiple(16 * nr_entries, 4096);
+ String buf = string_new(size);
+ size_t n = 0;
+ for (HistoryEntry *e = history->first; e; e = e->next, n++) {
+ string_append_cstring(&buf, e->text);
+ string_append_byte(&buf, '\n');
+ }
+ BUG_ON(n != nr_entries);
+ return buf;
+}
+
+String dump_command_history(EditorState *e)
+{
+ return do_history_dump(&e->command_history);
+}
+
+String dump_search_history(EditorState *e)
+{
+ return do_history_dump(&e->search_history);
+}
+
+typedef struct {
+ const char *name;
+ const char *value;
+} CommandAlias;
+
+static int alias_cmp(const void *ap, const void *bp)
+{
+ const CommandAlias *a = ap;
+ const CommandAlias *b = bp;
+ return strcmp(a->name, b->name);
+}
+
+String dump_normal_aliases(EditorState *e)
+{
+ const size_t count = e->aliases.count;
+ if (unlikely(count == 0)) {
+ return string_new(0);
+ }
+
+ // Clone the contents of the HashMap as an array of name/value pairs
+ CommandAlias *array = xnew(CommandAlias, count);
+ size_t n = 0;
+ for (HashMapIter it = hashmap_iter(&e->aliases); hashmap_next(&it); ) {
+ array[n++] = (CommandAlias) {
+ .name = it.entry->key,
+ .value = it.entry->value,
+ };
+ }
+
+ // Sort the array
+ BUG_ON(n != count);
+ qsort(array, count, sizeof(array[0]), alias_cmp);
+
+ // Serialize the aliases in sorted order
+ String buf = string_new(4096);
+ for (size_t i = 0; i < count; i++) {
+ const char *name = array[i].name;
+ string_append_literal(&buf, "alias ");
+ if (unlikely(name[0] == '-')) {
+ string_append_literal(&buf, "-- ");
+ }
+ string_append_escaped_arg(&buf, name, true);
+ string_append_byte(&buf, ' ');
+ string_append_escaped_arg(&buf, array[i].value, true);
+ string_append_byte(&buf, '\n');
+ }
+
+ free(array);
+ return buf;
+}
+
+String dump_all_bindings(EditorState *e)
+{
+ static const char flags[][4] = {
+ [INPUT_NORMAL] = "",
+ [INPUT_COMMAND] = "-c ",
+ [INPUT_SEARCH] = "-s ",
+ };
+
+ static_assert(ARRAYLEN(flags) == ARRAYLEN(e->modes));
+ String buf = string_new(4096);
+ for (InputMode i = 0, n = ARRAYLEN(e->modes); i < n; i++) {
+ const IntMap *bindings = &e->modes[i].key_bindings;
+ if (dump_bindings(bindings, flags[i], &buf) && i != n - 1) {
+ string_append_byte(&buf, '\n');
+ }
+ }
+ return buf;
+}
+
+String dump_frames(EditorState *e)
+{
+ String str = string_new(4096);
+ dump_frame(e->root_frame, 0, &str);
+ return str;
+}
+
+String dump_compilers(EditorState *e)
+{
+ String buf = string_new(4096);
+ for (HashMapIter it = hashmap_iter(&e->compilers); hashmap_next(&it); ) {
+ const char *name = it.entry->key;
+ const Compiler *c = it.entry->value;
+ dump_compiler(c, name, &buf);
+ string_append_byte(&buf, '\n');
+ }
+ return buf;
+}
+
+String dump_cursors(EditorState *e)
+{
+ String buf = string_new(128);
+ for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) {
+ const TermCursorStyle *style = &e->cursor_styles[m];
+ string_append_literal(&buf, "cursor ");
+ string_append_cstring(&buf, cursor_mode_to_str(m));
+ string_append_byte(&buf, ' ');
+ string_append_cstring(&buf, cursor_type_to_str(style->type));
+ string_append_byte(&buf, ' ');
+ string_append_cstring(&buf, cursor_color_to_str(style->color));
+ string_append_byte(&buf, '\n');
+ }
+ return buf;
+}
+
+// Dump option values only
+String do_dump_options(EditorState *e)
+{
+ return dump_options(&e->options, &e->buffer->options);
+}
+
+// Dump option values and FileOption entries
+String dump_options_and_fileopts(EditorState *e)
+{
+ String str = do_dump_options(e);
+ string_append_literal(&str, "\n\n");
+ dump_file_options(&e->file_options, &str);
+ return str;
+}
+
+String do_dump_builtin_configs(EditorState* UNUSED_ARG(e))
+{
+ return dump_builtin_configs();
+}
+
+String do_dump_hl_colors(EditorState *e)
+{
+ return dump_hl_colors(&e->colors);
+}
+
+String do_dump_filetypes(EditorState *e)
+{
+ return dump_filetypes(&e->filetypes);
+}
+
+static String do_dump_messages(EditorState *e)
+{
+ return dump_messages(&e->messages);
+}
+
+static String do_dump_macro(EditorState *e)
+{
+ return dump_macro(&e->macro);
+}
+
+static String do_dump_buffer(EditorState *e)
+{
+ return dump_buffer(e->buffer);
+}
+
+static const ShowHandler show_handlers[] = {
+ {"alias", DTERC, dump_normal_aliases, show_normal_alias, collect_normal_aliases},
+ {"bind", DTERC, dump_all_bindings, show_binding, collect_bound_normal_keys},
+ {"buffer", 0, do_dump_buffer, NULL, NULL},
+ {"builtin", 0, do_dump_builtin_configs, show_builtin, do_collect_builtin_configs},
+ {"color", DTERC, do_dump_hl_colors, show_color, collect_hl_colors},
+ {"command", DTERC | LASTLINE, dump_command_history, NULL, NULL},
+ {"cursor", DTERC, dump_cursors, show_cursor, do_collect_cursor_modes},
+ {"env", 0, dump_env, show_env, collect_env},
+ {"errorfmt", DTERC, dump_compilers, show_compiler, collect_compilers},
+ {"ft", DTERC, do_dump_filetypes, NULL, NULL},
+ {"hi", DTERC, do_dump_hl_colors, show_color, collect_hl_colors},
+ {"include", 0, do_dump_builtin_configs, show_builtin, do_collect_builtin_includes},
+ {"macro", DTERC, do_dump_macro, NULL, NULL},
+ {"msg", MSGLINE, do_dump_messages, NULL, NULL},
+ {"option", DTERC, dump_options_and_fileopts, show_option, collect_all_options},
+ {"search", LASTLINE, dump_search_history, NULL, NULL},
+ {"set", DTERC, do_dump_options, show_option, collect_all_options},
+ {"setenv", DTERC, dump_setenv, show_env, collect_env},
+ {"wsplit", 0, dump_frames, show_wsplit, NULL},
+};
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(show_handlers, name, strcmp);
+}
+
+bool show(EditorState *e, const char *type, const char *key, bool cflag)
+{
+ const ShowHandler *handler = BSEARCH(type, show_handlers, vstrcmp);
+ if (!handler) {
+ return error_msg("invalid argument: '%s'", type);
+ }
+
+ if (key) {
+ if (!handler->show) {
+ return error_msg("'show %s' doesn't take extra arguments", type);
+ }
+ return handler->show(e, key, cflag);
+ }
+
+ String str = handler->dump(e);
+ open_temporary_buffer(e, str.buffer, str.len, "show", type, handler->flags);
+ string_free(&str);
+ return true;
+}
+
+void collect_show_subcommands(PointerArray *a, const char *prefix)
+{
+ COLLECT_STRING_FIELDS(show_handlers, name, a, prefix);
+}
+
+void collect_show_subcommand_args(EditorState *e, PointerArray *a, const char *name, const char *arg_prefix)
+{
+ const ShowHandler *handler = BSEARCH(name, show_handlers, vstrcmp);
+ if (handler && handler->complete_arg) {
+ handler->complete_arg(e, a, arg_prefix);
+ }
+}
diff --git a/examples/dte/show.h b/examples/dte/show.h
new file mode 100644
index 0000000..8736c50
--- /dev/null
+++ b/examples/dte/show.h
@@ -0,0 +1,27 @@
+#ifndef SHOW_H
+#define SHOW_H
+
+#include <stdbool.h>
+#include "editor.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string.h"
+
+bool show(EditorState *e, const char *type, const char *key, bool cflag) NONNULL_ARG(1, 2) WARN_UNUSED_RESULT;
+void collect_show_subcommands(PointerArray *a, const char *prefix) NONNULL_ARGS;
+void collect_show_subcommand_args(EditorState *e, PointerArray *a, const char *name, const char *arg_prefix) NONNULL_ARGS;
+
+String dump_all_bindings(EditorState *e);
+String dump_command_history(EditorState *e);
+String dump_compilers(EditorState *e);
+String dump_cursors(EditorState *e);
+String dump_frames(EditorState *e);
+String dump_normal_aliases(EditorState *e);
+String dump_options_and_fileopts(EditorState *e);
+String dump_search_history(EditorState *e);
+String do_dump_builtin_configs(EditorState *e);
+String do_dump_filetypes(EditorState *e);
+String do_dump_hl_colors(EditorState *e);
+String do_dump_options(EditorState *e);
+
+#endif
diff --git a/examples/dte/signals.c b/examples/dte/signals.c
new file mode 100644
index 0000000..e1a7155
--- /dev/null
+++ b/examples/dte/signals.c
@@ -0,0 +1,169 @@
+#include "compat.h"
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include "signals.h"
+#include "util/debug.h"
+#include "util/exitcode.h"
+#include "util/log.h"
+#include "util/macros.h"
+
+volatile sig_atomic_t resized = 0;
+
+static const int ignored_signals[] = {
+ SIGINT, // Terminal interrupt (see: VINTR in termios(3))
+ SIGQUIT, // Terminal quit (see: VQUIT in termios(3))
+ SIGTSTP, // Terminal stop (see: VSUSP in termios(3))
+ SIGXFSZ, // File size limit exceeded (see: RLIMIT_FSIZE in getrlimit(3))
+ SIGPIPE, // Broken pipe (see: EPIPE error in write(3))
+ SIGUSR1, // User signal 1 (terminates by default, for no good reason)
+ SIGUSR2, // User signal 2 (as above)
+};
+
+static const int default_signals[] = {
+ SIGABRT, // Terminate (cleanup already done)
+ SIGCHLD, // Ignore (see: wait(3))
+ SIGURG, // Ignore
+ SIGTTIN, // Stop
+ SIGTTOU, // Stop
+ SIGCONT, // Continue
+};
+
+static const int fatal_signals[] = {
+ SIGBUS,
+ SIGFPE,
+ SIGILL,
+ SIGSEGV,
+ SIGSYS,
+ SIGTRAP,
+ SIGXCPU,
+ SIGALRM,
+ SIGVTALRM,
+ SIGHUP,
+ SIGTERM,
+#ifdef SIGPROF
+ SIGPROF,
+#endif
+#ifdef SIGEMT
+ SIGEMT,
+#endif
+};
+
+void handle_sigwinch(int UNUSED_ARG(signum))
+{
+ resized = 1;
+}
+
+static noreturn COLD void handle_fatal_signal(int signum)
+{
+ LOG_CRITICAL("received signal %d (%s)", signum, strsignal(signum));
+
+ // If `signum` is SIGHUP, there's no point in trying to clean up the
+ // state of the (disconnected) terminal
+ if (signum != SIGHUP) {
+ fatal_error_cleanup();
+ }
+
+ // Restore and unblock `signum` and then re-raise it, to ensure the
+ // termination status (as seen by e.g. waitpid(3) in the parent) is
+ // set appropriately
+ struct sigaction sa = {.sa_handler = SIG_DFL};
+ if (
+ sigemptyset(&sa.sa_mask) == 0
+ && sigaction(signum, &sa, NULL) == 0
+ && sigaddset(&sa.sa_mask, signum) == 0
+ && sigprocmask(SIG_UNBLOCK, &sa.sa_mask, NULL) == 0
+ ) {
+ raise(signum);
+ }
+
+ // This is here just to make extra certain the handler never returns.
+ // If everything is working correctly, this code should be unreachable.
+ raise(SIGKILL);
+ _exit(EX_OSERR);
+}
+
+// strsignal(3) is fine in situations where a signal is being reported
+// as terminating a process, but it tends to be confusing in most other
+// circumstances, where the signal name (not description) is usually
+// clearer
+static const char *signum_to_str(int signum)
+{
+#if HAVE_SIG2STR
+ static char buf[SIG2STR_MAX + 3];
+ if (sig2str(signum, buf + 3) == 0) {
+ return memcpy(buf, "SIG", 3);
+ }
+#elif HAVE_SIGABBREV_NP
+ static char buf[16];
+ const char *abbr = sigabbrev_np(signum);
+ if (abbr && memccpy(buf + 3, abbr, '\0', sizeof(buf) - 3)) {
+ return memcpy(buf, "SIG", 3);
+ }
+#endif
+
+ const char *str = strsignal(signum);
+ return likely(str) ? str : "??";
+}
+
+static void do_sigaction(int sig, const struct sigaction *action)
+{
+ struct sigaction old_action;
+ if (unlikely(sigaction(sig, action, &old_action) != 0)) {
+ const char *err = strerror(errno);
+ LOG_ERROR("failed to set disposition for signal %d: %s", sig, err);
+ return;
+ }
+ if (unlikely(old_action.sa_handler == SIG_IGN)) {
+ const char *str = signum_to_str(sig);
+ LOG_WARNING("ignored signal was inherited: %d (%s)", sig, str);
+ }
+}
+
+/*
+ * "A program that uses these functions should be written to catch all
+ * signals and take other appropriate actions to ensure that when the
+ * program terminates, whether planned or not, the terminal device's
+ * state is restored to its original state."
+ *
+ * (https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcgetattr.html)
+ */
+void set_signal_handlers(void)
+{
+ struct sigaction action = {.sa_handler = handle_fatal_signal};
+ sigfillset(&action.sa_mask);
+ for (size_t i = 0; i < ARRAYLEN(fatal_signals); i++) {
+ do_sigaction(fatal_signals[i], &action);
+ }
+
+ // "The default actions for the realtime signals in the range SIGRTMIN
+ // to SIGRTMAX shall be to terminate the process abnormally."
+ // (POSIX.1-2017 §2.4.3)
+#if defined(SIGRTMIN) && defined(SIGRTMAX)
+ for (int s = SIGRTMIN, max = SIGRTMAX; s <= max; s++) {
+ do_sigaction(s, &action);
+ }
+#endif
+
+ action.sa_handler = SIG_IGN;
+ for (size_t i = 0; i < ARRAYLEN(ignored_signals); i++) {
+ do_sigaction(ignored_signals[i], &action);
+ }
+
+ action.sa_handler = SIG_DFL;
+ for (size_t i = 0; i < ARRAYLEN(default_signals); i++) {
+ do_sigaction(default_signals[i], &action);
+ }
+
+#if defined(SIGWINCH)
+ LOG_INFO("setting SIGWINCH handler");
+ action.sa_handler = handle_sigwinch;
+ do_sigaction(SIGWINCH, &action);
+#endif
+
+ // Set signal mask explicitly, to avoid any possibility of
+ // inheriting blocked signals
+ sigset_t mask;
+ sigemptyset(&mask);
+ sigprocmask(SIG_SETMASK, &mask, NULL);
+}
diff --git a/examples/dte/signals.h b/examples/dte/signals.h
new file mode 100644
index 0000000..de0859a
--- /dev/null
+++ b/examples/dte/signals.h
@@ -0,0 +1,11 @@
+#ifndef SIGNALS_H
+#define SIGNALS_H
+
+#include <signal.h>
+
+extern volatile sig_atomic_t resized;
+
+void set_signal_handlers(void);
+void handle_sigwinch(int signum);
+
+#endif
diff --git a/examples/dte/spawn.c b/examples/dte/spawn.c
new file mode 100644
index 0000000..0d9e0d6
--- /dev/null
+++ b/examples/dte/spawn.c
@@ -0,0 +1,396 @@
+#include <errno.h>
+#include <poll.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include "spawn.h"
+#include "error.h"
+#include "regexp.h"
+#include "terminal/mode.h"
+#include "util/debug.h"
+#include "util/fd.h"
+#include "util/fork-exec.h"
+#include "util/ptr-array.h"
+#include "util/str-util.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+#include "util/xstdio.h"
+
+static void handle_error_msg(const Compiler *c, MessageArray *msgs, char *str)
+{
+ if (str[0] == '\0' || str[0] == '\n') {
+ return;
+ }
+
+ size_t str_len = str_replace_byte(str, '\t', ' ');
+ if (str[str_len - 1] == '\n') {
+ str[--str_len] = '\0';
+ }
+
+ for (size_t i = 0, n = c->error_formats.count; i < n; i++) {
+ const ErrorFormat *p = c->error_formats.ptrs[i];
+ regmatch_t m[ERRORFMT_CAPTURE_MAX];
+ if (!regexp_exec(&p->re, str, str_len, ARRAYLEN(m), m, 0)) {
+ continue;
+ }
+ if (p->ignore) {
+ return;
+ }
+
+ int8_t mi = p->capture_index[ERRFMT_MESSAGE];
+ if (m[mi].rm_so < 0) {
+ mi = 0;
+ }
+
+ Message *msg = new_message(str + m[mi].rm_so, m[mi].rm_eo - m[mi].rm_so);
+ msg->loc = xnew0(FileLocation, 1);
+
+ int8_t fi = p->capture_index[ERRFMT_FILE];
+ if (fi >= 0 && m[fi].rm_so >= 0) {
+ msg->loc->filename = xstrslice(str, m[fi].rm_so, m[fi].rm_eo);
+
+ unsigned long *const ptrs[] = {
+ [ERRFMT_LINE] = &msg->loc->line,
+ [ERRFMT_COLUMN] = &msg->loc->column,
+ };
+
+ static_assert(ARRAYLEN(ptrs) == 3);
+ static_assert(ERRFMT_LINE == 1);
+ static_assert(ERRFMT_COLUMN == 2);
+
+ for (size_t j = ERRFMT_LINE; j < ARRAYLEN(ptrs); j++) {
+ int8_t ci = p->capture_index[j];
+ if (ci >= 0 && m[ci].rm_so >= 0) {
+ size_t len = m[ci].rm_eo - m[ci].rm_so;
+ unsigned long val;
+ if (len == buf_parse_ulong(str + m[ci].rm_so, len, &val)) {
+ *ptrs[j] = val;
+ }
+ }
+ }
+ }
+
+ add_message(msgs, msg);
+ return;
+ }
+
+ add_message(msgs, new_message(str, str_len));
+}
+
+static void read_errors(const Compiler *c, MessageArray *msgs, int fd, bool quiet)
+{
+ FILE *f = fdopen(fd, "r");
+ if (unlikely(!f)) {
+ return;
+ }
+ char line[4096];
+ while (xfgets(line, sizeof(line), f)) {
+ if (!quiet) {
+ xfputs(line, stderr);
+ }
+ handle_error_msg(c, msgs, line);
+ }
+ fclose(f);
+}
+
+static void handle_piped_data(int f[3], SpawnContext *ctx)
+{
+ BUG_ON(f[0] < 0 && f[1] < 0 && f[2] < 0);
+ BUG_ON(f[0] >= 0 && f[0] <= 2);
+ BUG_ON(f[1] >= 0 && f[1] <= 2);
+ BUG_ON(f[2] >= 0 && f[2] <= 2);
+
+ if (ctx->input.length == 0) {
+ xclose(f[0]);
+ f[0] = -1;
+ if (f[1] < 0 && f[2] < 0) {
+ return;
+ }
+ }
+
+ struct pollfd fds[] = {
+ {.fd = f[0], .events = POLLOUT},
+ {.fd = f[1], .events = POLLIN},
+ {.fd = f[2], .events = POLLIN},
+ };
+
+ size_t wlen = 0;
+ while (1) {
+ if (unlikely(poll(fds, ARRAYLEN(fds), -1) < 0)) {
+ if (errno == EINTR) {
+ continue;
+ }
+ error_msg_errno("poll");
+ return;
+ }
+
+ for (size_t i = 0; i < ARRAYLEN(ctx->outputs); i++) {
+ struct pollfd *pfd = fds + i + 1;
+ if (pfd->revents & POLLIN) {
+ String *output = &ctx->outputs[i];
+ char *buf = string_reserve_space(output, 4096);
+ ssize_t rc = xread(pfd->fd, buf, output->alloc - output->len);
+ if (unlikely(rc < 0)) {
+ error_msg_errno("read");
+ return;
+ }
+ if (rc == 0) { // EOF
+ if (xclose(pfd->fd)) {
+ error_msg_errno("close");
+ return;
+ }
+ pfd->fd = -1;
+ continue;
+ }
+ output->len += rc;
+ }
+ }
+
+ if (fds[0].revents & POLLOUT) {
+ ssize_t rc = xwrite(fds[0].fd, ctx->input.data + wlen, ctx->input.length - wlen);
+ if (unlikely(rc < 0)) {
+ error_msg_errno("write");
+ return;
+ }
+ wlen += (size_t) rc;
+ if (wlen == ctx->input.length) {
+ if (xclose(fds[0].fd)) {
+ error_msg_errno("close");
+ return;
+ }
+ fds[0].fd = -1;
+ }
+ }
+
+ size_t active_fds = ARRAYLEN(fds);
+ for (size_t i = 0; i < ARRAYLEN(fds); i++) {
+ int rev = fds[i].revents;
+ if (fds[i].fd < 0 || rev & POLLNVAL) {
+ fds[i].fd = -1;
+ active_fds--;
+ continue;
+ }
+ if (rev & POLLERR || (rev & (POLLHUP | POLLIN)) == POLLHUP) {
+ if (xclose(fds[i].fd)) {
+ error_msg_errno("close");
+ }
+ fds[i].fd = -1;
+ active_fds--;
+ }
+ }
+ if (active_fds == 0) {
+ return;
+ }
+ }
+}
+
+static int open_dev_null(int flags)
+{
+ int fd = xopen("/dev/null", flags | O_CLOEXEC, 0);
+ if (unlikely(fd < 0)) {
+ error_msg_errno("Error opening /dev/null");
+ }
+ return fd;
+}
+
+static int handle_child_error(pid_t pid)
+{
+ int ret = wait_child(pid);
+ if (ret < 0) {
+ error_msg_errno("waitpid");
+ } else if (ret >= 256) {
+ int sig = ret >> 8;
+ const char *str = strsignal(sig);
+ error_msg("Child received signal %d (%s)", sig, str ? str : "??");
+ } else if (ret) {
+ error_msg("Child returned %d", ret);
+ }
+ return ret;
+}
+
+static void yield_terminal(EditorState *e, bool quiet)
+{
+ if (quiet) {
+ term_raw_isig();
+ } else {
+ e->child_controls_terminal = true;
+ ui_end(e);
+ }
+}
+
+static void resume_terminal(EditorState *e, bool quiet, bool prompt)
+{
+ term_raw();
+ if (!quiet && e->child_controls_terminal) {
+ if (prompt) {
+ any_key(&e->terminal, e->options.esc_timeout);
+ }
+ ui_start(e);
+ e->child_controls_terminal = false;
+ }
+}
+
+static void exec_error(const char *argv0)
+{
+ error_msg("Unable to exec '%s': %s", argv0, strerror(errno));
+}
+
+bool spawn_compiler(SpawnContext *ctx, const Compiler *c, MessageArray *msgs)
+{
+ BUG_ON(!ctx->editor);
+ BUG_ON(!ctx->argv[0]);
+
+ int fd[3];
+ fd[0] = open_dev_null(O_RDONLY);
+ if (fd[0] < 0) {
+ return false;
+ }
+
+ int dev_null = open_dev_null(O_WRONLY);
+ if (dev_null < 0) {
+ xclose(fd[0]);
+ return false;
+ }
+
+ int p[2];
+ if (xpipe2(p, O_CLOEXEC) != 0) {
+ error_msg_errno("pipe");
+ xclose(dev_null);
+ xclose(fd[0]);
+ return false;
+ }
+
+ SpawnFlags flags = ctx->flags;
+ bool read_stdout = !!(flags & SPAWN_READ_STDOUT);
+ bool quiet = !!(flags & SPAWN_QUIET);
+ bool prompt = !!(flags & SPAWN_PROMPT);
+ if (read_stdout) {
+ fd[1] = p[1];
+ fd[2] = quiet ? dev_null : 2;
+ } else {
+ fd[1] = quiet ? dev_null : 1;
+ fd[2] = p[1];
+ }
+
+ yield_terminal(ctx->editor, quiet);
+ pid_t pid = fork_exec(ctx->argv, NULL, fd, quiet);
+ if (pid == -1) {
+ exec_error(ctx->argv[0]);
+ xclose(p[1]);
+ prompt = false;
+ } else {
+ // Must close write end of the pipe before read_errors() or
+ // the read end never gets EOF!
+ xclose(p[1]);
+ read_errors(c, msgs, p[0], quiet);
+ handle_child_error(pid);
+ }
+ resume_terminal(ctx->editor, quiet, prompt);
+
+ xclose(p[0]);
+ xclose(dev_null);
+ xclose(fd[0]);
+ return (pid != -1);
+}
+
+// Close fd only if valid (positive) and not stdin/stdout/stderr
+static int safe_xclose(int fd)
+{
+ return (fd > STDERR_FILENO) ? xclose(fd) : 0;
+}
+
+static void safe_xclose_all(int fds[], size_t nr_fds)
+{
+ for (size_t i = 0; i < nr_fds; i++) {
+ safe_xclose(fds[i]);
+ fds[i] = -1;
+ }
+}
+
+UNITTEST {
+ int fds[] = {-2, -3, -4};
+ safe_xclose_all(fds, 2);
+ BUG_ON(fds[0] != -1);
+ BUG_ON(fds[1] != -1);
+ BUG_ON(fds[2] != -4);
+ safe_xclose_all(fds, 3);
+ BUG_ON(fds[2] != -1);
+}
+
+int spawn(SpawnContext *ctx)
+{
+ BUG_ON(!ctx->editor);
+ BUG_ON(!ctx->argv[0]);
+
+ int child_fds[3] = {-1, -1, -1};
+ int parent_fds[3] = {-1, -1, -1};
+ bool quiet = !!(ctx->flags & SPAWN_QUIET);
+ size_t nr_pipes = 0;
+
+ for (size_t i = 0; i < ARRAYLEN(child_fds); i++) {
+ switch (ctx->actions[i]) {
+ case SPAWN_TTY:
+ if (!quiet) {
+ child_fds[i] = i;
+ break;
+ }
+ // Fallthrough
+ case SPAWN_NULL:
+ child_fds[i] = open_dev_null(O_RDWR);
+ if (child_fds[i] < 0) {
+ goto error_close;
+ }
+ break;
+ case SPAWN_PIPE: {
+ int p[2];
+ if (xpipe2(p, O_CLOEXEC) != 0) {
+ error_msg_errno("pipe");
+ goto error_close;
+ }
+ BUG_ON(p[0] <= STDERR_FILENO);
+ BUG_ON(p[1] <= STDERR_FILENO);
+ child_fds[i] = i ? p[1] : p[0];
+ parent_fds[i] = i ? p[0] : p[1];
+ if (!fd_set_nonblock(parent_fds[i], true)) {
+ error_msg_errno("fcntl");
+ goto error_close;
+ }
+ nr_pipes++;
+ break;
+ }
+ default:
+ BUG("unhandled action type");
+ goto error_close;
+ }
+ }
+
+ yield_terminal(ctx->editor, quiet);
+ pid_t pid = fork_exec(ctx->argv, ctx->env, child_fds, quiet);
+ if (pid == -1) {
+ exec_error(ctx->argv[0]);
+ goto error_resume;
+ }
+
+ safe_xclose_all(child_fds, ARRAYLEN(child_fds));
+ if (nr_pipes > 0) {
+ handle_piped_data(parent_fds, ctx);
+ }
+
+ safe_xclose_all(parent_fds, ARRAYLEN(parent_fds));
+ int err = wait_child(pid);
+ if (err < 0) {
+ error_msg_errno("waitpid");
+ }
+
+ resume_terminal(ctx->editor, quiet, !!(ctx->flags & SPAWN_PROMPT));
+ return err;
+
+error_resume:
+ resume_terminal(ctx->editor, quiet, false);
+error_close:
+ safe_xclose_all(child_fds, ARRAYLEN(child_fds));
+ safe_xclose_all(parent_fds, ARRAYLEN(parent_fds));
+ return -1;
+}
diff --git a/examples/dte/spawn.h b/examples/dte/spawn.h
new file mode 100644
index 0000000..659f2cd
--- /dev/null
+++ b/examples/dte/spawn.h
@@ -0,0 +1,37 @@
+#ifndef SPAWN_H
+#define SPAWN_H
+
+#include <stdbool.h>
+#include "compiler.h"
+#include "editor.h"
+#include "msg.h"
+#include "util/macros.h"
+#include "util/string.h"
+#include "util/string-view.h"
+
+typedef enum {
+ SPAWN_QUIET = 1 << 0, // Interpret SPAWN_TTY as SPAWN_NULL and don't yield terminal to child
+ SPAWN_PROMPT = 1 << 1, // Show "press any key to continue" prompt
+ SPAWN_READ_STDOUT = 1 << 2, // Read errors from stdout instead of stderr
+} SpawnFlags;
+
+typedef enum {
+ SPAWN_NULL,
+ SPAWN_TTY,
+ SPAWN_PIPE,
+} SpawnAction;
+
+typedef struct {
+ EditorState *editor;
+ const char **argv;
+ const char **env;
+ StringView input;
+ String outputs[2]; // For stdout/stderr
+ SpawnFlags flags;
+ SpawnAction actions[3];
+} SpawnContext;
+
+int spawn(SpawnContext *ctx) NONNULL_ARGS WARN_UNUSED_RESULT;
+bool spawn_compiler(SpawnContext *ctx, const Compiler *c, MessageArray *msgs) NONNULL_ARGS WARN_UNUSED_RESULT;
+
+#endif
diff --git a/examples/dte/status.c b/examples/dte/status.c
new file mode 100644
index 0000000..228b1c6
--- /dev/null
+++ b/examples/dte/status.c
@@ -0,0 +1,337 @@
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include "status.h"
+#include "search.h"
+#include "selection.h"
+#include "util/debug.h"
+#include "util/macros.h"
+#include "util/numtostr.h"
+#include "util/utf8.h"
+#include "util/xsnprintf.h"
+
+typedef struct {
+ char *buf;
+ size_t size;
+ size_t pos;
+ size_t separator;
+ const Window *window;
+ const GlobalOptions *opts;
+ InputMode input_mode;
+} Formatter;
+
+typedef enum {
+ STATUS_INVALID = 0,
+ STATUS_ESCAPED_PERCENT,
+ STATUS_ENCODING,
+ STATUS_MISC,
+ STATUS_IS_CRLF,
+ STATUS_SEPARATOR_LONG,
+ STATUS_CURSOR_COL_BYTES,
+ STATUS_TOTAL_ROWS,
+ STATUS_BOM,
+ STATUS_FILENAME,
+ STATUS_MODIFIED,
+ STATUS_LINE_ENDING,
+ STATUS_OVERWRITE,
+ STATUS_SCROLL_POSITION,
+ STATUS_READONLY,
+ STATUS_SEPARATOR,
+ STATUS_FILETYPE,
+ STATUS_UNICODE,
+ STATUS_CURSOR_COL,
+ STATUS_CURSOR_ROW,
+} FormatSpecifierType;
+
+static FormatSpecifierType lookup_format_specifier(unsigned char ch)
+{
+ switch (ch) {
+ case '%': return STATUS_ESCAPED_PERCENT;
+ case 'E': return STATUS_ENCODING;
+ case 'M': return STATUS_MISC;
+ case 'N': return STATUS_IS_CRLF;
+ case 'S': return STATUS_SEPARATOR_LONG;
+ case 'X': return STATUS_CURSOR_COL_BYTES;
+ case 'Y': return STATUS_TOTAL_ROWS;
+ case 'b': return STATUS_BOM;
+ case 'f': return STATUS_FILENAME;
+ case 'm': return STATUS_MODIFIED;
+ case 'n': return STATUS_LINE_ENDING;
+ case 'o': return STATUS_OVERWRITE;
+ case 'p': return STATUS_SCROLL_POSITION;
+ case 'r': return STATUS_READONLY;
+ case 's': return STATUS_SEPARATOR;
+ case 't': return STATUS_FILETYPE;
+ case 'u': return STATUS_UNICODE;
+ case 'x': return STATUS_CURSOR_COL;
+ case 'y': return STATUS_CURSOR_ROW;
+ }
+ return STATUS_INVALID;
+}
+
+#define add_status_literal(f, s) add_status_bytes(f, s, STRLEN(s))
+
+static void add_ch(Formatter *f, char ch)
+{
+ f->buf[f->pos++] = ch;
+}
+
+static void add_separator(Formatter *f)
+{
+ while (f->separator && f->pos < f->size) {
+ add_ch(f, ' ');
+ f->separator--;
+ }
+}
+
+static void add_status_str(Formatter *f, const char *str)
+{
+ BUG_ON(!str);
+ if (unlikely(!str[0])) {
+ return;
+ }
+ add_separator(f);
+ size_t idx = 0;
+ while (f->pos < f->size && str[idx]) {
+ u_set_char(f->buf, &f->pos, u_str_get_char(str, &idx));
+ }
+}
+
+static void add_status_bytes(Formatter *f, const char *str, size_t len)
+{
+ if (unlikely(len == 0)) {
+ return;
+ }
+ add_separator(f);
+ if (f->pos >= f->size) {
+ return;
+ }
+ const size_t avail = f->size - f->pos;
+ len = MIN(len, avail);
+ memcpy(f->buf + f->pos, str, len);
+ f->pos += len;
+}
+
+PRINTF(2)
+static void add_status_format(Formatter *f, const char *format, ...)
+{
+ char buf[1024];
+ va_list ap;
+ va_start(ap, format);
+ size_t len = xvsnprintf(buf, sizeof(buf), format, ap);
+ va_end(ap);
+ add_status_bytes(f, buf, len);
+}
+
+static void add_status_umax(Formatter *f, uintmax_t x)
+{
+ char buf[DECIMAL_STR_MAX(x)];
+ size_t len = buf_umax_to_str(x, buf);
+ add_status_bytes(f, buf, len);
+}
+
+static void add_status_pos(Formatter *f)
+{
+ size_t lines = f->window->view->buffer->nl;
+ int h = f->window->edit_h;
+ long pos = f->window->view->vy;
+ if (lines <= h) {
+ if (pos) {
+ add_status_literal(f, "Bot");
+ } else {
+ add_status_literal(f, "All");
+ }
+ } else if (pos == 0) {
+ add_status_literal(f, "Top");
+ } else if (pos + h - 1 >= lines) {
+ add_status_literal(f, "Bot");
+ } else {
+ unsigned int d = lines - (h - 1);
+ unsigned int percent = (pos * 100 + d / 2) / d;
+ BUG_ON(percent > 100);
+ char buf[4];
+ size_t len = buf_uint_to_str(percent, buf);
+ buf[len++] = '%';
+ add_status_bytes(f, buf, len);
+ }
+}
+
+static void add_misc_status(Formatter *f)
+{
+ static const struct {
+ const char str[24];
+ size_t len;
+ } css_strs[] = {
+ [CSS_FALSE] = {STRN("[case-sensitive = false]")},
+ [CSS_TRUE] = {STRN("[case-sensitive = true]")},
+ [CSS_AUTO] = {STRN("[case-sensitive = auto]")},
+ };
+
+ if (f->input_mode == INPUT_SEARCH) {
+ SearchCaseSensitivity css = f->opts->case_sensitive_search;
+ BUG_ON(css >= ARRAYLEN(css_strs));
+ add_status_bytes(f, css_strs[css].str, css_strs[css].len);
+ return;
+ }
+
+ const View *view = f->window->view;
+ if (view->selection == SELECT_NONE) {
+ return;
+ }
+
+ SelectionInfo si;
+ init_selection(view, &si);
+ bool is_lines = (view->selection == SELECT_LINES);
+ size_t n = is_lines ? get_nr_selected_lines(&si) : get_nr_selected_chars(&si);
+ const char *unit = is_lines ? "line" : "char";
+ const char *plural = unlikely(n == 1) ? "" : "s";
+ add_status_format(f, "[%zu %s%s]", n, unit, plural);
+}
+
+void sf_format (
+ const Window *window,
+ const GlobalOptions *opts,
+ InputMode mode,
+ char *buf, // NOLINT(readability-non-const-parameter)
+ size_t size,
+ const char *format
+) {
+ BUG_ON(size < 16);
+ Formatter f = {
+ .window = window,
+ .opts = opts,
+ .input_mode = mode,
+ .buf = buf,
+ .size = size - 5, // Max length of char and terminating NUL
+ };
+
+ const View *view = window->view;
+ const Buffer *buffer = view->buffer;
+ CodePoint u;
+
+ while (f.pos < f.size && *format) {
+ unsigned char ch = *format++;
+ if (ch != '%') {
+ add_separator(&f);
+ add_ch(&f, ch);
+ continue;
+ }
+
+ switch (lookup_format_specifier(*format++)) {
+ case STATUS_BOM:
+ if (buffer->bom) {
+ add_status_literal(&f, "BOM");
+ }
+ break;
+ case STATUS_FILENAME:
+ add_status_str(&f, buffer_filename(buffer));
+ break;
+ case STATUS_MODIFIED:
+ if (buffer_modified(buffer)) {
+ add_separator(&f);
+ add_ch(&f, '*');
+ }
+ break;
+ case STATUS_READONLY:
+ if (buffer->readonly) {
+ add_status_literal(&f, "RO");
+ } else if (buffer->temporary) {
+ add_status_literal(&f, "TMP");
+ }
+ break;
+ case STATUS_CURSOR_ROW:
+ add_status_umax(&f, view->cy + 1);
+ break;
+ case STATUS_TOTAL_ROWS:
+ add_status_umax(&f, buffer->nl);
+ break;
+ case STATUS_CURSOR_COL:
+ add_status_umax(&f, view->cx_display + 1);
+ break;
+ case STATUS_CURSOR_COL_BYTES:
+ add_status_umax(&f, view->cx_char + 1);
+ if (view->cx_display != view->cx_char) {
+ add_ch(&f, '-');
+ add_status_umax(&f, view->cx_display + 1);
+ }
+ break;
+ case STATUS_SCROLL_POSITION:
+ add_status_pos(&f);
+ break;
+ case STATUS_ENCODING:
+ add_status_str(&f, buffer->encoding.name);
+ break;
+ case STATUS_MISC:
+ add_misc_status(&f);
+ break;
+ case STATUS_IS_CRLF:
+ if (buffer->crlf_newlines) {
+ add_status_literal(&f, "CRLF");
+ }
+ break;
+ case STATUS_LINE_ENDING:
+ if (buffer->crlf_newlines) {
+ add_status_literal(&f, "CRLF");
+ } else {
+ add_status_literal(&f, "LF");
+ }
+ break;
+ case STATUS_OVERWRITE:
+ if (buffer->options.overwrite) {
+ add_status_literal(&f, "OVR");
+ } else {
+ add_status_literal(&f, "INS");
+ }
+ break;
+ case STATUS_SEPARATOR_LONG:
+ f.separator = 3;
+ break;
+ case STATUS_SEPARATOR:
+ f.separator = 1;
+ break;
+ case STATUS_FILETYPE:
+ add_status_str(&f, buffer->options.filetype);
+ break;
+ case STATUS_UNICODE:
+ if (unlikely(!block_iter_get_char(&view->cursor, &u))) {
+ break;
+ }
+ if (u_is_unicode(u)) {
+ char str[STRLEN("U+10FFFF") + 1];
+ str[0] = 'U';
+ str[1] = '+';
+ size_t ndigits = buf_umax_to_hex_str(u, str + 2, 4);
+ add_status_bytes(&f, str, 2 + ndigits);
+ } else {
+ add_status_literal(&f, "Invalid");
+ }
+ break;
+ case STATUS_ESCAPED_PERCENT:
+ add_separator(&f);
+ add_ch(&f, '%');
+ break;
+ case STATUS_INVALID:
+ default:
+ BUG("should be unreachable, due to validate_statusline_format()");
+ }
+ }
+
+ f.buf[f.pos] = '\0';
+}
+
+// Returns the offset of the first invalid format specifier, or 0 if
+// the whole format string is valid. It's safe to use 0 to indicate
+// "no errors", since it's not possible for there to be an error at
+// the very start of the string.
+size_t statusline_format_find_error(const char *str)
+{
+ for (size_t i = 0; str[i]; ) {
+ if (str[i++] != '%') {
+ continue;
+ }
+ if (lookup_format_specifier(str[i++]) == STATUS_INVALID) {
+ return i - 1;
+ }
+ }
+ return 0;
+}
diff --git a/examples/dte/status.h b/examples/dte/status.h
new file mode 100644
index 0000000..df84f0f
--- /dev/null
+++ b/examples/dte/status.h
@@ -0,0 +1,20 @@
+#ifndef STATUS_H
+#define STATUS_H
+
+#include <stddef.h>
+#include "editor.h"
+#include "options.h"
+#include "window.h"
+
+size_t statusline_format_find_error(const char *str);
+
+void sf_format (
+ const Window *window,
+ const GlobalOptions *opts,
+ InputMode mode,
+ char *buf,
+ size_t size,
+ const char *format
+);
+
+#endif
diff --git a/examples/dte/tag.c b/examples/dte/tag.c
new file mode 100644
index 0000000..bd030c0
--- /dev/null
+++ b/examples/dte/tag.c
@@ -0,0 +1,322 @@
+#include <errno.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include "tag.h"
+#include "error.h"
+#include "util/debug.h"
+#include "util/log.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/xmalloc.h"
+#include "util/xreadwrite.h"
+
+static const char *current_filename; // For sorting tags
+
+static int visibility_cmp(const Tag *a, const Tag *b)
+{
+ bool a_this_file = false;
+ bool b_this_file = false;
+
+ if (!a->local && !b->local) {
+ return 0;
+ }
+
+ // Is tag visibility limited to the current file?
+ if (a->local) {
+ a_this_file = current_filename && strview_equal_cstring(&a->filename, current_filename);
+ }
+ if (b->local) {
+ b_this_file = current_filename && strview_equal_cstring(&b->filename, current_filename);
+ }
+
+ // Tags local to other file than current are not interesting
+ if (a->local && !a_this_file) {
+ // a is not interesting
+ if (b->local && !b_this_file) {
+ // b is equally uninteresting
+ return 0;
+ }
+ // b is more interesting, sort it before a
+ return 1;
+ }
+ if (b->local && !b_this_file) {
+ // b is not interesting
+ return -1;
+ }
+
+ // Both are NOT UNinteresting
+
+ if (a->local && a_this_file) {
+ if (b->local && b_this_file) {
+ return 0;
+ }
+ // a is more interesting because it is local symbol
+ return -1;
+ }
+ if (b->local && b_this_file) {
+ // b is more interesting because it is local symbol
+ return 1;
+ }
+ return 0;
+}
+
+static int kind_cmp(const Tag *a, const Tag *b)
+{
+ if (a->kind == b->kind) {
+ return 0;
+ }
+
+ // Struct member (m) is not very interesting
+ if (a->kind == 'm') {
+ return 1;
+ }
+ if (b->kind == 'm') {
+ return -1;
+ }
+
+ // Global variable (v) is not very interesting
+ if (a->kind == 'v') {
+ return 1;
+ }
+ if (b->kind == 'v') {
+ return -1;
+ }
+
+ // Struct (s), union (u)
+ return 0;
+}
+
+static int tag_cmp(const void *ap, const void *bp)
+{
+ const Tag *const *a = ap;
+ const Tag *const *b = bp;
+ int r = visibility_cmp(*a, *b);
+ return r ? r : kind_cmp(*a, *b);
+}
+
+// Find "tags" file from directory path and its parent directories
+static int open_tag_file(char *path)
+{
+ static const char tags[] = "tags";
+ while (*path) {
+ size_t len = strlen(path);
+ char *slash = strrchr(path, '/');
+ if (slash != path + len - 1) {
+ path[len++] = '/';
+ }
+ memcpy(path + len, tags, sizeof(tags));
+ int fd = xopen(path, O_RDONLY | O_CLOEXEC, 0);
+ if (fd >= 0) {
+ return fd;
+ }
+ if (errno != ENOENT) {
+ return -1;
+ }
+ *slash = '\0';
+ }
+ errno = ENOENT;
+ return -1;
+}
+
+static bool tag_file_changed (
+ const TagFile *tf,
+ const char *filename,
+ const struct stat *st
+) {
+ return tf->mtime != st->st_mtime || !streq(tf->filename, filename);
+}
+
+// Note: does not free `tf` itself
+void tag_file_free(TagFile *tf)
+{
+ free(tf->filename);
+ free(tf->buf);
+ *tf = (TagFile){.filename = NULL};
+}
+
+static bool load_tag_file(TagFile *tf)
+{
+ char path[4096];
+ if (unlikely(!getcwd(path, sizeof(path) - STRLEN("/tags")))) {
+ LOG_ERRNO("getcwd");
+ return false;
+ }
+
+ int fd = open_tag_file(path);
+ if (fd < 0) {
+ return false;
+ }
+
+ struct stat st;
+ if (unlikely(fstat(fd, &st) != 0)) {
+ LOG_ERRNO("fstat");
+ xclose(fd);
+ return false;
+ }
+
+ if (unlikely(st.st_size <= 0)) {
+ xclose(fd);
+ return false;
+ }
+
+ if (tf->filename) {
+ if (!tag_file_changed(tf, path, &st)) {
+ xclose(fd);
+ return true;
+ }
+ tag_file_free(tf);
+ BUG_ON(tf->filename);
+ }
+
+ char *buf = malloc(st.st_size);
+ if (unlikely(!buf)) {
+ LOG_ERRNO("malloc");
+ xclose(fd);
+ return false;
+ }
+
+ ssize_t size = xread_all(fd, buf, st.st_size);
+ xclose(fd);
+ if (size < 0) {
+ free(buf);
+ return false;
+ }
+
+ *tf = (TagFile) {
+ .filename = xstrdup(path),
+ .buf = buf,
+ .size = size,
+ .mtime = st.st_mtime,
+ };
+
+ return true;
+}
+
+static void free_tags_cb(Tag *t)
+{
+ free_tag(t);
+ free(t);
+}
+
+static void free_tags(PointerArray *tags)
+{
+ ptr_array_free_cb(tags, FREE_FUNC(free_tags_cb));
+}
+
+// Both parameters must be absolute and clean
+static const char *path_slice_relative(const char *filename, const StringView dir)
+{
+ if (strncmp(filename, dir.data, dir.length) != 0) {
+ // Filename doesn't start with dir
+ return NULL;
+ }
+ switch (filename[dir.length]) {
+ case '\0': // Equal strings
+ return ".";
+ case '/':
+ return filename + dir.length + 1;
+ }
+ return NULL;
+}
+
+static void tag_file_find_tags (
+ const TagFile *tf,
+ const char *filename,
+ const StringView *name,
+ PointerArray *tags
+) {
+ Tag *t = xnew(Tag, 1);
+ size_t pos = 0;
+ while (next_tag(tf->buf, tf->size, &pos, name, true, t)) {
+ ptr_array_append(tags, t);
+ t = xnew(Tag, 1);
+ }
+ free(t);
+
+ if (!filename) {
+ current_filename = NULL;
+ } else {
+ StringView dir = path_slice_dirname(tf->filename);
+ current_filename = path_slice_relative(filename, dir);
+ }
+ ptr_array_sort(tags, tag_cmp);
+ current_filename = NULL;
+}
+
+// Note: this moves ownership of tag->pattern to the generated Message
+// and assigns NULL to the old pointer
+void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir)
+{
+ BUG_ON(dir->length == 0);
+ BUG_ON(dir->data[0] != '/');
+
+ static const char prefix[] = "Tag ";
+ size_t prefix_len = sizeof(prefix) - 1;
+ size_t msg_len = prefix_len + tag->name.length;
+ Message *m = xmalloc(sizeof(*m) + msg_len + 1);
+
+ memcpy(m->msg, prefix, prefix_len);
+ memcpy(m->msg + prefix_len, tag->name.data, tag->name.length);
+ m->msg[msg_len] = '\0';
+
+ m->loc = xnew0(FileLocation, 1);
+ m->loc->filename = path_join_sv(dir, &tag->filename, false);
+
+ if (tag->pattern) {
+ m->loc->pattern = tag->pattern; // Message takes ownership
+ tag->pattern = NULL;
+ } else {
+ m->loc->line = tag->lineno;
+ }
+
+ add_message(messages, m);
+}
+
+size_t tag_lookup(TagFile *tf, const StringView *name, const char *filename, MessageArray *messages)
+{
+ clear_messages(messages);
+ if (!load_tag_file(tf)) {
+ error_msg("No tags file");
+ return 0;
+ }
+
+ // Filename helps to find correct tags
+ PointerArray tags = PTR_ARRAY_INIT;
+ tag_file_find_tags(tf, filename, name, &tags);
+
+ size_t ntags = tags.count;
+ if (ntags == 0) {
+ error_msg("Tag '%.*s' not found", (int)name->length, name->data);
+ return 0;
+ }
+
+ StringView tf_dir = path_slice_dirname(tf->filename);
+ for (size_t i = 0; i < ntags; i++) {
+ Tag *tag = tags.ptrs[i];
+ add_message_for_tag(messages, tag, &tf_dir);
+ }
+
+ free_tags(&tags);
+ return ntags;
+}
+
+void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix)
+{
+ if (!load_tag_file(tf)) {
+ return;
+ }
+
+ Tag t;
+ size_t pos = 0;
+ StringView prev = STRING_VIEW_INIT;
+ while (next_tag(tf->buf, tf->size, &pos, prefix, false, &t)) {
+ BUG_ON(t.name.length == 0);
+ if (prev.length == 0 || !strview_equal(&t.name, &prev)) {
+ ptr_array_append(a, xstrcut(t.name.data, t.name.length));
+ prev = t.name;
+ }
+ free_tag(&t);
+ }
+}
diff --git a/examples/dte/tag.h b/examples/dte/tag.h
new file mode 100644
index 0000000..eddc04a
--- /dev/null
+++ b/examples/dte/tag.h
@@ -0,0 +1,23 @@
+#ifndef TAG_H
+#define TAG_H
+
+#include <sys/types.h>
+#include "ctags.h"
+#include "msg.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "util/string-view.h"
+
+typedef struct {
+ char *filename;
+ char *buf;
+ size_t size;
+ time_t mtime;
+} TagFile;
+
+void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir) NONNULL_ARGS;
+size_t tag_lookup(TagFile *tf, const StringView *name, const char *filename, MessageArray *messages) NONNULL_ARG(1, 2, 4);
+void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix) NONNULL_ARGS;
+void tag_file_free(TagFile *tf) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/vars.c b/examples/dte/vars.c
new file mode 100644
index 0000000..5155ca2
--- /dev/null
+++ b/examples/dte/vars.c
@@ -0,0 +1,113 @@
+#include <stddef.h>
+#include <string.h>
+#include <unistd.h>
+#include "vars.h"
+#include "buffer.h"
+#include "editor.h"
+#include "selection.h"
+#include "util/array.h"
+#include "util/bsearch.h"
+#include "util/numtostr.h"
+#include "util/path.h"
+#include "util/xmalloc.h"
+#include "view.h"
+
+typedef struct {
+ char name[12];
+ char *(*expand)(const EditorState *e);
+} BuiltinVar;
+
+static char *expand_dte_home(const EditorState *e)
+{
+ return xstrdup(e->user_config_dir);
+}
+
+static char *expand_file(const EditorState *e)
+{
+ if (!e->buffer || !e->buffer->abs_filename) {
+ return NULL;
+ }
+ return xstrdup(e->buffer->abs_filename);
+}
+
+static char *expand_file_dir(const EditorState *e)
+{
+ if (!e->buffer || !e->buffer->abs_filename) {
+ return NULL;
+ }
+ return path_dirname(e->buffer->abs_filename);
+}
+
+static char *expand_rfile(const EditorState *e)
+{
+ if (!e->buffer || !e->buffer->abs_filename) {
+ return NULL;
+ }
+ char buf[8192];
+ const char *cwd = getcwd(buf, sizeof buf);
+ const char *abs = e->buffer->abs_filename;
+ return likely(cwd) ? path_relative(abs, cwd) : xstrdup(abs);
+}
+
+static char *expand_filetype(const EditorState *e)
+{
+ return e->buffer ? xstrdup(e->buffer->options.filetype) : NULL;
+}
+
+static char *expand_colno(const EditorState *e)
+{
+ return e->view ? xstrdup(umax_to_str(e->view->cx_display + 1)) : NULL;
+}
+
+static char *expand_lineno(const EditorState *e)
+{
+ return e->view ? xstrdup(umax_to_str(e->view->cy + 1)) : NULL;
+}
+
+static char *expand_word(const EditorState *e)
+{
+ if (!e->view) {
+ return NULL;
+ }
+
+ size_t size;
+ char *selection = view_get_selection(e->view, &size);
+ if (selection) {
+ xrenew(selection, size + 1);
+ selection[size] = '\0';
+ return selection;
+ }
+
+ StringView word = view_get_word_under_cursor(e->view);
+ return word.length ? xstrcut(word.data, word.length) : NULL;
+}
+
+static const BuiltinVar normal_vars[] = {
+ {"COLNO", expand_colno},
+ {"DTE_HOME", expand_dte_home},
+ {"FILE", expand_file},
+ {"FILEDIR", expand_file_dir},
+ {"FILETYPE", expand_filetype},
+ {"LINENO", expand_lineno},
+ {"RFILE", expand_rfile},
+ {"WORD", expand_word},
+};
+
+UNITTEST {
+ CHECK_BSEARCH_ARRAY(normal_vars, name, strcmp);
+}
+
+bool expand_normal_var(const char *name, char **value, const void *userdata)
+{
+ const BuiltinVar *var = BSEARCH(name, normal_vars, vstrcmp);
+ if (!var) {
+ return false;
+ }
+ *value = var->expand(userdata);
+ return true;
+}
+
+void collect_normal_vars(PointerArray *a, const char *prefix)
+{
+ COLLECT_STRING_FIELDS(normal_vars, name, a, prefix);
+}
diff --git a/examples/dte/vars.h b/examples/dte/vars.h
new file mode 100644
index 0000000..02d53be
--- /dev/null
+++ b/examples/dte/vars.h
@@ -0,0 +1,11 @@
+#ifndef VARS_H
+#define VARS_H
+
+#include <stdbool.h>
+#include "util/macros.h"
+#include "util/ptr-array.h"
+
+bool expand_normal_var(const char *name, char **value, const void *userdata) NONNULL_ARGS;
+void collect_normal_vars(PointerArray *a, const char *prefix) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/view.c b/examples/dte/view.c
new file mode 100644
index 0000000..cc259c0
--- /dev/null
+++ b/examples/dte/view.c
@@ -0,0 +1,178 @@
+#include "view.h"
+#include "buffer.h"
+#include "indent.h"
+#include "util/ascii.h"
+#include "util/debug.h"
+#include "util/str-util.h"
+#include "util/utf8.h"
+#include "window.h"
+
+void view_update_cursor_y(View *view)
+{
+ Buffer *buffer = view->buffer;
+ Block *blk;
+ size_t nl = 0;
+ block_for_each(blk, &buffer->blocks) {
+ if (blk == view->cursor.blk) {
+ nl += count_nl(blk->data, view->cursor.offset);
+ view->cy = nl;
+ return;
+ }
+ nl += blk->nl;
+ }
+ BUG("unreachable");
+}
+
+void view_update_cursor_x(View *view)
+{
+ StringView line;
+ const unsigned int tw = view->buffer->options.tab_width;
+ const size_t cx = fetch_this_line(&view->cursor, &line);
+ long cx_char = 0;
+ long w = 0;
+
+ for (size_t idx = 0; idx < cx; cx_char++) {
+ CodePoint u = line.data[idx++];
+ if (likely(u < 0x80)) {
+ if (likely(!ascii_iscntrl(u))) {
+ w++;
+ } else if (u == '\t') {
+ w = next_indent_width(w, tw);
+ } else {
+ w += 2;
+ }
+ } else {
+ idx--;
+ u = u_get_nonascii(line.data, line.length, &idx);
+ w += u_char_width(u);
+ }
+ }
+
+ view->cx = cx;
+ view->cx_char = cx_char;
+ view->cx_display = w;
+}
+
+static bool view_is_cursor_visible(const View *v)
+{
+ return v->cy < v->vy || v->cy > v->vy + v->window->edit_h - 1;
+}
+
+static void view_center_to_cursor(View *v)
+{
+ size_t lines = v->buffer->nl;
+ Window *window = v->window;
+ unsigned int hh = window->edit_h / 2;
+
+ if (window->edit_h >= lines || v->cy < hh) {
+ v->vy = 0;
+ return;
+ }
+
+ v->vy = v->cy - hh;
+ if (v->vy + window->edit_h > lines) {
+ // -1 makes one ~ line visible so that you know where the EOF is
+ v->vy -= v->vy + window->edit_h - lines - 1;
+ }
+}
+
+static void view_update_vx(View *v)
+{
+ Window *window = v->window;
+ unsigned int c = 8;
+
+ if (v->cx_display - v->vx >= window->edit_w) {
+ v->vx = (v->cx_display - window->edit_w + c) / c * c;
+ }
+ if (v->cx_display < v->vx) {
+ v->vx = v->cx_display / c * c;
+ }
+}
+
+static void view_update_vy(View *v, unsigned int scroll_margin)
+{
+ Window *window = v->window;
+ int margin = window_get_scroll_margin(window, scroll_margin);
+ long max_y = v->vy + window->edit_h - 1 - margin;
+
+ if (v->cy < v->vy + margin) {
+ v->vy = MAX(v->cy - margin, 0);
+ } else if (v->cy > max_y) {
+ v->vy += v->cy - max_y;
+ max_y = v->buffer->nl - window->edit_h + 1;
+ if (v->vy > max_y && max_y >= 0) {
+ v->vy = max_y;
+ }
+ }
+}
+
+void view_update(View *v, unsigned int scroll_margin)
+{
+ view_update_vx(v);
+ if (v->force_center || (v->center_on_scroll && view_is_cursor_visible(v))) {
+ view_center_to_cursor(v);
+ } else {
+ view_update_vy(v, scroll_margin);
+ }
+ v->force_center = false;
+ v->center_on_scroll = false;
+}
+
+long view_get_preferred_x(View *v)
+{
+ if (v->preferred_x < 0) {
+ view_update_cursor_x(v);
+ v->preferred_x = v->cx_display;
+ }
+ return v->preferred_x;
+}
+
+bool view_can_close(const View *view)
+{
+ const Buffer *buffer = view->buffer;
+ return !buffer_modified(buffer) || buffer->views.count > 1;
+}
+
+StringView view_do_get_word_under_cursor(const View *view, size_t *offset_in_line)
+{
+ StringView line;
+ size_t si = fetch_this_line(&view->cursor, &line);
+ while (si < line.length) {
+ size_t i = si;
+ if (u_is_word_char(u_get_char(line.data, line.length, &i))) {
+ break;
+ }
+ si = i;
+ }
+
+ if (si == line.length) {
+ *offset_in_line = 0;
+ return string_view(NULL, 0);
+ }
+
+ size_t ei = si;
+ while (si > 0) {
+ size_t i = si;
+ if (!u_is_word_char(u_prev_char(line.data, &i))) {
+ break;
+ }
+ si = i;
+ }
+
+ while (ei < line.length) {
+ size_t i = ei;
+ if (!u_is_word_char(u_get_char(line.data, line.length, &i))) {
+ break;
+ }
+ ei = i;
+ }
+
+ *offset_in_line = si;
+ return string_view(line.data + si, ei - si);
+}
+
+StringView view_get_word_under_cursor(const View *view)
+{
+ size_t offset_in_line;
+ return view_do_get_word_under_cursor(view, &offset_in_line);
+}
diff --git a/examples/dte/view.h b/examples/dte/view.h
new file mode 100644
index 0000000..c7bb254
--- /dev/null
+++ b/examples/dte/view.h
@@ -0,0 +1,62 @@
+#ifndef VIEW_H
+#define VIEW_H
+
+#include <limits.h>
+#include <stdbool.h>
+#include <sys/types.h>
+#include "block-iter.h"
+#include "util/macros.h"
+#include "util/string-view.h"
+
+typedef enum {
+ SELECT_NONE,
+ SELECT_CHARS,
+ SELECT_LINES,
+} SelectionType;
+
+// A view into a Buffer, with its own cursor position and selection.
+// Visually speaking, each tab in a Window corresponds to a View.
+typedef struct View {
+ struct Buffer *buffer;
+ struct Window *window;
+ BlockIter cursor;
+ long cx, cy; // Cursor position
+ long cx_display; // Visual cursor x (char widths: wide 2, tab 1-8, control 2, invalid char 4)
+ long cx_char; // Cursor x in characters (invalid UTF-8 character (byte) is 1 char)
+ long vx, vy; // Top left corner
+ long preferred_x; // Preferred cursor x (preferred value for cx_display)
+ int tt_width; // Tab title width
+ int tt_truncated_width;
+ bool center_on_scroll; // Center view to cursor if scrolled
+ bool force_center; // Force centering view to cursor
+
+ SelectionType selection;
+ SelectionType select_mode;
+ ssize_t sel_so; // Cursor offset when selection was started
+ ssize_t sel_eo; // See `SEL_EO_RECALC` below
+
+ // Used to save cursor state when multiple views share same buffer
+ bool restore_cursor;
+ size_t saved_cursor_offset;
+} View;
+
+// If View::sel_eo is set to this value it means the offset must
+// be calculated from the cursor iterator. Otherwise the offset
+// is precalculated and may not be the same as the cursor position
+// (see search/replace code).
+#define SEL_EO_RECALC SSIZE_MAX
+
+static inline void view_reset_preferred_x(View *view)
+{
+ view->preferred_x = -1;
+}
+
+void view_update_cursor_y(View *view) NONNULL_ARGS;
+void view_update_cursor_x(View *view) NONNULL_ARGS;
+void view_update(View *view, unsigned int scroll_margin) NONNULL_ARGS;
+long view_get_preferred_x(View *view) NONNULL_ARGS;
+bool view_can_close(const View *view) NONNULL_ARGS;
+StringView view_do_get_word_under_cursor(const View *view, size_t *offset_in_line) NONNULL_ARGS;
+StringView view_get_word_under_cursor(const View *view) NONNULL_ARGS;
+
+#endif
diff --git a/examples/dte/window.c b/examples/dte/window.c
new file mode 100644
index 0000000..6b0c5cb
--- /dev/null
+++ b/examples/dte/window.c
@@ -0,0 +1,507 @@
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include "window.h"
+#include "editor.h"
+#include "error.h"
+#include "file-history.h"
+#include "load-save.h"
+#include "lock.h"
+#include "move.h"
+#include "util/path.h"
+#include "util/str-util.h"
+#include "util/strtonum.h"
+#include "util/xmalloc.h"
+
+Window *new_window(EditorState *e)
+{
+ Window *window = xnew0(Window, 1);
+ window->editor = e;
+ return window;
+}
+
+View *window_add_buffer(Window *window, Buffer *buffer)
+{
+ // We rely on this being 0, for implicit initialization of
+ // View::selection and View::select_mode
+ static_assert(SELECT_NONE == 0);
+
+ View *view = xnew(View, 1);
+ *view = (View) {
+ .buffer = buffer,
+ .window = window,
+ .cursor = {
+ .blk = BLOCK(buffer->blocks.next),
+ .head = &buffer->blocks,
+ }
+ };
+
+ ptr_array_append(&buffer->views, view);
+ ptr_array_append(&window->views, view);
+ window->update_tabbar = true;
+ return view;
+}
+
+View *window_open_empty_buffer(Window *window)
+{
+ EditorState *e = window->editor;
+ return window_add_buffer(window, open_empty_buffer(&e->buffers, &e->options));
+}
+
+View *window_open_buffer (
+ Window *window,
+ const char *filename,
+ bool must_exist,
+ const Encoding *encoding
+) {
+ if (unlikely(filename[0] == '\0')) {
+ error_msg("Empty filename not allowed");
+ return NULL;
+ }
+
+ EditorState *e = window->editor;
+ bool dir_missing = false;
+ char *absolute = path_absolute(filename);
+ if (absolute) {
+ // Already open?
+ Buffer *buffer = find_buffer(&e->buffers, absolute);
+ if (buffer) {
+ if (!streq(absolute, buffer->abs_filename)) {
+ const char *bufname = buffer_filename(buffer);
+ char *s = short_filename(absolute, &e->home_dir);
+ info_msg("%s and %s are the same file", s, bufname);
+ free(s);
+ }
+ free(absolute);
+ return window_get_view(window, buffer);
+ }
+ } else {
+ // Let load_buffer() create error message
+ dir_missing = (errno == ENOENT);
+ }
+
+ /*
+ /proc/$PID/fd/ contains symbolic links to files that have been opened
+ by process $PID. Some of the files may have been deleted but can still
+ be opened using the symbolic link but not by using the absolute path.
+
+ # create file
+ mkdir /tmp/x
+ echo foo > /tmp/x/file
+
+ # in another shell: keep the file open
+ tail -f /tmp/x/file
+
+ # make the absolute path unavailable
+ rm /tmp/x/file
+ rmdir /tmp/x
+
+ # this should still succeed
+ dte /proc/$(pidof tail)/fd/3
+ */
+
+ Buffer *buffer = buffer_new(&e->buffers, &e->options, encoding);
+ if (!load_buffer(buffer, filename, &e->options, must_exist)) {
+ remove_and_free_buffer(&e->buffers, buffer);
+ free(absolute);
+ return NULL;
+ }
+ if (unlikely(buffer->file.mode == 0 && dir_missing)) {
+ // New file in non-existing directory; this is usually a mistake
+ error_msg("Error opening %s: Directory does not exist", filename);
+ remove_and_free_buffer(&e->buffers, buffer);
+ free(absolute);
+ return NULL;
+ }
+
+ if (absolute) {
+ buffer->abs_filename = absolute;
+ } else {
+ // FIXME: obviously wrong
+ buffer->abs_filename = xstrdup(filename);
+ }
+ update_short_filename(buffer, &e->home_dir);
+
+ if (e->options.lock_files) {
+ if (!lock_file(buffer->abs_filename)) {
+ buffer->readonly = true;
+ } else {
+ buffer->locked = true;
+ }
+ }
+
+ if (buffer->file.mode != 0 && !buffer->readonly && access(filename, W_OK)) {
+ error_msg("No write permission to %s, marking read-only", filename);
+ buffer->readonly = true;
+ }
+
+ return window_add_buffer(window, buffer);
+}
+
+View *window_get_view(Window *window, Buffer *buffer)
+{
+ View *view = window_find_view(window, buffer);
+ if (!view) {
+ // Open the buffer in other window to this window
+ view = window_add_buffer(window, buffer);
+ view->cursor = ((View*)buffer->views.ptrs[0])->cursor;
+ }
+ return view;
+}
+
+View *window_find_view(Window *window, Buffer *buffer)
+{
+ for (size_t i = 0, n = buffer->views.count; i < n; i++) {
+ View *view = buffer->views.ptrs[i];
+ if (view->window == window) {
+ return view;
+ }
+ }
+ // Buffer isn't open in this window
+ return NULL;
+}
+
+View *window_find_unclosable_view(Window *window)
+{
+ // Check active view first
+ if (window->view && !view_can_close(window->view)) {
+ return window->view;
+ }
+ for (size_t i = 0, n = window->views.count; i < n; i++) {
+ View *view = window->views.ptrs[i];
+ if (!view_can_close(view)) {
+ return view;
+ }
+ }
+ return NULL;
+}
+
+static void window_remove_views(Window *window)
+{
+ while (window->views.count > 0) {
+ View *view = window->views.ptrs[window->views.count - 1];
+ remove_view(view);
+ }
+}
+
+// NOTE: window->frame isn't removed
+void window_free(Window *window)
+{
+ window_remove_views(window);
+ free(window->views.ptrs);
+ window->frame = NULL;
+ free(window);
+}
+
+// Remove view from view->window and view->buffer->views and free it
+size_t remove_view(View *view)
+{
+ Window *window = view->window;
+ EditorState *e = window->editor;
+ if (view == window->prev_view) {
+ window->prev_view = NULL;
+ }
+ if (view == e->view) {
+ e->view = NULL;
+ e->buffer = NULL;
+ }
+
+ size_t idx = ptr_array_idx(&window->views, view);
+ BUG_ON(idx >= window->views.count);
+ ptr_array_remove_idx(&window->views, idx);
+ window->update_tabbar = true;
+
+ Buffer *buffer = view->buffer;
+ ptr_array_remove(&buffer->views, view);
+ if (buffer->views.count == 0) {
+ if (buffer->options.file_history && buffer->abs_filename) {
+ FileHistory *hist = &e->file_history;
+ file_history_add(hist, view->cy + 1, view->cx_char + 1, buffer->abs_filename);
+ }
+ remove_and_free_buffer(&e->buffers, buffer);
+ }
+
+ free(view);
+ return idx;
+}
+
+void window_close_current_view(Window *window)
+{
+ size_t idx = remove_view(window->view);
+ if (window->prev_view) {
+ window->view = window->prev_view;
+ window->prev_view = NULL;
+ return;
+ }
+ if (window->views.count == 0) {
+ window_open_empty_buffer(window);
+ }
+ if (window->views.count == idx) {
+ idx--;
+ }
+ window->view = window->views.ptrs[idx];
+}
+
+static void restore_cursor_from_history(const FileHistory *hist, View *view)
+{
+ unsigned long row, col;
+ if (file_history_find(hist, view->buffer->abs_filename, &row, &col)) {
+ move_to_filepos(view, row, col);
+ }
+}
+
+void set_view(View *view)
+{
+ EditorState *e = view->window->editor;
+ if (e->view == view) {
+ return;
+ }
+
+ // Forget previous view when changing view using any other command but open
+ if (e->window) {
+ e->window->prev_view = NULL;
+ }
+
+ e->view = view;
+ e->buffer = view->buffer;
+ e->window = view->window;
+ e->window->view = view;
+
+ if (!view->buffer->setup) {
+ buffer_setup(e, view->buffer);
+ if (view->buffer->options.file_history && view->buffer->abs_filename) {
+ restore_cursor_from_history(&e->file_history, view);
+ }
+ }
+
+ // view.cursor can be invalid if same buffer was modified from another view
+ if (view->restore_cursor) {
+ view->cursor.blk = BLOCK(view->buffer->blocks.next);
+ block_iter_goto_offset(&view->cursor, view->saved_cursor_offset);
+ view->restore_cursor = false;
+ view->saved_cursor_offset = 0;
+ }
+
+ // Save cursor states of views sharing same buffer
+ for (size_t i = 0, n = view->buffer->views.count; i < n; i++) {
+ View *other = view->buffer->views.ptrs[i];
+ if (other != view) {
+ other->saved_cursor_offset = block_iter_get_offset(&other->cursor);
+ other->restore_cursor = true;
+ }
+ }
+}
+
+View *window_open_new_file(Window *window)
+{
+ View *prev = window->view;
+ View *view = window_open_empty_buffer(window);
+ set_view(view);
+ window->prev_view = prev;
+ return view;
+}
+
+static bool buffer_is_empty_and_untouched(const Buffer *b)
+{
+ return !b->abs_filename && b->change_head.nr_prev == 0 && !b->display_filename;
+}
+
+// If window contains only one untouched buffer it'll be closed after
+// opening another file. This is done because closing the last buffer
+// causes an empty buffer to be opened (windows must contain at least
+// one buffer).
+static bool is_useless_empty_view(const View *v)
+{
+ return v && v->window->views.count == 1 && buffer_is_empty_and_untouched(v->buffer);
+}
+
+View *window_open_file(Window *window, const char *filename, const Encoding *encoding)
+{
+ View *prev = window->view;
+ bool useless = is_useless_empty_view(prev);
+ View *view = window_open_buffer(window, filename, false, encoding);
+ if (view) {
+ set_view(view);
+ if (useless) {
+ remove_view(prev);
+ } else {
+ window->prev_view = prev;
+ }
+ }
+ return view;
+}
+
+// Open multiple files in window and return the first opened View
+View *window_open_files(Window *window, char **filenames, const Encoding *encoding)
+{
+ View *empty = window->view;
+ bool useless = is_useless_empty_view(empty);
+ View *first = NULL;
+
+ for (size_t i = 0; filenames[i]; i++) {
+ View *view = window_open_buffer(window, filenames[i], false, encoding);
+ if (view && !first) {
+ set_view(view);
+ first = view;
+ }
+ }
+
+ if (useless && window->view != empty) {
+ remove_view(empty);
+ }
+
+ return first;
+}
+
+void mark_buffer_tabbars_changed(Buffer *buffer)
+{
+ for (size_t i = 0, n = buffer->views.count; i < n; i++) {
+ View *view = buffer->views.ptrs[i];
+ view->window->update_tabbar = true;
+ }
+}
+
+static int line_numbers_width(const Window *window, const GlobalOptions *options)
+{
+ if (!options->show_line_numbers || !window->view) {
+ return 0;
+ }
+ size_t width = size_str_width(window->view->buffer->nl) + 1;
+ return MAX(width, LINE_NUMBERS_MIN_WIDTH);
+}
+
+static int edit_x_offset(const Window *window, const GlobalOptions *options)
+{
+ return line_numbers_width(window, options);
+}
+
+static int edit_y_offset(const GlobalOptions *options)
+{
+ return options->tab_bar ? 1 : 0;
+}
+
+static void set_edit_size(Window *window, const GlobalOptions *options)
+{
+ int xo = edit_x_offset(window, options);
+ int yo = edit_y_offset(options);
+
+ window->edit_w = window->w - xo;
+ window->edit_h = window->h - yo - 1; // statusline
+ window->edit_x = window->x + xo;
+}
+
+void calculate_line_numbers(Window *window)
+{
+ const GlobalOptions *options = &window->editor->options;
+ int w = line_numbers_width(window, options);
+ if (w != window->line_numbers.width) {
+ window->line_numbers.width = w;
+ window->line_numbers.first = 0;
+ window->line_numbers.last = 0;
+ mark_all_lines_changed(window->view->buffer);
+ }
+ set_edit_size(window, options);
+}
+
+void set_window_coordinates(Window *window, int x, int y)
+{
+ const GlobalOptions *options = &window->editor->options;
+ window->x = x;
+ window->y = y;
+ window->edit_x = x + edit_x_offset(window, options);
+ window->edit_y = y + edit_y_offset(options);
+}
+
+void set_window_size(Window *window, int w, int h)
+{
+ window->w = w;
+ window->h = h;
+ calculate_line_numbers(window);
+}
+
+int window_get_scroll_margin(const Window *window, unsigned int scroll_margin)
+{
+ int max = (window->edit_h - 1) / 2;
+ BUG_ON(max < 0);
+ return MIN(max, scroll_margin);
+}
+
+void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data)
+{
+ if (frame->window) {
+ func(frame->window, data);
+ return;
+ }
+ for (size_t i = 0, n = frame->frames.count; i < n; i++) {
+ frame_for_each_window(frame->frames.ptrs[i], func, data);
+ }
+}
+
+typedef struct {
+ const Window *const target; // Window to search for (set at init.)
+ Window *first; // Window passed in first callback invocation
+ Window *last; // Window passed in last callback invocation
+ Window *prev; // Window immediately before target (if any)
+ Window *next; // Window immediately after target (if any)
+ bool found; // Set to true when target is found
+} WindowCallbackData;
+
+static void find_prev_and_next(Window *window, void *ud)
+{
+ WindowCallbackData *data = ud;
+ data->last = window;
+ if (data->found) {
+ if (!data->next) {
+ data->next = window;
+ }
+ return;
+ }
+ if (!data->first) {
+ data->first = window;
+ }
+ if (window == data->target) {
+ data->found = true;
+ return;
+ }
+ data->prev = window;
+}
+
+Window *prev_window(Window *window)
+{
+ WindowCallbackData data = {.target = window};
+ frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
+ BUG_ON(!data.found);
+ return data.prev ? data.prev : data.last;
+}
+
+Window *next_window(Window *window)
+{
+ WindowCallbackData data = {.target = window};
+ frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
+ BUG_ON(!data.found);
+ return data.next ? data.next : data.first;
+}
+
+void window_close(Window *window)
+{
+ EditorState *e = window->editor;
+ if (!window->frame->parent) {
+ // Don't close last window
+ window_remove_views(window);
+ set_view(window_open_empty_buffer(window));
+ return;
+ }
+
+ WindowCallbackData data = {.target = window};
+ frame_for_each_window(e->root_frame, find_prev_and_next, &data);
+ BUG_ON(!data.found);
+ Window *next_or_prev = data.next ? data.next : data.prev;
+ BUG_ON(!next_or_prev);
+
+ remove_frame(e, window->frame);
+ e->window = NULL;
+ set_view(next_or_prev->view);
+
+ mark_everything_changed(e);
+ debug_frame(e->root_frame);
+}
diff --git a/examples/dte/window.h b/examples/dte/window.h
new file mode 100644
index 0000000..57378b1
--- /dev/null
+++ b/examples/dte/window.h
@@ -0,0 +1,68 @@
+#ifndef WINDOW_H
+#define WINDOW_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "buffer.h"
+#include "encoding.h"
+#include "frame.h"
+#include "util/macros.h"
+#include "util/ptr-array.h"
+#include "view.h"
+
+enum {
+ // Minimum width of line numbers bar (including padding)
+ LINE_NUMBERS_MIN_WIDTH = 5
+};
+
+// A sub-division of the screen, similar to a window in a tiling window
+// manager. There can be multiple Views associated with each Window, but
+// only one is visible at a time. Each tab displayed in the tab bar
+// corresponds to a View and the editable text area corresponds to the
+// Buffer of the *current* View (Window::view::buffer).
+typedef struct Window {
+ struct EditorState *editor;
+ PointerArray views;
+ Frame *frame;
+ View *view; // Current view
+ View *prev_view; // Previous view, if set
+ int x, y; // Coordinates for top left of window
+ int w, h; // Width and height of window (including tabbar and status)
+ int edit_x, edit_y; // Top left of editable area
+ int edit_w, edit_h; // Width and height of editable area
+ size_t first_tab_idx;
+ bool update_tabbar;
+ struct {
+ int width;
+ long first;
+ long last;
+ } line_numbers;
+} Window;
+
+struct EditorState;
+
+Window *new_window(struct EditorState *e) NONNULL_ARGS_AND_RETURN;
+View *window_add_buffer(Window *window, Buffer *buffer);
+View *window_open_empty_buffer(Window *window);
+View *window_open_buffer(Window *window, const char *filename, bool must_exist, const Encoding *encoding);
+View *window_get_view(Window *window, Buffer *buffer);
+View *window_find_view(Window *window, Buffer *buffer);
+View *window_find_unclosable_view(Window *window);
+void window_free(Window *window);
+size_t remove_view(View *view);
+void window_close(Window *window);
+void window_close_current_view(Window *window);
+void set_view(View *view);
+View *window_open_new_file(Window *window);
+View *window_open_file(Window *window, const char *filename, const Encoding *encoding);
+View *window_open_files(Window *window, char **filenames, const Encoding *encoding);
+void mark_buffer_tabbars_changed(Buffer *buffer);
+void calculate_line_numbers(Window *window);
+void set_window_coordinates(Window *window, int x, int y);
+void set_window_size(Window *window, int w, int h);
+int window_get_scroll_margin(const Window *window, unsigned int scroll_margin);
+void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data);
+Window *prev_window(Window *window);
+Window *next_window(Window *window);
+
+#endif
diff --git a/list.c b/list.c
new file mode 100644
index 0000000..ef2ba2e
--- /dev/null
+++ b/list.c
@@ -0,0 +1,64 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <dirent.h>
+#include <sys/stat.h>
+#include <string.h>
+
+#include "list.h"
+
+void add_file_path(Node **head, char *file_path) {
+ Node *new = (Node *)malloc(sizeof(Node));
+ new->file_path = strdup(file_path);
+ new->next = *head;
+ *head = new;
+}
+
+void list_files_recursively(char *base_path, Node **head) {
+ char path[1000];
+ struct dirent *dp;
+ DIR *dir = opendir(base_path);
+
+ if (!dir) return;
+
+ while ((dp = readdir(dir)) != NULL) {
+ if (strcmp(dp->d_name, ".") != 0 && strcmp(dp->d_name, "..") != 0) {
+ strcpy(path, base_path);
+ strcat(path, "/");
+ strcat(path, dp->d_name);
+
+ struct stat statbuf;
+ if (stat(path, &statbuf) != -1) {
+ if (S_ISDIR(statbuf.st_mode)) {
+ list_files_recursively(path, head);
+ } else {
+ add_file_path(head, path);
+ }
+ }
+ }
+ }
+
+ closedir(dir);
+}
+
+void free_file_list(Node *head) {
+ Node *tmp;
+
+ while (head != NULL) {
+ tmp = head;
+ head = head->next;
+ free(tmp->file_path);
+ free(tmp);
+ }
+}
+
+int size_of_file_list(Node *head) {
+ int count = 0;
+
+ Node *current = head;
+ while (current != NULL) {
+ count++;
+ current = current->next;
+ }
+
+ return count;
+}
diff --git a/list.h b/list.h
new file mode 100644
index 0000000..e494cba
--- /dev/null
+++ b/list.h
@@ -0,0 +1,14 @@
+#ifndef LIST_H
+#define LIST_H
+
+typedef struct node {
+ char *file_path;
+ struct node *next;
+} Node;
+
+void add_file_path(Node **head, char *file_path);
+void list_files_recursively(char *base_path, Node **head);
+void free_file_list(Node *head);
+int size_of_file_list(Node *head);
+
+#endif
diff --git a/main.c b/main.c
index 8e09859..ac05ea9 100644
--- a/main.c
+++ b/main.c
@@ -1,11 +1,13 @@
+#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
-#include <assert.h>
#include <string.h>
+#include <pthread.h>
#include <tree_sitter/api.h>
#include "file.h"
+#include "list.h"
#define DEBUG 1
@@ -30,7 +32,41 @@ const char *extract_value(TSNode captured_node, const char *source_code) {
return NULL;
}
-void parse_source_file(const char *file_path, const char *source_code, TSLanguage *language) {
+char* remove_newlines(const char* str) {
+ size_t length = strlen(str);
+ char* result = (char*)malloc(length + 1); // +1 for the null terminator
+ if (result == NULL) {
+ fprintf(stderr, "Memory allocation failed\n");
+ exit(1);
+ }
+
+ size_t j = 0;
+ for (size_t i = 0; i < length; i++) {
+ if (str[i] != '\n') {
+ result[j++] = str[i];
+ }
+ }
+
+ result[j] = '\0';
+ return result;
+}
+
+struct ThreadArgs {
+ const char* file_path;
+ const char* source_code;
+ TSLanguage* language;
+ const char* cfname;
+};
+
+// void parse_source_file(const char *file_path, const char *source_code, TSLanguage *language, const char *cfname) {
+void *parse_source_file(void *arg) {
+ struct ThreadArgs* args = (struct ThreadArgs*)arg;
+
+ const char *file_path = args->file_path;
+ const char *source_code = args->source_code;
+ TSLanguage *language = args->language;
+ const char *cfname = args->cfname;
+
TSParser *parser = ts_parser_new();
ts_parser_set_language(parser, language);
@@ -55,8 +91,6 @@ void parse_source_file(const char *file_path, const char *source_code, TSLanguag
TSQueryCapture capture = match.captures[i];
TSNode captured_node = capture.node;
- /* fprintf(stderr, "Query: %p, Capture index: %u\n", (void *)query, capture.index); */
-
uint32_t capture_name_length;
const char *capture_name = ts_query_capture_name_for_id(query, capture.index, &capture_name_length);
@@ -76,7 +110,17 @@ void parse_source_file(const char *file_path, const char *source_code, TSLanguag
}
}
- printf("%s:%zu\t%s %s %s\n", file_path, fn.lineno, fn.ftype, fn.fname, fn.fparams);
+ // Full matching.
+ /* if (strcmp(fn.fname, cfname) == 0) { */
+ /* printf("%s:%zu\t%s %s %s\n", file_path, fn.lineno, fn.ftype, fn.fname, fn.fparams); */
+ /* } */
+
+ // Substring matching.
+ char *result = strstr(fn.fname, cfname);
+ if (result != NULL) {
+ char *fparams_formatted = remove_newlines(fn.fparams);
+ printf("%s:%zu\t%s %s %s\n", file_path, fn.lineno, fn.ftype, fn.fname, fparams_formatted);
+ }
}
} else {
if (DEBUG) {
@@ -88,6 +132,8 @@ void parse_source_file(const char *file_path, const char *source_code, TSLanguag
ts_query_delete(query);
ts_tree_delete(tree);
ts_parser_delete(parser);
+
+ return NULL;
}
const char *get_file_extension(const char *file_path) {
@@ -98,45 +144,70 @@ const char *get_file_extension(const char *file_path) {
return NULL;
}
-int main(void) {
- const char *file_path = "examples/cmdline.c";
- /* const char *file_path = "examples/tabs.py"; */
- const char *extension = get_file_extension(file_path);
+int main(int argc, char *argv[]) {
+ if (argc < 2) {
+ printf("Usage: %s <argument>\n", argv[0]);
+ return 1;
+ }
+
+ char *cfname = argv[1];
TSLanguage *tree_sitter_c(void);
TSLanguage *tree_sitter_python(void);
- struct FileContent source_file = read_entire_file(file_path);
- if (source_file.content != NULL) {
- if (DEBUG) {
- /* fprintf(stdout, "File contents:\n%s\n", source_file.content); */
- /* fprintf(stdout, "Count of characters: %zu\n", source_file.count); */
- }
-
- if (extension != NULL) {
- if (DEBUG) {
- fprintf(stdout, "File extension: %s\n", extension);
- }
-
- if (strcmp(extension, "c") == 0) {
- parse_source_file(file_path, source_file.content, tree_sitter_c());
- }
-
- if (strcmp(extension, "py") == 0) {
- parse_source_file(file_path, source_file.content, tree_sitter_python());
+ Node *head = NULL;
+ list_files_recursively("./examples", &head);
+ int list_size = size_of_file_list(head);
+ /* pthread_t threads[list_size]; */
+
+ printf("size: %d\n", list_size);
+
+ Node *current = head;
+ int thread_index = 0;
+ while (current != NULL) {
+ const char *file_path = current->file_path;
+ const char *extension = get_file_extension(file_path);
+ struct FileContent source_file = read_entire_file(file_path);
+
+ if (source_file.content != NULL) {
+ if (extension != NULL) {
+ if (strcmp(extension, "c") == 0 || strcmp(extension, "h") == 0) {
+ /* parse_source_file(file_path, source_file.content, tree_sitter_c(), cfname); */
+
+ struct ThreadArgs thread_args;
+ thread_args.file_path = file_path;
+ thread_args.source_code = source_file.content;
+ thread_args.language = tree_sitter_c();
+ thread_args.cfname = cfname;
+
+ parse_source_file(&thread_args);
+
+ /* printf("> creating thread #%d\n", thread_index); */
+ /* if (pthread_create(&threads[thread_index], NULL, parse_source_file, &thread_args) != 0) { */
+ /* fprintf(stderr, "Error creating thread %d\n", thread_index); */
+ /* return 1; */
+ /* } */
+ }
}
+ free((void *)source_file.content);
} else {
if (DEBUG) {
- fprintf(stderr,"No file extension found.\n");
+ fprintf(stderr, "Failed to read file.\n");
}
}
-
- free((void *)source_file.content);
- } else {
- if (DEBUG) {
- fprintf(stderr, "Failed to read file.\n");
- }
+ current = current->next;
+ thread_index++;
}
+ // Collecting threads.
+ /* for (int i = 0; i < list_size; i++) { */
+ /* printf("> collecting thread #%d\n", thread_index); */
+ /* if (pthread_join(threads[i], NULL) != 0) { */
+ /* fprintf(stderr, "Error joining thread %d\n", i); */
+ /* return 1; */
+ /* } */
+ /* } */
+
+ free_file_list(head);
return 0;
}