diff options
107 files changed, 19711 insertions, 36 deletions
@@ -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, ×[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(®ex, pattern, REG_NEWLINE)) { + return false; + } + + BlockIter bi = block_iter(view->buffer); + bool found = do_search_fwd(view, ®ex, &bi, false); + regfree(®ex); + + 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 @@ -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; +} @@ -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 @@ -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; } |
