diff options
Diffstat (limited to 'examples')
102 files changed, 19527 insertions, 0 deletions
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 @@ | |||
| 1 | dte source code | ||
| 2 | =============== | ||
| 3 | |||
| 4 | This directory contains the `dte` source code. It makes liberal use | ||
| 5 | of ISO C99 features and POSIX 2008 APIs, but generally requires very | ||
| 6 | little else. | ||
| 7 | |||
| 8 | The main editor code is in the base directory and various other | ||
| 9 | (somewhat reusable) parts are in sub-directories: | ||
| 10 | |||
| 11 | * `command/` - command language parsing and execution | ||
| 12 | * `editorconfig/` - [EditorConfig] implementation | ||
| 13 | * `filetype/` - filetype detection | ||
| 14 | * `syntax/` - syntax highlighting | ||
| 15 | * `terminal/` - terminal input/output handling | ||
| 16 | * `util/` - data structures, string utilities, etc. | ||
| 17 | |||
| 18 | |||
| 19 | [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 @@ | |||
| 1 | #include <limits.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include "bind.h" | ||
| 4 | #include "change.h" | ||
| 5 | #include "command/macro.h" | ||
| 6 | #include "command/run.h" | ||
| 7 | #include "command/serialize.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | #include "util/xmalloc.h" | ||
| 10 | |||
| 11 | void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc) | ||
| 12 | { | ||
| 13 | cached_command_free(intmap_insert_or_replace(bindings, key, cc)); | ||
| 14 | } | ||
| 15 | |||
| 16 | void remove_binding(IntMap *bindings, KeyCode key) | ||
| 17 | { | ||
| 18 | cached_command_free(intmap_remove(bindings, key)); | ||
| 19 | } | ||
| 20 | |||
| 21 | const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key) | ||
| 22 | { | ||
| 23 | return intmap_get(bindings, key); | ||
| 24 | } | ||
| 25 | |||
| 26 | void free_bindings(IntMap *bindings) | ||
| 27 | { | ||
| 28 | intmap_free(bindings, (FreeFunction)cached_command_free); | ||
| 29 | } | ||
| 30 | |||
| 31 | bool handle_binding(EditorState *e, InputMode mode, KeyCode key) | ||
| 32 | { | ||
| 33 | const IntMap *bindings = &e->modes[mode].key_bindings; | ||
| 34 | const CachedCommand *binding = lookup_binding(bindings, key); | ||
| 35 | if (!binding) { | ||
| 36 | return false; | ||
| 37 | } | ||
| 38 | |||
| 39 | // If the command isn't cached or a macro is being recorded | ||
| 40 | const CommandSet *cmds = e->modes[mode].cmds; | ||
| 41 | if (!binding->cmd || (cmds->macro_record && macro_is_recording(&e->macro))) { | ||
| 42 | // Parse and run command string | ||
| 43 | CommandRunner runner = cmdrunner_for_mode(e, mode, true); | ||
| 44 | return handle_command(&runner, binding->cmd_str); | ||
| 45 | } | ||
| 46 | |||
| 47 | // Command is cached; call it directly | ||
| 48 | begin_change(CHANGE_MERGE_NONE); | ||
| 49 | current_command = binding->cmd; | ||
| 50 | bool r = binding->cmd->cmd(e, &binding->a); | ||
| 51 | current_command = NULL; | ||
| 52 | end_change(); | ||
| 53 | return r; | ||
| 54 | } | ||
| 55 | |||
| 56 | typedef struct { | ||
| 57 | KeyCode key; | ||
| 58 | const char *cmd; | ||
| 59 | } KeyBinding; | ||
| 60 | |||
| 61 | static int binding_cmp(const void *ap, const void *bp) | ||
| 62 | { | ||
| 63 | static_assert((MOD_MASK | KEY_SPECIAL_MAX) <= INT_MAX); | ||
| 64 | const KeyBinding *a = ap; | ||
| 65 | const KeyBinding *b = bp; | ||
| 66 | return (int)a->key - (int)b->key; | ||
| 67 | } | ||
| 68 | |||
| 69 | UNITTEST { | ||
| 70 | KeyBinding a = {.key = KEY_F5}; | ||
| 71 | KeyBinding b = {.key = KEY_F5}; | ||
| 72 | BUG_ON(binding_cmp(&a, &b) != 0); | ||
| 73 | b.key = KEY_F3; | ||
| 74 | BUG_ON(binding_cmp(&a, &b) <= 0); | ||
| 75 | b.key = KEY_F12; | ||
| 76 | BUG_ON(binding_cmp(&a, &b) >= 0); | ||
| 77 | } | ||
| 78 | |||
| 79 | bool dump_bindings(const IntMap *bindings, const char *flag, String *buf) | ||
| 80 | { | ||
| 81 | const size_t count = bindings->count; | ||
| 82 | if (unlikely(count == 0)) { | ||
| 83 | return false; | ||
| 84 | } | ||
| 85 | |||
| 86 | // Clone the contents of the map as an array of key/command pairs | ||
| 87 | KeyBinding *array = xnew(*array, count); | ||
| 88 | size_t n = 0; | ||
| 89 | for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) { | ||
| 90 | const CachedCommand *cc = it.entry->value; | ||
| 91 | array[n++] = (KeyBinding) { | ||
| 92 | .key = it.entry->key, | ||
| 93 | .cmd = cc->cmd_str, | ||
| 94 | }; | ||
| 95 | } | ||
| 96 | |||
| 97 | // Sort the array | ||
| 98 | BUG_ON(n != count); | ||
| 99 | qsort(array, count, sizeof(array[0]), binding_cmp); | ||
| 100 | |||
| 101 | // Serialize the bindings in sorted order | ||
| 102 | char keystr[KEYCODE_STR_MAX]; | ||
| 103 | for (size_t i = 0; i < count; i++) { | ||
| 104 | string_append_literal(buf, "bind "); | ||
| 105 | string_append_cstring(buf, flag); | ||
| 106 | size_t keylen = keycode_to_string(array[i].key, keystr); | ||
| 107 | string_append_escaped_arg_sv(buf, string_view(keystr, keylen), true); | ||
| 108 | string_append_byte(buf, ' '); | ||
| 109 | string_append_escaped_arg(buf, array[i].cmd, true); | ||
| 110 | string_append_byte(buf, '\n'); | ||
| 111 | } | ||
| 112 | |||
| 113 | free(array); | ||
| 114 | return true; | ||
| 115 | } | ||
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 @@ | |||
| 1 | #ifndef BIND_H | ||
| 2 | #define BIND_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "command/cache.h" | ||
| 6 | #include "editor.h" | ||
| 7 | #include "terminal/key.h" | ||
| 8 | #include "util/intmap.h" | ||
| 9 | #include "util/macros.h" | ||
| 10 | #include "util/string.h" | ||
| 11 | |||
| 12 | void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc) NONNULL_ARGS; | ||
| 13 | void remove_binding(IntMap *bindings, KeyCode key) NONNULL_ARGS; | ||
| 14 | const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key) NONNULL_ARGS; | ||
| 15 | bool handle_binding(EditorState *e, InputMode mode, KeyCode key) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 16 | void free_bindings(IntMap *bindings) NONNULL_ARGS; | ||
| 17 | bool dump_bindings(const IntMap *bindings, const char *flag, String *buf) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 18 | |||
| 19 | #endif | ||
| 20 | |||
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 @@ | |||
| 1 | #include <string.h> | ||
| 2 | #include "block-iter.h" | ||
| 3 | #include "util/debug.h" | ||
| 4 | #include "util/utf8.h" | ||
| 5 | #include "util/xmalloc.h" | ||
| 6 | |||
| 7 | void block_iter_normalize(BlockIter *bi) | ||
| 8 | { | ||
| 9 | const Block *blk = bi->blk; | ||
| 10 | if (bi->offset == blk->size && blk->node.next != bi->head) { | ||
| 11 | bi->blk = BLOCK(blk->node.next); | ||
| 12 | bi->offset = 0; | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | /* | ||
| 17 | * Move after next newline (beginning of next line or end of file). | ||
| 18 | * Returns number of bytes iterator advanced. | ||
| 19 | */ | ||
| 20 | size_t block_iter_eat_line(BlockIter *bi) | ||
| 21 | { | ||
| 22 | block_iter_normalize(bi); | ||
| 23 | const size_t offset = bi->offset; | ||
| 24 | if (unlikely(offset == bi->blk->size)) { | ||
| 25 | return 0; | ||
| 26 | } | ||
| 27 | |||
| 28 | // There must be at least one newline | ||
| 29 | if (bi->blk->nl == 1) { | ||
| 30 | bi->offset = bi->blk->size; | ||
| 31 | } else { | ||
| 32 | const unsigned char *end; | ||
| 33 | end = memchr(bi->blk->data + offset, '\n', bi->blk->size - offset); | ||
| 34 | BUG_ON(!end); | ||
| 35 | bi->offset = (size_t)(end + 1 - bi->blk->data); | ||
| 36 | } | ||
| 37 | |||
| 38 | return bi->offset - offset; | ||
| 39 | } | ||
| 40 | |||
| 41 | /* | ||
| 42 | * Move to beginning of next line. | ||
| 43 | * If there is no next line, iterator is not advanced. | ||
| 44 | * Returns number of bytes iterator advanced. | ||
| 45 | */ | ||
| 46 | size_t block_iter_next_line(BlockIter *bi) | ||
| 47 | { | ||
| 48 | block_iter_normalize(bi); | ||
| 49 | const size_t offset = bi->offset; | ||
| 50 | if (unlikely(offset == bi->blk->size)) { | ||
| 51 | return 0; | ||
| 52 | } | ||
| 53 | |||
| 54 | // There must be at least one newline | ||
| 55 | size_t new_offset; | ||
| 56 | if (bi->blk->nl == 1) { | ||
| 57 | new_offset = bi->blk->size; | ||
| 58 | } else { | ||
| 59 | const unsigned char *end; | ||
| 60 | end = memchr(bi->blk->data + offset, '\n', bi->blk->size - offset); | ||
| 61 | BUG_ON(!end); | ||
| 62 | new_offset = (size_t)(end + 1 - bi->blk->data); | ||
| 63 | } | ||
| 64 | if (new_offset == bi->blk->size && bi->blk->node.next == bi->head) { | ||
| 65 | return 0; | ||
| 66 | } | ||
| 67 | |||
| 68 | bi->offset = new_offset; | ||
| 69 | return bi->offset - offset; | ||
| 70 | } | ||
| 71 | |||
| 72 | /* | ||
| 73 | * Move to beginning of previous line. | ||
| 74 | * Returns number of bytes moved, which is zero if there's no previous line. | ||
| 75 | */ | ||
| 76 | size_t block_iter_prev_line(BlockIter *bi) | ||
| 77 | { | ||
| 78 | Block *blk = bi->blk; | ||
| 79 | size_t offset = bi->offset; | ||
| 80 | size_t start = offset; | ||
| 81 | |||
| 82 | while (offset && blk->data[offset - 1] != '\n') { | ||
| 83 | offset--; | ||
| 84 | } | ||
| 85 | |||
| 86 | if (!offset) { | ||
| 87 | if (blk->node.prev == bi->head) { | ||
| 88 | return 0; | ||
| 89 | } | ||
| 90 | bi->blk = blk = BLOCK(blk->node.prev); | ||
| 91 | offset = blk->size; | ||
| 92 | start += offset; | ||
| 93 | } | ||
| 94 | |||
| 95 | offset--; | ||
| 96 | while (offset && blk->data[offset - 1] != '\n') { | ||
| 97 | offset--; | ||
| 98 | } | ||
| 99 | bi->offset = offset; | ||
| 100 | return start - offset; | ||
| 101 | } | ||
| 102 | |||
| 103 | size_t block_iter_get_char(const BlockIter *bi, CodePoint *up) | ||
| 104 | { | ||
| 105 | BlockIter tmp = *bi; | ||
| 106 | return block_iter_next_char(&tmp, up); | ||
| 107 | } | ||
| 108 | |||
| 109 | size_t block_iter_next_char(BlockIter *bi, CodePoint *up) | ||
| 110 | { | ||
| 111 | size_t offset = bi->offset; | ||
| 112 | if (unlikely(offset == bi->blk->size)) { | ||
| 113 | if (unlikely(bi->blk->node.next == bi->head)) { | ||
| 114 | return 0; | ||
| 115 | } | ||
| 116 | bi->blk = BLOCK(bi->blk->node.next); | ||
| 117 | bi->offset = offset = 0; | ||
| 118 | } | ||
| 119 | |||
| 120 | // Note: this block can't be empty | ||
| 121 | *up = bi->blk->data[offset]; | ||
| 122 | if (likely(*up < 0x80)) { | ||
| 123 | bi->offset++; | ||
| 124 | return 1; | ||
| 125 | } | ||
| 126 | |||
| 127 | *up = u_get_nonascii(bi->blk->data, bi->blk->size, &bi->offset); | ||
| 128 | return bi->offset - offset; | ||
| 129 | } | ||
| 130 | |||
| 131 | size_t block_iter_prev_char(BlockIter *bi, CodePoint *up) | ||
| 132 | { | ||
| 133 | size_t offset = bi->offset; | ||
| 134 | if (unlikely(offset == 0)) { | ||
| 135 | if (unlikely(bi->blk->node.prev == bi->head)) { | ||
| 136 | return 0; | ||
| 137 | } | ||
| 138 | bi->blk = BLOCK(bi->blk->node.prev); | ||
| 139 | bi->offset = offset = bi->blk->size; | ||
| 140 | } | ||
| 141 | |||
| 142 | // Note: this block can't be empty | ||
| 143 | *up = bi->blk->data[offset - 1]; | ||
| 144 | if (likely(*up < 0x80)) { | ||
| 145 | bi->offset--; | ||
| 146 | return 1; | ||
| 147 | } | ||
| 148 | |||
| 149 | *up = u_prev_char(bi->blk->data, &bi->offset); | ||
| 150 | return offset - bi->offset; | ||
| 151 | } | ||
| 152 | |||
| 153 | size_t block_iter_next_column(BlockIter *bi) | ||
| 154 | { | ||
| 155 | CodePoint u; | ||
| 156 | size_t size = block_iter_next_char(bi, &u); | ||
| 157 | while (block_iter_get_char(bi, &u) && u_is_zero_width(u)) { | ||
| 158 | size += block_iter_next_char(bi, &u); | ||
| 159 | } | ||
| 160 | return size; | ||
| 161 | } | ||
| 162 | |||
| 163 | size_t block_iter_prev_column(BlockIter *bi) | ||
| 164 | { | ||
| 165 | CodePoint u; | ||
| 166 | size_t skip, total = 0; | ||
| 167 | do { | ||
| 168 | skip = block_iter_prev_char(bi, &u); | ||
| 169 | total += skip; | ||
| 170 | } while (skip && u_is_zero_width(u)); | ||
| 171 | return total; | ||
| 172 | } | ||
| 173 | |||
| 174 | size_t block_iter_bol(BlockIter *bi) | ||
| 175 | { | ||
| 176 | block_iter_normalize(bi); | ||
| 177 | size_t offset = bi->offset; | ||
| 178 | if (offset == 0 || offset == bi->blk->size) { | ||
| 179 | return 0; | ||
| 180 | } | ||
| 181 | |||
| 182 | if (bi->blk->nl == 1) { | ||
| 183 | offset = 0; | ||
| 184 | } else { | ||
| 185 | while (offset && bi->blk->data[offset - 1] != '\n') { | ||
| 186 | offset--; | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | const size_t ret = bi->offset - offset; | ||
| 191 | bi->offset = offset; | ||
| 192 | return ret; | ||
| 193 | } | ||
| 194 | |||
| 195 | size_t block_iter_eol(BlockIter *bi) | ||
| 196 | { | ||
| 197 | block_iter_normalize(bi); | ||
| 198 | const Block *blk = bi->blk; | ||
| 199 | const size_t offset = bi->offset; | ||
| 200 | if (unlikely(offset == blk->size)) { | ||
| 201 | // Cursor at end of last block | ||
| 202 | return 0; | ||
| 203 | } | ||
| 204 | if (blk->nl == 1) { | ||
| 205 | bi->offset = blk->size - 1; | ||
| 206 | return bi->offset - offset; | ||
| 207 | } | ||
| 208 | const unsigned char *end = memchr(blk->data + offset, '\n', blk->size - offset); | ||
| 209 | BUG_ON(!end); | ||
| 210 | bi->offset = (size_t)(end - blk->data); | ||
| 211 | return bi->offset - offset; | ||
| 212 | } | ||
| 213 | |||
| 214 | void block_iter_back_bytes(BlockIter *bi, size_t count) | ||
| 215 | { | ||
| 216 | while (count > bi->offset) { | ||
| 217 | count -= bi->offset; | ||
| 218 | bi->blk = BLOCK(bi->blk->node.prev); | ||
| 219 | bi->offset = bi->blk->size; | ||
| 220 | } | ||
| 221 | bi->offset -= count; | ||
| 222 | } | ||
| 223 | |||
| 224 | void block_iter_skip_bytes(BlockIter *bi, size_t count) | ||
| 225 | { | ||
| 226 | size_t avail = bi->blk->size - bi->offset; | ||
| 227 | while (count > avail) { | ||
| 228 | count -= avail; | ||
| 229 | bi->blk = BLOCK(bi->blk->node.next); | ||
| 230 | bi->offset = 0; | ||
| 231 | avail = bi->blk->size; | ||
| 232 | } | ||
| 233 | bi->offset += count; | ||
| 234 | } | ||
| 235 | |||
| 236 | void block_iter_goto_offset(BlockIter *bi, size_t offset) | ||
| 237 | { | ||
| 238 | Block *blk; | ||
| 239 | block_for_each(blk, bi->head) { | ||
| 240 | if (offset <= blk->size) { | ||
| 241 | bi->blk = blk; | ||
| 242 | bi->offset = offset; | ||
| 243 | return; | ||
| 244 | } | ||
| 245 | offset -= blk->size; | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | void block_iter_goto_line(BlockIter *bi, size_t line) | ||
| 250 | { | ||
| 251 | Block *blk = BLOCK(bi->head->next); | ||
| 252 | size_t nl = 0; | ||
| 253 | while (blk->node.next != bi->head && nl + blk->nl < line) { | ||
| 254 | nl += blk->nl; | ||
| 255 | blk = BLOCK(blk->node.next); | ||
| 256 | } | ||
| 257 | |||
| 258 | bi->blk = blk; | ||
| 259 | bi->offset = 0; | ||
| 260 | while (nl < line) { | ||
| 261 | if (!block_iter_eat_line(bi)) { | ||
| 262 | break; | ||
| 263 | } | ||
| 264 | nl++; | ||
| 265 | } | ||
| 266 | } | ||
| 267 | |||
| 268 | size_t block_iter_get_offset(const BlockIter *bi) | ||
| 269 | { | ||
| 270 | const Block *blk; | ||
| 271 | size_t offset = 0; | ||
| 272 | block_for_each(blk, bi->head) { | ||
| 273 | if (blk == bi->blk) { | ||
| 274 | break; | ||
| 275 | } | ||
| 276 | offset += blk->size; | ||
| 277 | } | ||
| 278 | return offset + bi->offset; | ||
| 279 | } | ||
| 280 | |||
| 281 | char *block_iter_get_bytes(const BlockIter *bi, size_t len) | ||
| 282 | { | ||
| 283 | if (len == 0) { | ||
| 284 | return NULL; | ||
| 285 | } | ||
| 286 | |||
| 287 | const Block *blk = bi->blk; | ||
| 288 | size_t offset = bi->offset; | ||
| 289 | size_t pos = 0; | ||
| 290 | char *buf = xmalloc(len); | ||
| 291 | |||
| 292 | while (pos < len) { | ||
| 293 | const size_t avail = blk->size - offset; | ||
| 294 | size_t count = MIN(len - pos, avail); | ||
| 295 | memcpy(buf + pos, blk->data + offset, count); | ||
| 296 | pos += count; | ||
| 297 | BUG_ON(pos < len && blk->node.next == bi->head); | ||
| 298 | blk = BLOCK(blk->node.next); | ||
| 299 | offset = 0; | ||
| 300 | } | ||
| 301 | |||
| 302 | return buf; | ||
| 303 | } | ||
| 304 | |||
| 305 | // bi should be at bol | ||
| 306 | void fill_line_nl_ref(BlockIter *bi, StringView *line) | ||
| 307 | { | ||
| 308 | block_iter_normalize(bi); | ||
| 309 | line->data = bi->blk->data + bi->offset; | ||
| 310 | const size_t max = bi->blk->size - bi->offset; | ||
| 311 | if (unlikely(max == 0)) { | ||
| 312 | // Cursor at end of last block | ||
| 313 | line->length = 0; | ||
| 314 | return; | ||
| 315 | } | ||
| 316 | if (bi->blk->nl == 1) { | ||
| 317 | BUG_ON(line->data[max - 1] != '\n'); | ||
| 318 | line->length = max; | ||
| 319 | return; | ||
| 320 | } | ||
| 321 | const unsigned char *nl = memchr(line->data, '\n', max); | ||
| 322 | BUG_ON(!nl); | ||
| 323 | line->length = (size_t)(nl - line->data + 1); | ||
| 324 | BUG_ON(line->length == 0); | ||
| 325 | } | ||
| 326 | |||
| 327 | void fill_line_ref(BlockIter *bi, StringView *line) | ||
| 328 | { | ||
| 329 | fill_line_nl_ref(bi, line); | ||
| 330 | // Trim the newline | ||
| 331 | line->length -= (line->length > 0); | ||
| 332 | } | ||
| 333 | |||
| 334 | // Set the `line` argument to point to the current line and return | ||
| 335 | // the offset of the cursor, relative to the start of the line | ||
| 336 | // (zero means cursor is at bol) | ||
| 337 | size_t fetch_this_line(const BlockIter *bi, StringView *line) | ||
| 338 | { | ||
| 339 | BlockIter tmp = *bi; | ||
| 340 | size_t count = block_iter_bol(&tmp); | ||
| 341 | fill_line_ref(&tmp, line); | ||
| 342 | return count; | ||
| 343 | } | ||
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 @@ | |||
| 1 | #ifndef BLOCK_ITER_H | ||
| 2 | #define BLOCK_ITER_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "block.h" | ||
| 7 | #include "util/list.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/string-view.h" | ||
| 10 | #include "util/unicode.h" | ||
| 11 | |||
| 12 | typedef struct { | ||
| 13 | Block *blk; | ||
| 14 | const ListHead *head; | ||
| 15 | size_t offset; | ||
| 16 | } BlockIter; | ||
| 17 | |||
| 18 | static inline void block_iter_bof(BlockIter *bi) | ||
| 19 | { | ||
| 20 | bi->blk = BLOCK(bi->head->next); | ||
| 21 | bi->offset = 0; | ||
| 22 | } | ||
| 23 | |||
| 24 | static inline void block_iter_eof(BlockIter *bi) | ||
| 25 | { | ||
| 26 | bi->blk = BLOCK(bi->head->prev); | ||
| 27 | bi->offset = bi->blk->size; | ||
| 28 | } | ||
| 29 | |||
| 30 | static inline bool block_iter_is_eof(const BlockIter *bi) | ||
| 31 | { | ||
| 32 | return bi->offset == bi->blk->size && bi->blk->node.next == bi->head; | ||
| 33 | } | ||
| 34 | |||
| 35 | static inline bool block_iter_is_bol(const BlockIter *bi) | ||
| 36 | { | ||
| 37 | return bi->offset == 0 || bi->blk->data[bi->offset - 1] == '\n'; | ||
| 38 | } | ||
| 39 | |||
| 40 | static inline bool block_iter_is_eol(const BlockIter *bi) | ||
| 41 | { | ||
| 42 | const Block *blk = bi->blk; | ||
| 43 | size_t offset = bi->offset; | ||
| 44 | if (offset == blk->size) { | ||
| 45 | if (blk->node.next == bi->head) { | ||
| 46 | // EOF | ||
| 47 | return true; | ||
| 48 | } | ||
| 49 | // Normalize | ||
| 50 | blk = BLOCK(blk->node.next); | ||
| 51 | offset = 0; | ||
| 52 | } | ||
| 53 | return blk->data[offset] == '\n'; | ||
| 54 | } | ||
| 55 | |||
| 56 | void block_iter_normalize(BlockIter *bi); | ||
| 57 | size_t block_iter_eat_line(BlockIter *bi); | ||
| 58 | size_t block_iter_next_line(BlockIter *bi); | ||
| 59 | size_t block_iter_prev_line(BlockIter *bi); | ||
| 60 | size_t block_iter_next_char(BlockIter *bi, CodePoint *up); | ||
| 61 | size_t block_iter_prev_char(BlockIter *bi, CodePoint *up); | ||
| 62 | size_t block_iter_next_column(BlockIter *bi); | ||
| 63 | size_t block_iter_prev_column(BlockIter *bi); | ||
| 64 | size_t block_iter_bol(BlockIter *bi); | ||
| 65 | size_t block_iter_eol(BlockIter *bi); | ||
| 66 | void block_iter_back_bytes(BlockIter *bi, size_t count); | ||
| 67 | void block_iter_skip_bytes(BlockIter *bi, size_t count); | ||
| 68 | void block_iter_goto_offset(BlockIter *bi, size_t offset); | ||
| 69 | void block_iter_goto_line(BlockIter *bi, size_t line); | ||
| 70 | size_t block_iter_get_offset(const BlockIter *bi) WARN_UNUSED_RESULT; | ||
| 71 | size_t block_iter_get_char(const BlockIter *bi, CodePoint *up) WARN_UNUSED_RESULT; | ||
| 72 | char *block_iter_get_bytes(const BlockIter *bi, size_t len) WARN_UNUSED_RESULT; | ||
| 73 | |||
| 74 | void fill_line_ref(BlockIter *bi, StringView *line); | ||
| 75 | void fill_line_nl_ref(BlockIter *bi, StringView *line); | ||
| 76 | size_t fetch_this_line(const BlockIter *bi, StringView *line); | ||
| 77 | |||
| 78 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include "block.h" | ||
| 3 | #include "util/xmalloc.h" | ||
| 4 | |||
| 5 | Block *block_new(size_t alloc) | ||
| 6 | { | ||
| 7 | Block *blk = xnew0(Block, 1); | ||
| 8 | alloc = round_size_to_next_multiple(alloc, BLOCK_ALLOC_MULTIPLE); | ||
| 9 | blk->data = xmalloc(alloc); | ||
| 10 | blk->alloc = alloc; | ||
| 11 | return blk; | ||
| 12 | } | ||
| 13 | |||
| 14 | void block_free(Block *blk) | ||
| 15 | { | ||
| 16 | list_del(&blk->node); | ||
| 17 | free(blk->data); | ||
| 18 | free(blk); | ||
| 19 | } | ||
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 @@ | |||
| 1 | #ifndef BLOCK_H | ||
| 2 | #define BLOCK_H | ||
| 3 | |||
| 4 | #include <stddef.h> | ||
| 5 | #include "util/list.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | |||
| 8 | enum { | ||
| 9 | BLOCK_ALLOC_MULTIPLE = 64 | ||
| 10 | }; | ||
| 11 | |||
| 12 | // Blocks always contain whole lines. | ||
| 13 | // There's one zero-sized block for an empty file. | ||
| 14 | // Otherwise zero-sized blocks are forbidden. | ||
| 15 | typedef struct { | ||
| 16 | ListHead node; | ||
| 17 | unsigned char NONSTRING *data; | ||
| 18 | size_t size; | ||
| 19 | size_t alloc; | ||
| 20 | size_t nl; | ||
| 21 | } Block; | ||
| 22 | |||
| 23 | #define block_for_each(block_, list_head_) \ | ||
| 24 | for ( \ | ||
| 25 | block_ = BLOCK((list_head_)->next); \ | ||
| 26 | &block_->node != (list_head_); \ | ||
| 27 | block_ = BLOCK(block_->node.next) \ | ||
| 28 | ) | ||
| 29 | |||
| 30 | static inline Block *BLOCK(ListHead *item) | ||
| 31 | { | ||
| 32 | static_assert(offsetof(Block, node) == 0); | ||
| 33 | return (Block*)item; | ||
| 34 | } | ||
| 35 | |||
| 36 | Block *block_new(size_t alloc) RETURNS_NONNULL; | ||
| 37 | void block_free(Block *blk) NONNULL_ARGS; | ||
| 38 | |||
| 39 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include "bookmark.h" | ||
| 3 | #include "buffer.h" | ||
| 4 | #include "editor.h" | ||
| 5 | #include "misc.h" | ||
| 6 | #include "move.h" | ||
| 7 | #include "search.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | #include "util/xmalloc.h" | ||
| 10 | |||
| 11 | FileLocation *get_current_file_location(const View *view) | ||
| 12 | { | ||
| 13 | const char *filename = view->buffer->abs_filename; | ||
| 14 | FileLocation *loc = xmalloc(sizeof(*loc)); | ||
| 15 | *loc = (FileLocation) { | ||
| 16 | .filename = filename ? xstrdup(filename) : NULL, | ||
| 17 | .buffer_id = view->buffer->id, | ||
| 18 | .line = view->cy + 1, | ||
| 19 | .column = view->cx_char + 1 | ||
| 20 | }; | ||
| 21 | return loc; | ||
| 22 | } | ||
| 23 | |||
| 24 | bool file_location_go(Window *window, const FileLocation *loc) | ||
| 25 | { | ||
| 26 | View *view = window_open_buffer(window, loc->filename, true, NULL); | ||
| 27 | if (!view) { | ||
| 28 | // Failed to open file; error message should be visible | ||
| 29 | return false; | ||
| 30 | } | ||
| 31 | |||
| 32 | if (window->view != view) { | ||
| 33 | set_view(view); | ||
| 34 | // Force centering view to cursor, because file changed | ||
| 35 | view->force_center = true; | ||
| 36 | } | ||
| 37 | |||
| 38 | if (loc->pattern) { | ||
| 39 | if (!search_tag(view, loc->pattern)) { | ||
| 40 | return false; | ||
| 41 | } | ||
| 42 | } else if (loc->line > 0) { | ||
| 43 | move_to_filepos(view, loc->line, loc->column ? loc->column : 1); | ||
| 44 | } | ||
| 45 | |||
| 46 | unselect(view); | ||
| 47 | return true; | ||
| 48 | } | ||
| 49 | |||
| 50 | static bool file_location_return(Window *window, const FileLocation *loc) | ||
| 51 | { | ||
| 52 | Buffer *buffer = find_buffer_by_id(&window->editor->buffers, loc->buffer_id); | ||
| 53 | View *view; | ||
| 54 | if (buffer) { | ||
| 55 | view = window_get_view(window, buffer); | ||
| 56 | } else { | ||
| 57 | if (!loc->filename) { | ||
| 58 | // Can't restore closed buffer that had no filename; try again | ||
| 59 | return false; | ||
| 60 | } | ||
| 61 | view = window_open_buffer(window, loc->filename, true, NULL); | ||
| 62 | } | ||
| 63 | |||
| 64 | if (!view) { | ||
| 65 | // Open failed; don't try again | ||
| 66 | return true; | ||
| 67 | } | ||
| 68 | |||
| 69 | set_view(view); | ||
| 70 | unselect(view); | ||
| 71 | move_to_filepos(view, loc->line, loc->column); | ||
| 72 | return true; | ||
| 73 | } | ||
| 74 | |||
| 75 | void file_location_free(FileLocation *loc) | ||
| 76 | { | ||
| 77 | free(loc->filename); | ||
| 78 | free(loc->pattern); | ||
| 79 | free(loc); | ||
| 80 | } | ||
| 81 | |||
| 82 | void bookmark_push(PointerArray *bookmarks, FileLocation *loc) | ||
| 83 | { | ||
| 84 | const size_t max_entries = 256; | ||
| 85 | if (bookmarks->count == max_entries) { | ||
| 86 | file_location_free(ptr_array_remove_idx(bookmarks, 0)); | ||
| 87 | } | ||
| 88 | BUG_ON(bookmarks->count >= max_entries); | ||
| 89 | ptr_array_append(bookmarks, loc); | ||
| 90 | } | ||
| 91 | |||
| 92 | void bookmark_pop(Window *window, PointerArray *bookmarks) | ||
| 93 | { | ||
| 94 | void **ptrs = bookmarks->ptrs; | ||
| 95 | size_t count = bookmarks->count; | ||
| 96 | bool go = true; | ||
| 97 | while (count > 0 && go) { | ||
| 98 | FileLocation *loc = ptrs[--count]; | ||
| 99 | go = !file_location_return(window, loc); | ||
| 100 | file_location_free(loc); | ||
| 101 | } | ||
| 102 | bookmarks->count = count; | ||
| 103 | } | ||
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 @@ | |||
| 1 | #ifndef BOOKMARK_H | ||
| 2 | #define BOOKMARK_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "util/macros.h" | ||
| 6 | #include "util/ptr-array.h" | ||
| 7 | #include "view.h" | ||
| 8 | #include "window.h" | ||
| 9 | |||
| 10 | typedef struct { | ||
| 11 | char *filename; // Needed after buffer is closed | ||
| 12 | unsigned long buffer_id; // Needed if buffer doesn't have a filename | ||
| 13 | char *pattern; // Regex from tag file (if set, line and column are 0) | ||
| 14 | unsigned long line, column; // File position (if non-zero, pattern is NULL) | ||
| 15 | } FileLocation; | ||
| 16 | |||
| 17 | FileLocation *get_current_file_location(const View *view) NONNULL_ARGS_AND_RETURN; | ||
| 18 | bool file_location_go(Window *window, const FileLocation *loc) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 19 | void file_location_free(FileLocation *loc) NONNULL_ARGS; | ||
| 20 | |||
| 21 | void bookmark_push(PointerArray *bookmarks, FileLocation *loc) NONNULL_ARGS; | ||
| 22 | void bookmark_pop(Window *window, PointerArray *bookmarks) NONNULL_ARGS; | ||
| 23 | |||
| 24 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <sys/stat.h> | ||
| 4 | #include "buffer.h" | ||
| 5 | #include "editor.h" | ||
| 6 | #include "file-option.h" | ||
| 7 | #include "filetype.h" | ||
| 8 | #include "lock.h" | ||
| 9 | #include "syntax/state.h" | ||
| 10 | #include "util/debug.h" | ||
| 11 | #include "util/intern.h" | ||
| 12 | #include "util/log.h" | ||
| 13 | #include "util/numtostr.h" | ||
| 14 | #include "util/path.h" | ||
| 15 | #include "util/str-util.h" | ||
| 16 | #include "util/time-util.h" | ||
| 17 | #include "util/xmalloc.h" | ||
| 18 | |||
| 19 | void set_display_filename(Buffer *buffer, char *name) | ||
| 20 | { | ||
| 21 | free(buffer->display_filename); | ||
| 22 | buffer->display_filename = name; | ||
| 23 | } | ||
| 24 | |||
| 25 | /* | ||
| 26 | * Mark line range min...max (inclusive) "changed". These lines will be | ||
| 27 | * redrawn when screen is updated. This is called even when content has not | ||
| 28 | * been changed, but selection has or line has been deleted and all lines | ||
| 29 | * after the deleted line move up. | ||
| 30 | * | ||
| 31 | * Syntax highlighter has different logic. It cares about contents of the | ||
| 32 | * lines, not about selection or if the lines have been moved up or down. | ||
| 33 | */ | ||
| 34 | void buffer_mark_lines_changed(Buffer *buffer, long min, long max) | ||
| 35 | { | ||
| 36 | if (min > max) { | ||
| 37 | long tmp = min; | ||
| 38 | min = max; | ||
| 39 | max = tmp; | ||
| 40 | } | ||
| 41 | buffer->changed_line_min = MIN(min, buffer->changed_line_min); | ||
| 42 | buffer->changed_line_max = MAX(max, buffer->changed_line_max); | ||
| 43 | } | ||
| 44 | |||
| 45 | const char *buffer_filename(const Buffer *buffer) | ||
| 46 | { | ||
| 47 | const char *name = buffer->display_filename; | ||
| 48 | return name ? name : "(No name)"; | ||
| 49 | } | ||
| 50 | |||
| 51 | void buffer_set_encoding(Buffer *buffer, Encoding encoding, bool utf8_bom) | ||
| 52 | { | ||
| 53 | if ( | ||
| 54 | buffer->encoding.type != encoding.type | ||
| 55 | || buffer->encoding.name != encoding.name | ||
| 56 | ) { | ||
| 57 | const EncodingType type = encoding.type; | ||
| 58 | if (type == UTF8) { | ||
| 59 | buffer->bom = utf8_bom; | ||
| 60 | } else { | ||
| 61 | buffer->bom = type < NR_ENCODING_TYPES && !!get_bom_for_encoding(type); | ||
| 62 | } | ||
| 63 | buffer->encoding = encoding; | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | Buffer *buffer_new(PointerArray *buffers, const GlobalOptions *gopts, const Encoding *encoding) | ||
| 68 | { | ||
| 69 | static unsigned long id; | ||
| 70 | Buffer *buffer = xnew0(Buffer, 1); | ||
| 71 | list_init(&buffer->blocks); | ||
| 72 | buffer->cur_change = &buffer->change_head; | ||
| 73 | buffer->saved_change = &buffer->change_head; | ||
| 74 | buffer->id = ++id; | ||
| 75 | buffer->crlf_newlines = gopts->crlf_newlines; | ||
| 76 | |||
| 77 | if (encoding) { | ||
| 78 | buffer_set_encoding(buffer, *encoding, gopts->utf8_bom); | ||
| 79 | } else { | ||
| 80 | buffer->encoding.type = ENCODING_AUTODETECT; | ||
| 81 | } | ||
| 82 | |||
| 83 | static_assert(sizeof(*gopts) >= sizeof(CommonOptions)); | ||
| 84 | memcpy(&buffer->options, gopts, sizeof(CommonOptions)); | ||
| 85 | buffer->options.brace_indent = 0; | ||
| 86 | buffer->options.filetype = str_intern("none"); | ||
| 87 | buffer->options.indent_regex = NULL; | ||
| 88 | |||
| 89 | ptr_array_append(buffers, buffer); | ||
| 90 | return buffer; | ||
| 91 | } | ||
| 92 | |||
| 93 | Buffer *open_empty_buffer(PointerArray *buffers, const GlobalOptions *gopts) | ||
| 94 | { | ||
| 95 | Encoding enc = encoding_from_type(UTF8); | ||
| 96 | Buffer *buffer = buffer_new(buffers, gopts, &enc); | ||
| 97 | |||
| 98 | // At least one block required | ||
| 99 | Block *blk = block_new(1); | ||
| 100 | list_add_before(&blk->node, &buffer->blocks); | ||
| 101 | |||
| 102 | return buffer; | ||
| 103 | } | ||
| 104 | |||
| 105 | void free_blocks(Buffer *buffer) | ||
| 106 | { | ||
| 107 | ListHead *item = buffer->blocks.next; | ||
| 108 | while (item != &buffer->blocks) { | ||
| 109 | ListHead *next = item->next; | ||
| 110 | Block *blk = BLOCK(item); | ||
| 111 | free(blk->data); | ||
| 112 | free(blk); | ||
| 113 | item = next; | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | void free_buffer(Buffer *buffer) | ||
| 118 | { | ||
| 119 | if (buffer->locked) { | ||
| 120 | unlock_file(buffer->abs_filename); | ||
| 121 | } | ||
| 122 | |||
| 123 | free_changes(&buffer->change_head); | ||
| 124 | free(buffer->line_start_states.ptrs); | ||
| 125 | free(buffer->views.ptrs); | ||
| 126 | free(buffer->display_filename); | ||
| 127 | free(buffer->abs_filename); | ||
| 128 | |||
| 129 | if (buffer->stdout_buffer) { | ||
| 130 | return; | ||
| 131 | } | ||
| 132 | |||
| 133 | free_blocks(buffer); | ||
| 134 | free(buffer); | ||
| 135 | } | ||
| 136 | |||
| 137 | void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer) | ||
| 138 | { | ||
| 139 | ptr_array_remove(buffers, buffer); | ||
| 140 | free_buffer(buffer); | ||
| 141 | } | ||
| 142 | |||
| 143 | static bool same_file(const Buffer *buffer, const struct stat *st) | ||
| 144 | { | ||
| 145 | return (st->st_dev == buffer->file.dev) && (st->st_ino == buffer->file.ino); | ||
| 146 | } | ||
| 147 | |||
| 148 | Buffer *find_buffer(const PointerArray *buffers, const char *abs_filename) | ||
| 149 | { | ||
| 150 | struct stat st; | ||
| 151 | bool st_ok = stat(abs_filename, &st) == 0; | ||
| 152 | for (size_t i = 0, n = buffers->count; i < n; i++) { | ||
| 153 | Buffer *buffer = buffers->ptrs[i]; | ||
| 154 | const char *f = buffer->abs_filename; | ||
| 155 | if ((f && streq(f, abs_filename)) || (st_ok && same_file(buffer, &st))) { | ||
| 156 | return buffer; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | return NULL; | ||
| 160 | } | ||
| 161 | |||
| 162 | Buffer *find_buffer_by_id(const PointerArray *buffers, unsigned long id) | ||
| 163 | { | ||
| 164 | for (size_t i = 0, n = buffers->count; i < n; i++) { | ||
| 165 | Buffer *buffer = buffers->ptrs[i]; | ||
| 166 | if (buffer->id == id) { | ||
| 167 | return buffer; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | return NULL; | ||
| 171 | } | ||
| 172 | |||
| 173 | bool buffer_detect_filetype(Buffer *buffer, const PointerArray *filetypes) | ||
| 174 | { | ||
| 175 | StringView line = STRING_VIEW_INIT; | ||
| 176 | if (BLOCK(buffer->blocks.next)->size) { | ||
| 177 | BlockIter bi = block_iter(buffer); | ||
| 178 | fill_line_ref(&bi, &line); | ||
| 179 | } else if (!buffer->abs_filename) { | ||
| 180 | return false; | ||
| 181 | } | ||
| 182 | |||
| 183 | const char *ft = find_ft(filetypes, buffer->abs_filename, line); | ||
| 184 | if (ft && !streq(ft, buffer->options.filetype)) { | ||
| 185 | buffer->options.filetype = str_intern(ft); | ||
| 186 | return true; | ||
| 187 | } | ||
| 188 | |||
| 189 | return false; | ||
| 190 | } | ||
| 191 | |||
| 192 | void update_short_filename_cwd(Buffer *buffer, const StringView *home, const char *cwd) | ||
| 193 | { | ||
| 194 | const char *abs = buffer->abs_filename; | ||
| 195 | if (!abs) { | ||
| 196 | return; | ||
| 197 | } | ||
| 198 | char *name = cwd ? short_filename_cwd(abs, cwd, home) : xstrdup(abs); | ||
| 199 | set_display_filename(buffer, name); | ||
| 200 | } | ||
| 201 | |||
| 202 | void update_short_filename(Buffer *buffer, const StringView *home) | ||
| 203 | { | ||
| 204 | BUG_ON(!buffer->abs_filename); | ||
| 205 | set_display_filename(buffer, short_filename(buffer->abs_filename, home)); | ||
| 206 | } | ||
| 207 | |||
| 208 | void buffer_update_syntax(EditorState *e, Buffer *buffer) | ||
| 209 | { | ||
| 210 | Syntax *syn = NULL; | ||
| 211 | if (buffer->options.syntax) { | ||
| 212 | // Even "none" can have syntax | ||
| 213 | syn = find_syntax(&e->syntaxes, buffer->options.filetype); | ||
| 214 | if (!syn) { | ||
| 215 | syn = load_syntax_by_filetype(e, buffer->options.filetype); | ||
| 216 | } | ||
| 217 | } | ||
| 218 | if (syn == buffer->syn) { | ||
| 219 | return; | ||
| 220 | } | ||
| 221 | |||
| 222 | buffer->syn = syn; | ||
| 223 | if (syn) { | ||
| 224 | // Start state of first line is constant | ||
| 225 | PointerArray *s = &buffer->line_start_states; | ||
| 226 | if (!s->alloc) { | ||
| 227 | ptr_array_init(s, 64); | ||
| 228 | } | ||
| 229 | s->ptrs[0] = syn->start_state; | ||
| 230 | s->count = 1; | ||
| 231 | } | ||
| 232 | |||
| 233 | mark_all_lines_changed(buffer); | ||
| 234 | } | ||
| 235 | |||
| 236 | static bool allow_odd_indent(uint8_t indents_bitmask) | ||
| 237 | { | ||
| 238 | static_assert(INDENT_WIDTH_MAX == 8); | ||
| 239 | return !!(indents_bitmask & 0x55); // 0x55 == 0b01010101 | ||
| 240 | } | ||
| 241 | |||
| 242 | static int indent_len(StringView line, uint8_t indents_bitmask, bool *tab_indent) | ||
| 243 | { | ||
| 244 | bool space_before_tab = false; | ||
| 245 | size_t spaces = 0; | ||
| 246 | size_t tabs = 0; | ||
| 247 | size_t pos = 0; | ||
| 248 | |||
| 249 | for (size_t n = line.length; pos < n; pos++) { | ||
| 250 | switch (line.data[pos]) { | ||
| 251 | case '\t': | ||
| 252 | tabs++; | ||
| 253 | if (spaces) { | ||
| 254 | space_before_tab = true; | ||
| 255 | } | ||
| 256 | continue; | ||
| 257 | case ' ': | ||
| 258 | spaces++; | ||
| 259 | continue; | ||
| 260 | } | ||
| 261 | break; | ||
| 262 | } | ||
| 263 | |||
| 264 | *tab_indent = false; | ||
| 265 | if (pos == line.length) { | ||
| 266 | return -1; // Whitespace only | ||
| 267 | } | ||
| 268 | if (pos == 0) { | ||
| 269 | return 0; // Not indented | ||
| 270 | } | ||
| 271 | if (space_before_tab) { | ||
| 272 | return -2; // Mixed indent | ||
| 273 | } | ||
| 274 | if (tabs) { | ||
| 275 | // Tabs and possible spaces after tab for alignment | ||
| 276 | *tab_indent = true; | ||
| 277 | return tabs * 8; | ||
| 278 | } | ||
| 279 | if (line.length > spaces && line.data[spaces] == '*') { | ||
| 280 | // '*' after indent, could be long C style comment | ||
| 281 | if (spaces & 1 || allow_odd_indent(indents_bitmask)) { | ||
| 282 | return spaces - 1; | ||
| 283 | } | ||
| 284 | } | ||
| 285 | return spaces; | ||
| 286 | } | ||
| 287 | |||
| 288 | UNITTEST { | ||
| 289 | bool tab; | ||
| 290 | int len = indent_len(strview_from_cstring(" 4 space"), 0, &tab); | ||
| 291 | BUG_ON(len != 4); | ||
| 292 | BUG_ON(tab); | ||
| 293 | |||
| 294 | len = indent_len(strview_from_cstring("\t\t2 tab"), 0, &tab); | ||
| 295 | BUG_ON(len != 16); | ||
| 296 | BUG_ON(!tab); | ||
| 297 | |||
| 298 | len = indent_len(strview_from_cstring("no indent"), 0, &tab); | ||
| 299 | BUG_ON(len != 0); | ||
| 300 | |||
| 301 | len = indent_len(strview_from_cstring(" \t mixed"), 0, &tab); | ||
| 302 | BUG_ON(len != -2); | ||
| 303 | |||
| 304 | len = indent_len(strview_from_cstring("\t \t "), 0, &tab); | ||
| 305 | BUG_ON(len != -1); // whitespace only | ||
| 306 | |||
| 307 | len = indent_len(strview_from_cstring(" * 5 space"), 0, &tab); | ||
| 308 | BUG_ON(len != 4); | ||
| 309 | |||
| 310 | StringView line = strview_from_cstring(" * 4 space"); | ||
| 311 | len = indent_len(line, 0, &tab); | ||
| 312 | BUG_ON(len != 4); | ||
| 313 | len = indent_len(line, 1 << 2, &tab); | ||
| 314 | BUG_ON(len != 3); | ||
| 315 | } | ||
| 316 | |||
| 317 | static bool detect_indent(Buffer *buffer) | ||
| 318 | { | ||
| 319 | LocalOptions *options = &buffer->options; | ||
| 320 | unsigned int bitset = options->detect_indent; | ||
| 321 | BlockIter bi = block_iter(buffer); | ||
| 322 | unsigned int tab_count = 0; | ||
| 323 | unsigned int space_count = 0; | ||
| 324 | int current_indent = 0; | ||
| 325 | int counts[INDENT_WIDTH_MAX + 1] = {0}; | ||
| 326 | BUG_ON((bitset & ((1u << INDENT_WIDTH_MAX) - 1)) != bitset); | ||
| 327 | |||
| 328 | for (size_t i = 0, j = 1; i < 200 && j > 0; i++, j = block_iter_next_line(&bi)) { | ||
| 329 | StringView line; | ||
| 330 | fill_line_ref(&bi, &line); | ||
| 331 | bool tab; | ||
| 332 | int indent = indent_len(line, bitset, &tab); | ||
| 333 | switch (indent) { | ||
| 334 | case -2: // Ignore mixed indent because tab width might not be 8 | ||
| 335 | case -1: // Empty line; no change in indent | ||
| 336 | continue; | ||
| 337 | case 0: | ||
| 338 | current_indent = 0; | ||
| 339 | continue; | ||
| 340 | } | ||
| 341 | |||
| 342 | BUG_ON(indent <= 0); | ||
| 343 | int change = indent - current_indent; | ||
| 344 | if (change >= 1 && change <= INDENT_WIDTH_MAX) { | ||
| 345 | counts[change]++; | ||
| 346 | } | ||
| 347 | |||
| 348 | if (tab) { | ||
| 349 | tab_count++; | ||
| 350 | } else { | ||
| 351 | space_count++; | ||
| 352 | } | ||
| 353 | current_indent = indent; | ||
| 354 | } | ||
| 355 | |||
| 356 | if (tab_count == 0 && space_count == 0) { | ||
| 357 | return false; | ||
| 358 | } | ||
| 359 | |||
| 360 | if (tab_count > space_count) { | ||
| 361 | options->emulate_tab = false; | ||
| 362 | options->expand_tab = false; | ||
| 363 | options->indent_width = options->tab_width; | ||
| 364 | return true; | ||
| 365 | } | ||
| 366 | |||
| 367 | size_t m = 0; | ||
| 368 | for (size_t i = 1; i < ARRAYLEN(counts); i++) { | ||
| 369 | unsigned int bit = 1u << (i - 1); | ||
| 370 | if ((bitset & bit) && counts[i] > counts[m]) { | ||
| 371 | m = i; | ||
| 372 | } | ||
| 373 | } | ||
| 374 | |||
| 375 | if (m == 0) { | ||
| 376 | return false; | ||
| 377 | } | ||
| 378 | |||
| 379 | options->emulate_tab = true; | ||
| 380 | options->expand_tab = true; | ||
| 381 | options->indent_width = m; | ||
| 382 | return true; | ||
| 383 | } | ||
| 384 | |||
| 385 | void buffer_setup(EditorState *e, Buffer *buffer) | ||
| 386 | { | ||
| 387 | const char *filename = buffer->abs_filename; | ||
| 388 | buffer->setup = true; | ||
| 389 | buffer_detect_filetype(buffer, &e->filetypes); | ||
| 390 | set_file_options(e, buffer); | ||
| 391 | set_editorconfig_options(buffer); | ||
| 392 | buffer_update_syntax(e, buffer); | ||
| 393 | if (buffer->options.detect_indent && filename) { | ||
| 394 | detect_indent(buffer); | ||
| 395 | } | ||
| 396 | sanity_check_local_options(&buffer->options); | ||
| 397 | } | ||
| 398 | |||
| 399 | void buffer_count_blocks_and_bytes(const Buffer *buffer, uintmax_t counts[2]) | ||
| 400 | { | ||
| 401 | uintmax_t blocks = 0; | ||
| 402 | uintmax_t bytes = 0; | ||
| 403 | Block *blk; | ||
| 404 | block_for_each(blk, &buffer->blocks) { | ||
| 405 | blocks += 1; | ||
| 406 | bytes += blk->size; | ||
| 407 | } | ||
| 408 | counts[0] = blocks; | ||
| 409 | counts[1] = bytes; | ||
| 410 | } | ||
| 411 | |||
| 412 | // TODO: Human-readable size (MiB/GiB/etc.) for "Bytes" and FileInfo::size | ||
| 413 | String dump_buffer(const Buffer *buffer) | ||
| 414 | { | ||
| 415 | uintmax_t counts[2]; | ||
| 416 | buffer_count_blocks_and_bytes(buffer, counts); | ||
| 417 | BUG_ON(counts[0] < 1); | ||
| 418 | BUG_ON(!buffer->setup); | ||
| 419 | String buf = string_new(1024); | ||
| 420 | |||
| 421 | string_sprintf ( | ||
| 422 | &buf, | ||
| 423 | "%s %s\n%s %lu\n%s %s\n%s %s\n%s %ju\n%s %zu\n%s %ju\n", | ||
| 424 | " Name:", buffer_filename(buffer), | ||
| 425 | " ID:", buffer->id, | ||
| 426 | " Encoding:", buffer->encoding.name, | ||
| 427 | " Filetype:", buffer->options.filetype, | ||
| 428 | " Blocks:", counts[0], | ||
| 429 | " Lines:", buffer->nl, | ||
| 430 | " Bytes:", counts[1] | ||
| 431 | ); | ||
| 432 | |||
| 433 | if ( | ||
| 434 | buffer->stdout_buffer || buffer->temporary || buffer->readonly | ||
| 435 | || buffer->locked || buffer->crlf_newlines || buffer->bom | ||
| 436 | ) { | ||
| 437 | string_sprintf ( | ||
| 438 | &buf, | ||
| 439 | " Flags:%s%s%s%s%s%s\n", | ||
| 440 | buffer->stdout_buffer ? " STDOUT" : "", | ||
| 441 | buffer->temporary ? " TMP" : "", | ||
| 442 | buffer->readonly ? " RO" : "", | ||
| 443 | buffer->locked ? " LOCKED" : "", | ||
| 444 | buffer->crlf_newlines ? " CRLF" : "", | ||
| 445 | buffer->bom ? " BOM" : "" | ||
| 446 | ); | ||
| 447 | } | ||
| 448 | |||
| 449 | if (buffer->views.count > 1) { | ||
| 450 | string_sprintf(&buf, " Views: %zu\n", buffer->views.count); | ||
| 451 | } | ||
| 452 | |||
| 453 | if (!buffer->abs_filename) { | ||
| 454 | return buf; | ||
| 455 | } | ||
| 456 | |||
| 457 | const FileInfo *file = &buffer->file; | ||
| 458 | unsigned int perms = file->mode & 07777; | ||
| 459 | char modestr[12]; | ||
| 460 | char timestr[64]; | ||
| 461 | if (!timespec_to_str(&file->mtime, timestr, sizeof(timestr))) { | ||
| 462 | memcpy(timestr, STRN("[error]") + 1); | ||
| 463 | } | ||
| 464 | |||
| 465 | string_sprintf ( | ||
| 466 | &buf, | ||
| 467 | "\nLast stat:\n----------\n\n" | ||
| 468 | "%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", | ||
| 469 | " Path:", buffer->abs_filename, | ||
| 470 | " Modified:", timestr, | ||
| 471 | " Mode:", filemode_to_str(file->mode, modestr), perms, | ||
| 472 | " User:", (intmax_t)file->uid, | ||
| 473 | " Group:", (intmax_t)file->gid, | ||
| 474 | " Size:", (uintmax_t)file->size, | ||
| 475 | " Device:", (intmax_t)file->dev, | ||
| 476 | " Inode:", (uintmax_t)file->ino | ||
| 477 | ); | ||
| 478 | |||
| 479 | return buf; | ||
| 480 | } | ||
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 @@ | |||
| 1 | #ifndef BUFFER_H | ||
| 2 | #define BUFFER_H | ||
| 3 | |||
| 4 | #include <limits.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stdint.h> | ||
| 7 | #include <sys/types.h> | ||
| 8 | #include <time.h> | ||
| 9 | #include "block-iter.h" | ||
| 10 | #include "change.h" | ||
| 11 | #include "encoding.h" | ||
| 12 | #include "options.h" | ||
| 13 | #include "syntax/syntax.h" | ||
| 14 | #include "util/list.h" | ||
| 15 | #include "util/macros.h" | ||
| 16 | #include "util/ptr-array.h" | ||
| 17 | #include "util/string-view.h" | ||
| 18 | #include "util/string.h" | ||
| 19 | |||
| 20 | // Subset of stat(3) struct | ||
| 21 | typedef struct { | ||
| 22 | dev_t dev; | ||
| 23 | ino_t ino; | ||
| 24 | mode_t mode; | ||
| 25 | uid_t uid; | ||
| 26 | gid_t gid; | ||
| 27 | off_t size; | ||
| 28 | struct timespec mtime; | ||
| 29 | } FileInfo; | ||
| 30 | |||
| 31 | // A representation of a specific file, as it pertains to editing, | ||
| 32 | // including text contents, filename (if saved), undo history and | ||
| 33 | // some file-specific metadata and options. | ||
| 34 | typedef struct Buffer { | ||
| 35 | ListHead blocks; | ||
| 36 | Change change_head; | ||
| 37 | Change *cur_change; | ||
| 38 | Change *saved_change; // Used to determine if buffer is modified | ||
| 39 | FileInfo file; | ||
| 40 | unsigned long id; // Needed for identifying buffers whose filename is NULL | ||
| 41 | size_t nl; | ||
| 42 | PointerArray views; // Views pointing to this buffer | ||
| 43 | char *display_filename; | ||
| 44 | char *abs_filename; | ||
| 45 | bool readonly; | ||
| 46 | bool temporary; | ||
| 47 | bool stdout_buffer; | ||
| 48 | bool locked; | ||
| 49 | bool setup; | ||
| 50 | bool crlf_newlines; | ||
| 51 | bool bom; | ||
| 52 | Encoding encoding; // Encoding of the file (buffer always contains UTF-8) | ||
| 53 | LocalOptions options; | ||
| 54 | Syntax *syn; | ||
| 55 | long changed_line_min; | ||
| 56 | long changed_line_max; | ||
| 57 | // Index 0 is always syn->states.ptrs[0]. | ||
| 58 | // Lowest bit of an invalidated value is 1. | ||
| 59 | PointerArray line_start_states; | ||
| 60 | } Buffer; | ||
| 61 | |||
| 62 | static inline void mark_all_lines_changed(Buffer *buffer) | ||
| 63 | { | ||
| 64 | buffer->changed_line_min = 0; | ||
| 65 | buffer->changed_line_max = LONG_MAX; | ||
| 66 | } | ||
| 67 | |||
| 68 | static inline bool buffer_modified(const Buffer *buffer) | ||
| 69 | { | ||
| 70 | return buffer->saved_change != buffer->cur_change && !buffer->temporary; | ||
| 71 | } | ||
| 72 | |||
| 73 | static inline BlockIter block_iter(Buffer *buffer) | ||
| 74 | { | ||
| 75 | return (BlockIter) { | ||
| 76 | .blk = BLOCK(buffer->blocks.next), | ||
| 77 | .head = &buffer->blocks, | ||
| 78 | .offset = 0 | ||
| 79 | }; | ||
| 80 | } | ||
| 81 | |||
| 82 | struct EditorState; | ||
| 83 | |||
| 84 | void buffer_mark_lines_changed(Buffer *buffer, long min, long max) NONNULL_ARGS; | ||
| 85 | void buffer_set_encoding(Buffer *buffer, Encoding encoding, bool utf8_bom) NONNULL_ARGS; | ||
| 86 | const char *buffer_filename(const Buffer *buffer) NONNULL_ARGS_AND_RETURN; | ||
| 87 | void set_display_filename(Buffer *buffer, char *name) NONNULL_ARG(1); | ||
| 88 | void update_short_filename_cwd(Buffer *buffer, const StringView *home, const char *cwd) NONNULL_ARG(1, 2); | ||
| 89 | void update_short_filename(Buffer *buffer, const StringView *home) NONNULL_ARGS; | ||
| 90 | Buffer *find_buffer(const PointerArray *buffers, const char *abs_filename) NONNULL_ARGS; | ||
| 91 | Buffer *find_buffer_by_id(const PointerArray *buffers, unsigned long id) NONNULL_ARGS; | ||
| 92 | Buffer *buffer_new(PointerArray *buffers, const GlobalOptions *gopts, const Encoding *encoding) RETURNS_NONNULL NONNULL_ARG(1, 2); | ||
| 93 | Buffer *open_empty_buffer(PointerArray *buffers, const GlobalOptions *gopts) NONNULL_ARGS_AND_RETURN; | ||
| 94 | void free_buffer(Buffer *buffer) NONNULL_ARGS; | ||
| 95 | void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer) NONNULL_ARGS; | ||
| 96 | void free_blocks(Buffer *buffer) NONNULL_ARGS; | ||
| 97 | bool buffer_detect_filetype(Buffer *buffer, const PointerArray *filetypes) NONNULL_ARGS; | ||
| 98 | void buffer_update_syntax(struct EditorState *e, Buffer *buffer) NONNULL_ARGS; | ||
| 99 | void buffer_setup(struct EditorState *e, Buffer *buffer) NONNULL_ARGS; | ||
| 100 | void buffer_count_blocks_and_bytes(const Buffer *buffer, uintmax_t counts[2]) NONNULL_ARGS; | ||
| 101 | String dump_buffer(const Buffer *buffer) NONNULL_ARGS; | ||
| 102 | |||
| 103 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "change.h" | ||
| 4 | #include "buffer.h" | ||
| 5 | #include "edit.h" | ||
| 6 | #include "error.h" | ||
| 7 | #include "util/debug.h" | ||
| 8 | #include "util/xmalloc.h" | ||
| 9 | |||
| 10 | static ChangeMergeEnum change_merge; | ||
| 11 | static ChangeMergeEnum prev_change_merge; | ||
| 12 | |||
| 13 | static Change *alloc_change(void) | ||
| 14 | { | ||
| 15 | return xcalloc(sizeof(Change)); | ||
| 16 | } | ||
| 17 | |||
| 18 | static void add_change(Buffer *buffer, Change *change) | ||
| 19 | { | ||
| 20 | Change *head = buffer->cur_change; | ||
| 21 | change->next = head; | ||
| 22 | xrenew(head->prev, head->nr_prev + 1); | ||
| 23 | head->prev[head->nr_prev++] = change; | ||
| 24 | buffer->cur_change = change; | ||
| 25 | } | ||
| 26 | |||
| 27 | // This doesn't need to be local to buffer because commands are atomic | ||
| 28 | static Change *change_barrier; | ||
| 29 | |||
| 30 | static bool is_change_chain_barrier(const Change *change) | ||
| 31 | { | ||
| 32 | return !change->ins_count && !change->del_count; | ||
| 33 | } | ||
| 34 | |||
| 35 | static Change *new_change(Buffer *buffer) | ||
| 36 | { | ||
| 37 | if (change_barrier) { | ||
| 38 | /* | ||
| 39 | * We are recording series of changes (:replace for example) | ||
| 40 | * and now we have just made the first change so we have to | ||
| 41 | * mark beginning of the chain. | ||
| 42 | * | ||
| 43 | * We could have done this before when starting the change | ||
| 44 | * chain but then we may have ended up with an empty chain. | ||
| 45 | * We don't want to record empty changes ever. | ||
| 46 | */ | ||
| 47 | add_change(buffer, change_barrier); | ||
| 48 | change_barrier = NULL; | ||
| 49 | } | ||
| 50 | |||
| 51 | Change *change = alloc_change(); | ||
| 52 | add_change(buffer, change); | ||
| 53 | return change; | ||
| 54 | } | ||
| 55 | |||
| 56 | static size_t buffer_offset(const View *view) | ||
| 57 | { | ||
| 58 | return block_iter_get_offset(&view->cursor); | ||
| 59 | } | ||
| 60 | |||
| 61 | static void record_insert(View *view, size_t len) | ||
| 62 | { | ||
| 63 | Change *change = view->buffer->cur_change; | ||
| 64 | BUG_ON(!len); | ||
| 65 | if ( | ||
| 66 | change_merge == prev_change_merge | ||
| 67 | && change_merge == CHANGE_MERGE_INSERT | ||
| 68 | ) { | ||
| 69 | BUG_ON(change->del_count); | ||
| 70 | change->ins_count += len; | ||
| 71 | return; | ||
| 72 | } | ||
| 73 | |||
| 74 | change = new_change(view->buffer); | ||
| 75 | change->offset = buffer_offset(view); | ||
| 76 | change->ins_count = len; | ||
| 77 | } | ||
| 78 | |||
| 79 | static void record_delete(View *view, char *buf, size_t len, bool move_after) | ||
| 80 | { | ||
| 81 | BUG_ON(!len); | ||
| 82 | BUG_ON(!buf); | ||
| 83 | |||
| 84 | Change *change = view->buffer->cur_change; | ||
| 85 | if (change_merge == prev_change_merge) { | ||
| 86 | if (change_merge == CHANGE_MERGE_DELETE) { | ||
| 87 | xrenew(change->buf, change->del_count + len); | ||
| 88 | memcpy(change->buf + change->del_count, buf, len); | ||
| 89 | change->del_count += len; | ||
| 90 | free(buf); | ||
| 91 | return; | ||
| 92 | } | ||
| 93 | if (change_merge == CHANGE_MERGE_ERASE) { | ||
| 94 | xrenew(buf, len + change->del_count); | ||
| 95 | memcpy(buf + len, change->buf, change->del_count); | ||
| 96 | change->del_count += len; | ||
| 97 | free(change->buf); | ||
| 98 | change->buf = buf; | ||
| 99 | change->offset -= len; | ||
| 100 | return; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | change = new_change(view->buffer); | ||
| 105 | change->offset = buffer_offset(view); | ||
| 106 | change->del_count = len; | ||
| 107 | change->move_after = move_after; | ||
| 108 | change->buf = buf; | ||
| 109 | } | ||
| 110 | |||
| 111 | static void record_replace(View *view, char *deleted, size_t del_count, size_t ins_count) | ||
| 112 | { | ||
| 113 | BUG_ON(del_count && !deleted); | ||
| 114 | BUG_ON(!del_count && deleted); | ||
| 115 | BUG_ON(!del_count && !ins_count); | ||
| 116 | |||
| 117 | Change *change = new_change(view->buffer); | ||
| 118 | change->offset = buffer_offset(view); | ||
| 119 | change->ins_count = ins_count; | ||
| 120 | change->del_count = del_count; | ||
| 121 | change->buf = deleted; | ||
| 122 | } | ||
| 123 | |||
| 124 | void begin_change(ChangeMergeEnum m) | ||
| 125 | { | ||
| 126 | change_merge = m; | ||
| 127 | } | ||
| 128 | |||
| 129 | void end_change(void) | ||
| 130 | { | ||
| 131 | prev_change_merge = change_merge; | ||
| 132 | } | ||
| 133 | |||
| 134 | void begin_change_chain(void) | ||
| 135 | { | ||
| 136 | BUG_ON(change_barrier); | ||
| 137 | |||
| 138 | // Allocate change chain barrier but add it to the change tree only if | ||
| 139 | // there will be any real changes | ||
| 140 | change_barrier = alloc_change(); | ||
| 141 | change_merge = CHANGE_MERGE_NONE; | ||
| 142 | } | ||
| 143 | |||
| 144 | void end_change_chain(View *view) | ||
| 145 | { | ||
| 146 | if (change_barrier) { | ||
| 147 | // There were no changes in this change chain | ||
| 148 | free(change_barrier); | ||
| 149 | change_barrier = NULL; | ||
| 150 | } else { | ||
| 151 | // There were some changes; add end of chain marker | ||
| 152 | add_change(view->buffer, alloc_change()); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | static void fix_cursors(const View *view, size_t offset, size_t del, size_t ins) | ||
| 157 | { | ||
| 158 | const Buffer *buffer = view->buffer; | ||
| 159 | for (size_t i = 0, n = buffer->views.count; i < n; i++) { | ||
| 160 | View *v = buffer->views.ptrs[i]; | ||
| 161 | if (v != view && offset < v->saved_cursor_offset) { | ||
| 162 | if (offset + del <= v->saved_cursor_offset) { | ||
| 163 | v->saved_cursor_offset -= del; | ||
| 164 | v->saved_cursor_offset += ins; | ||
| 165 | } else { | ||
| 166 | v->saved_cursor_offset = offset; | ||
| 167 | } | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | static void reverse_change(View *view, Change *change) | ||
| 173 | { | ||
| 174 | if (view->buffer->views.count > 1) { | ||
| 175 | fix_cursors(view, change->offset, change->ins_count, change->del_count); | ||
| 176 | } | ||
| 177 | |||
| 178 | block_iter_goto_offset(&view->cursor, change->offset); | ||
| 179 | if (!change->ins_count) { | ||
| 180 | // Convert delete to insert | ||
| 181 | do_insert(view, change->buf, change->del_count); | ||
| 182 | if (change->move_after) { | ||
| 183 | block_iter_skip_bytes(&view->cursor, change->del_count); | ||
| 184 | } | ||
| 185 | change->ins_count = change->del_count; | ||
| 186 | change->del_count = 0; | ||
| 187 | free(change->buf); | ||
| 188 | change->buf = NULL; | ||
| 189 | } else if (change->del_count) { | ||
| 190 | // Reverse replace | ||
| 191 | size_t del_count = change->ins_count; | ||
| 192 | size_t ins_count = change->del_count; | ||
| 193 | char *buf = do_replace(view, del_count, change->buf, ins_count); | ||
| 194 | free(change->buf); | ||
| 195 | change->buf = buf; | ||
| 196 | change->ins_count = ins_count; | ||
| 197 | change->del_count = del_count; | ||
| 198 | } else { | ||
| 199 | // Convert insert to delete | ||
| 200 | change->buf = do_delete(view, change->ins_count, true); | ||
| 201 | change->del_count = change->ins_count; | ||
| 202 | change->ins_count = 0; | ||
| 203 | } | ||
| 204 | } | ||
| 205 | |||
| 206 | bool undo(View *view) | ||
| 207 | { | ||
| 208 | Change *change = view->buffer->cur_change; | ||
| 209 | view_reset_preferred_x(view); | ||
| 210 | if (!change->next) { | ||
| 211 | return false; | ||
| 212 | } | ||
| 213 | |||
| 214 | if (is_change_chain_barrier(change)) { | ||
| 215 | unsigned long count = 0; | ||
| 216 | while (1) { | ||
| 217 | change = change->next; | ||
| 218 | if (is_change_chain_barrier(change)) { | ||
| 219 | break; | ||
| 220 | } | ||
| 221 | reverse_change(view, change); | ||
| 222 | count++; | ||
| 223 | } | ||
| 224 | if (count > 1) { | ||
| 225 | info_msg("Undid %lu changes", count); | ||
| 226 | } | ||
| 227 | } else { | ||
| 228 | reverse_change(view, change); | ||
| 229 | } | ||
| 230 | |||
| 231 | view->buffer->cur_change = change->next; | ||
| 232 | return true; | ||
| 233 | } | ||
| 234 | |||
| 235 | bool redo(View *view, unsigned long change_id) | ||
| 236 | { | ||
| 237 | Change *change = view->buffer->cur_change; | ||
| 238 | view_reset_preferred_x(view); | ||
| 239 | if (!change->prev) { | ||
| 240 | // Don't complain if change_id is 0 | ||
| 241 | if (change_id) { | ||
| 242 | error_msg("Nothing to redo"); | ||
| 243 | } | ||
| 244 | return false; | ||
| 245 | } | ||
| 246 | |||
| 247 | const unsigned long nr_prev = change->nr_prev; | ||
| 248 | BUG_ON(nr_prev == 0); | ||
| 249 | if (change_id == 0) { | ||
| 250 | // Default to newest change | ||
| 251 | change_id = nr_prev - 1; | ||
| 252 | if (nr_prev > 1) { | ||
| 253 | unsigned long i = change_id + 1; | ||
| 254 | info_msg("Redoing newest (%lu) of %lu possible changes", i, nr_prev); | ||
| 255 | } | ||
| 256 | } else { | ||
| 257 | if (--change_id >= nr_prev) { | ||
| 258 | if (nr_prev == 1) { | ||
| 259 | return error_msg("There is only 1 possible change to redo"); | ||
| 260 | } | ||
| 261 | return error_msg("There are only %lu possible changes to redo", nr_prev); | ||
| 262 | } | ||
| 263 | } | ||
| 264 | |||
| 265 | change = change->prev[change_id]; | ||
| 266 | if (is_change_chain_barrier(change)) { | ||
| 267 | unsigned long count = 0; | ||
| 268 | while (1) { | ||
| 269 | change = change->prev[change->nr_prev - 1]; | ||
| 270 | if (is_change_chain_barrier(change)) { | ||
| 271 | break; | ||
| 272 | } | ||
| 273 | reverse_change(view, change); | ||
| 274 | count++; | ||
| 275 | } | ||
| 276 | if (count > 1) { | ||
| 277 | info_msg("Redid %lu changes", count); | ||
| 278 | } | ||
| 279 | } else { | ||
| 280 | reverse_change(view, change); | ||
| 281 | } | ||
| 282 | |||
| 283 | view->buffer->cur_change = change; | ||
| 284 | return true; | ||
| 285 | } | ||
| 286 | |||
| 287 | void free_changes(Change *c) | ||
| 288 | { | ||
| 289 | top: | ||
| 290 | while (c->nr_prev) { | ||
| 291 | c = c->prev[c->nr_prev - 1]; | ||
| 292 | } | ||
| 293 | |||
| 294 | // c is leaf now | ||
| 295 | while (c->next) { | ||
| 296 | Change *next = c->next; | ||
| 297 | free(c->buf); | ||
| 298 | free(c); | ||
| 299 | |||
| 300 | c = next; | ||
| 301 | if (--c->nr_prev) { | ||
| 302 | goto top; | ||
| 303 | } | ||
| 304 | |||
| 305 | // We have become leaf | ||
| 306 | free(c->prev); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | void buffer_insert_bytes(View *view, const char *buf, const size_t len) | ||
| 311 | { | ||
| 312 | view_reset_preferred_x(view); | ||
| 313 | if (len == 0) { | ||
| 314 | return; | ||
| 315 | } | ||
| 316 | |||
| 317 | size_t rec_len = len; | ||
| 318 | if (buf[len - 1] != '\n' && block_iter_is_eof(&view->cursor)) { | ||
| 319 | // Force newline at EOF | ||
| 320 | do_insert(view, "\n", 1); | ||
| 321 | rec_len++; | ||
| 322 | } | ||
| 323 | |||
| 324 | do_insert(view, buf, len); | ||
| 325 | record_insert(view, rec_len); | ||
| 326 | |||
| 327 | if (view->buffer->views.count > 1) { | ||
| 328 | fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0); | ||
| 329 | } | ||
| 330 | } | ||
| 331 | |||
| 332 | static bool would_delete_last_bytes(const View *view, size_t count) | ||
| 333 | { | ||
| 334 | const Block *blk = view->cursor.blk; | ||
| 335 | size_t offset = view->cursor.offset; | ||
| 336 | while (1) { | ||
| 337 | size_t avail = blk->size - offset; | ||
| 338 | if (avail > count) { | ||
| 339 | return false; | ||
| 340 | } | ||
| 341 | |||
| 342 | if (blk->node.next == view->cursor.head) { | ||
| 343 | return true; | ||
| 344 | } | ||
| 345 | |||
| 346 | count -= avail; | ||
| 347 | blk = BLOCK(blk->node.next); | ||
| 348 | offset = 0; | ||
| 349 | } | ||
| 350 | } | ||
| 351 | |||
| 352 | static void buffer_delete_bytes_internal(View *view, size_t len, bool move_after) | ||
| 353 | { | ||
| 354 | view_reset_preferred_x(view); | ||
| 355 | if (len == 0) { | ||
| 356 | return; | ||
| 357 | } | ||
| 358 | |||
| 359 | // Check if all newlines from EOF would be deleted | ||
| 360 | if (would_delete_last_bytes(view, len)) { | ||
| 361 | BlockIter bi = view->cursor; | ||
| 362 | CodePoint u; | ||
| 363 | if (block_iter_prev_char(&bi, &u) && u != '\n') { | ||
| 364 | // No newline before cursor | ||
| 365 | if (--len == 0) { | ||
| 366 | begin_change(CHANGE_MERGE_NONE); | ||
| 367 | return; | ||
| 368 | } | ||
| 369 | } | ||
| 370 | } | ||
| 371 | record_delete(view, do_delete(view, len, true), len, move_after); | ||
| 372 | |||
| 373 | if (view->buffer->views.count > 1) { | ||
| 374 | fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0); | ||
| 375 | } | ||
| 376 | } | ||
| 377 | |||
| 378 | void buffer_delete_bytes(View *view, size_t len) | ||
| 379 | { | ||
| 380 | buffer_delete_bytes_internal(view, len, false); | ||
| 381 | } | ||
| 382 | |||
| 383 | void buffer_erase_bytes(View *view, size_t len) | ||
| 384 | { | ||
| 385 | buffer_delete_bytes_internal(view, len, true); | ||
| 386 | } | ||
| 387 | |||
| 388 | void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count) | ||
| 389 | { | ||
| 390 | view_reset_preferred_x(view); | ||
| 391 | if (del_count == 0) { | ||
| 392 | buffer_insert_bytes(view, ins, ins_count); | ||
| 393 | return; | ||
| 394 | } | ||
| 395 | if (ins_count == 0) { | ||
| 396 | buffer_delete_bytes(view, del_count); | ||
| 397 | return; | ||
| 398 | } | ||
| 399 | |||
| 400 | // Check if all newlines from EOF would be deleted | ||
| 401 | if (would_delete_last_bytes(view, del_count)) { | ||
| 402 | if (ins[ins_count - 1] != '\n') { | ||
| 403 | // Don't replace last newline | ||
| 404 | if (--del_count == 0) { | ||
| 405 | buffer_insert_bytes(view, ins, ins_count); | ||
| 406 | return; | ||
| 407 | } | ||
| 408 | } | ||
| 409 | } | ||
| 410 | |||
| 411 | char *deleted = do_replace(view, del_count, ins, ins_count); | ||
| 412 | record_replace(view, deleted, del_count, ins_count); | ||
| 413 | |||
| 414 | if (view->buffer->views.count > 1) { | ||
| 415 | fix_cursors(view, block_iter_get_offset(&view->cursor), del_count, ins_count); | ||
| 416 | } | ||
| 417 | } | ||
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 @@ | |||
| 1 | #ifndef CHANGE_H | ||
| 2 | #define CHANGE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | typedef enum { | ||
| 10 | CHANGE_MERGE_NONE, | ||
| 11 | CHANGE_MERGE_INSERT, | ||
| 12 | CHANGE_MERGE_DELETE, | ||
| 13 | CHANGE_MERGE_ERASE, | ||
| 14 | } ChangeMergeEnum; | ||
| 15 | |||
| 16 | typedef struct Change { | ||
| 17 | struct Change *next; | ||
| 18 | struct Change **prev; | ||
| 19 | unsigned long nr_prev; | ||
| 20 | bool move_after; // Move after inserted text when undoing delete? | ||
| 21 | size_t offset; | ||
| 22 | size_t del_count; | ||
| 23 | size_t ins_count; | ||
| 24 | char *buf; // Deleted bytes (inserted bytes need not be saved) | ||
| 25 | } Change; | ||
| 26 | |||
| 27 | void begin_change(ChangeMergeEnum m); | ||
| 28 | void end_change(void); | ||
| 29 | void begin_change_chain(void); | ||
| 30 | void end_change_chain(View *view) NONNULL_ARGS; | ||
| 31 | bool undo(View *view) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 32 | bool redo(View *view, unsigned long change_id) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 33 | void free_changes(Change *c) NONNULL_ARGS; | ||
| 34 | void buffer_insert_bytes(View *view, const char *buf, size_t len) NONNULL_ARG(1); | ||
| 35 | void buffer_delete_bytes(View *view, size_t len) NONNULL_ARGS; | ||
| 36 | void buffer_erase_bytes(View *view, size_t len) NONNULL_ARGS; | ||
| 37 | void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count) NONNULL_ARG(1); | ||
| 38 | |||
| 39 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "cmdline.h" | ||
| 4 | #include "command/args.h" | ||
| 5 | #include "command/macro.h" | ||
| 6 | #include "commands.h" | ||
| 7 | #include "completion.h" | ||
| 8 | #include "copy.h" | ||
| 9 | #include "editor.h" | ||
| 10 | #include "history.h" | ||
| 11 | #include "options.h" | ||
| 12 | #include "search.h" | ||
| 13 | #include "terminal/osc52.h" | ||
| 14 | #include "util/ascii.h" | ||
| 15 | #include "util/bsearch.h" | ||
| 16 | #include "util/debug.h" | ||
| 17 | #include "util/log.h" | ||
| 18 | #include "util/utf8.h" | ||
| 19 | |||
| 20 | static void cmdline_delete(CommandLine *c) | ||
| 21 | { | ||
| 22 | size_t pos = c->pos; | ||
| 23 | size_t len = 1; | ||
| 24 | |||
| 25 | if (pos == c->buf.len) { | ||
| 26 | return; | ||
| 27 | } | ||
| 28 | |||
| 29 | u_get_char(c->buf.buffer, c->buf.len, &pos); | ||
| 30 | len = pos - c->pos; | ||
| 31 | string_remove(&c->buf, c->pos, len); | ||
| 32 | } | ||
| 33 | |||
| 34 | void cmdline_clear(CommandLine *c) | ||
| 35 | { | ||
| 36 | string_clear(&c->buf); | ||
| 37 | c->pos = 0; | ||
| 38 | c->search_pos = NULL; | ||
| 39 | } | ||
| 40 | |||
| 41 | void cmdline_free(CommandLine *c) | ||
| 42 | { | ||
| 43 | cmdline_clear(c); | ||
| 44 | string_free(&c->buf); | ||
| 45 | free(c->search_text); | ||
| 46 | reset_completion(c); | ||
| 47 | } | ||
| 48 | |||
| 49 | static void set_text(CommandLine *c, const char *text) | ||
| 50 | { | ||
| 51 | string_clear(&c->buf); | ||
| 52 | const size_t text_len = strlen(text); | ||
| 53 | c->pos = text_len; | ||
| 54 | string_append_buf(&c->buf, text, text_len); | ||
| 55 | } | ||
| 56 | |||
| 57 | void cmdline_set_text(CommandLine *c, const char *text) | ||
| 58 | { | ||
| 59 | c->search_pos = NULL; | ||
| 60 | set_text(c, text); | ||
| 61 | } | ||
| 62 | |||
| 63 | static bool cmd_bol(EditorState *e, const CommandArgs *a) | ||
| 64 | { | ||
| 65 | BUG_ON(a->nr_args); | ||
| 66 | e->cmdline.pos = 0; | ||
| 67 | reset_completion(&e->cmdline); | ||
| 68 | return true; | ||
| 69 | } | ||
| 70 | |||
| 71 | static bool cmd_cancel(EditorState *e, const CommandArgs *a) | ||
| 72 | { | ||
| 73 | BUG_ON(a->nr_args); | ||
| 74 | CommandLine *c = &e->cmdline; | ||
| 75 | cmdline_clear(c); | ||
| 76 | set_input_mode(e, INPUT_NORMAL); | ||
| 77 | reset_completion(c); | ||
| 78 | return true; | ||
| 79 | } | ||
| 80 | |||
| 81 | static bool cmd_clear(EditorState *e, const CommandArgs *a) | ||
| 82 | { | ||
| 83 | BUG_ON(a->nr_args); | ||
| 84 | cmdline_clear(&e->cmdline); | ||
| 85 | return true; | ||
| 86 | } | ||
| 87 | |||
| 88 | static bool cmd_copy(EditorState *e, const CommandArgs *a) | ||
| 89 | { | ||
| 90 | bool internal = cmdargs_has_flag(a, 'i') || a->flag_set == 0; | ||
| 91 | bool clipboard = cmdargs_has_flag(a, 'b'); | ||
| 92 | bool primary = cmdargs_has_flag(a, 'p'); | ||
| 93 | |||
| 94 | String *buf = &e->cmdline.buf; | ||
| 95 | size_t len = buf->len; | ||
| 96 | if (internal) { | ||
| 97 | char *str = string_clone_cstring(buf); | ||
| 98 | record_copy(&e->clipboard, str, len, false); | ||
| 99 | } | ||
| 100 | |||
| 101 | Terminal *term = &e->terminal; | ||
| 102 | if ((clipboard || primary) && term->features & TFLAG_OSC52_COPY) { | ||
| 103 | const char *str = string_borrow_cstring(buf); | ||
| 104 | if (!term_osc52_copy(&term->obuf, str, len, clipboard, primary)) { | ||
| 105 | LOG_ERRNO("term_osc52_copy"); | ||
| 106 | // TODO: return false ? | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | return true; | ||
| 111 | } | ||
| 112 | |||
| 113 | static bool cmd_delete(EditorState *e, const CommandArgs *a) | ||
| 114 | { | ||
| 115 | BUG_ON(a->nr_args); | ||
| 116 | CommandLine *c = &e->cmdline; | ||
| 117 | cmdline_delete(c); | ||
| 118 | c->search_pos = NULL; | ||
| 119 | reset_completion(c); | ||
| 120 | return true; | ||
| 121 | } | ||
| 122 | |||
| 123 | static bool cmd_delete_eol(EditorState *e, const CommandArgs *a) | ||
| 124 | { | ||
| 125 | BUG_ON(a->nr_args); | ||
| 126 | CommandLine *c = &e->cmdline; | ||
| 127 | c->buf.len = c->pos; | ||
| 128 | c->search_pos = NULL; | ||
| 129 | reset_completion(c); | ||
| 130 | return true; | ||
| 131 | } | ||
| 132 | |||
| 133 | static bool cmd_delete_word(EditorState *e, const CommandArgs *a) | ||
| 134 | { | ||
| 135 | BUG_ON(a->nr_args); | ||
| 136 | CommandLine *c = &e->cmdline; | ||
| 137 | const unsigned char *buf = c->buf.buffer; | ||
| 138 | const size_t len = c->buf.len; | ||
| 139 | size_t i = c->pos; | ||
| 140 | |||
| 141 | if (i == len) { | ||
| 142 | return true; | ||
| 143 | } | ||
| 144 | |||
| 145 | while (i < len && is_word_byte(buf[i])) { | ||
| 146 | i++; | ||
| 147 | } | ||
| 148 | |||
| 149 | while (i < len && !is_word_byte(buf[i])) { | ||
| 150 | i++; | ||
| 151 | } | ||
| 152 | |||
| 153 | string_remove(&c->buf, c->pos, i - c->pos); | ||
| 154 | |||
| 155 | c->search_pos = NULL; | ||
| 156 | reset_completion(c); | ||
| 157 | return true; | ||
| 158 | } | ||
| 159 | |||
| 160 | static bool cmd_eol(EditorState *e, const CommandArgs *a) | ||
| 161 | { | ||
| 162 | BUG_ON(a->nr_args); | ||
| 163 | CommandLine *c = &e->cmdline; | ||
| 164 | c->pos = c->buf.len; | ||
| 165 | reset_completion(c); | ||
| 166 | return true; | ||
| 167 | } | ||
| 168 | |||
| 169 | static bool cmd_erase(EditorState *e, const CommandArgs *a) | ||
| 170 | { | ||
| 171 | BUG_ON(a->nr_args); | ||
| 172 | CommandLine *c = &e->cmdline; | ||
| 173 | if (c->pos > 0) { | ||
| 174 | u_prev_char(c->buf.buffer, &c->pos); | ||
| 175 | cmdline_delete(c); | ||
| 176 | } | ||
| 177 | c->search_pos = NULL; | ||
| 178 | reset_completion(c); | ||
| 179 | return true; | ||
| 180 | } | ||
| 181 | |||
| 182 | static bool cmd_erase_bol(EditorState *e, const CommandArgs *a) | ||
| 183 | { | ||
| 184 | BUG_ON(a->nr_args); | ||
| 185 | CommandLine *c = &e->cmdline; | ||
| 186 | string_remove(&c->buf, 0, c->pos); | ||
| 187 | c->pos = 0; | ||
| 188 | c->search_pos = NULL; | ||
| 189 | reset_completion(c); | ||
| 190 | return true; | ||
| 191 | } | ||
| 192 | |||
| 193 | static bool cmd_erase_word(EditorState *e, const CommandArgs *a) | ||
| 194 | { | ||
| 195 | BUG_ON(a->nr_args); | ||
| 196 | CommandLine *c = &e->cmdline; | ||
| 197 | size_t i = c->pos; | ||
| 198 | if (i == 0) { | ||
| 199 | return true; | ||
| 200 | } | ||
| 201 | |||
| 202 | // open /path/to/file^W => open /path/to/ | ||
| 203 | |||
| 204 | // erase whitespace | ||
| 205 | while (i && ascii_isspace(c->buf.buffer[i - 1])) { | ||
| 206 | i--; | ||
| 207 | } | ||
| 208 | |||
| 209 | // erase non-word bytes | ||
| 210 | while (i && !is_word_byte(c->buf.buffer[i - 1])) { | ||
| 211 | i--; | ||
| 212 | } | ||
| 213 | |||
| 214 | // erase word bytes | ||
| 215 | while (i && is_word_byte(c->buf.buffer[i - 1])) { | ||
| 216 | i--; | ||
| 217 | } | ||
| 218 | |||
| 219 | string_remove(&c->buf, i, c->pos - i); | ||
| 220 | c->pos = i; | ||
| 221 | c->search_pos = NULL; | ||
| 222 | reset_completion(c); | ||
| 223 | return true; | ||
| 224 | } | ||
| 225 | |||
| 226 | static bool do_history_prev(const History *hist, CommandLine *c) | ||
| 227 | { | ||
| 228 | if (!c->search_pos) { | ||
| 229 | free(c->search_text); | ||
| 230 | c->search_text = string_clone_cstring(&c->buf); | ||
| 231 | } | ||
| 232 | |||
| 233 | if (history_search_forward(hist, &c->search_pos, c->search_text)) { | ||
| 234 | BUG_ON(!c->search_pos); | ||
| 235 | set_text(c, c->search_pos->text); | ||
| 236 | } | ||
| 237 | |||
| 238 | reset_completion(c); | ||
| 239 | return true; | ||
| 240 | } | ||
| 241 | |||
| 242 | static bool do_history_next(const History *hist, CommandLine *c) | ||
| 243 | { | ||
| 244 | if (!c->search_pos) { | ||
| 245 | goto out; | ||
| 246 | } | ||
| 247 | |||
| 248 | if (history_search_backward(hist, &c->search_pos, c->search_text)) { | ||
| 249 | BUG_ON(!c->search_pos); | ||
| 250 | set_text(c, c->search_pos->text); | ||
| 251 | } else { | ||
| 252 | set_text(c, c->search_text); | ||
| 253 | c->search_pos = NULL; | ||
| 254 | } | ||
| 255 | |||
| 256 | out: | ||
| 257 | reset_completion(c); | ||
| 258 | return true; | ||
| 259 | } | ||
| 260 | |||
| 261 | static bool cmd_search_history_next(EditorState *e, const CommandArgs *a) | ||
| 262 | { | ||
| 263 | BUG_ON(a->nr_args); | ||
| 264 | return do_history_next(&e->search_history, &e->cmdline); | ||
| 265 | } | ||
| 266 | |||
| 267 | static bool cmd_search_history_prev(EditorState *e, const CommandArgs *a) | ||
| 268 | { | ||
| 269 | BUG_ON(a->nr_args); | ||
| 270 | return do_history_prev(&e->search_history, &e->cmdline); | ||
| 271 | } | ||
| 272 | |||
| 273 | static bool cmd_command_history_next(EditorState *e, const CommandArgs *a) | ||
| 274 | { | ||
| 275 | BUG_ON(a->nr_args); | ||
| 276 | return do_history_next(&e->command_history, &e->cmdline); | ||
| 277 | } | ||
| 278 | |||
| 279 | static bool cmd_command_history_prev(EditorState *e, const CommandArgs *a) | ||
| 280 | { | ||
| 281 | BUG_ON(a->nr_args); | ||
| 282 | return do_history_prev(&e->command_history, &e->cmdline); | ||
| 283 | } | ||
| 284 | |||
| 285 | static bool cmd_left(EditorState *e, const CommandArgs *a) | ||
| 286 | { | ||
| 287 | BUG_ON(a->nr_args); | ||
| 288 | CommandLine *c = &e->cmdline; | ||
| 289 | if (c->pos) { | ||
| 290 | u_prev_char(c->buf.buffer, &c->pos); | ||
| 291 | } | ||
| 292 | reset_completion(c); | ||
| 293 | return true; | ||
| 294 | } | ||
| 295 | |||
| 296 | static bool cmd_paste(EditorState *e, const CommandArgs *a) | ||
| 297 | { | ||
| 298 | CommandLine *c = &e->cmdline; | ||
| 299 | const Clipboard *clip = &e->clipboard; | ||
| 300 | string_insert_buf(&c->buf, c->pos, clip->buf, clip->len); | ||
| 301 | if (cmdargs_has_flag(a, 'm')) { | ||
| 302 | c->pos += clip->len; | ||
| 303 | } | ||
| 304 | c->search_pos = NULL; | ||
| 305 | reset_completion(c); | ||
| 306 | return true; | ||
| 307 | } | ||
| 308 | |||
| 309 | static bool cmd_right(EditorState *e, const CommandArgs *a) | ||
| 310 | { | ||
| 311 | BUG_ON(a->nr_args); | ||
| 312 | CommandLine *c = &e->cmdline; | ||
| 313 | if (c->pos < c->buf.len) { | ||
| 314 | u_get_char(c->buf.buffer, c->buf.len, &c->pos); | ||
| 315 | } | ||
| 316 | reset_completion(c); | ||
| 317 | return true; | ||
| 318 | } | ||
| 319 | |||
| 320 | static bool cmd_toggle(EditorState *e, const CommandArgs *a) | ||
| 321 | { | ||
| 322 | const char *option_name = a->args[0]; | ||
| 323 | bool global = cmdargs_has_flag(a, 'g'); | ||
| 324 | size_t nr_values = a->nr_args - 1; | ||
| 325 | if (nr_values == 0) { | ||
| 326 | return toggle_option(e, option_name, global, false); | ||
| 327 | } | ||
| 328 | |||
| 329 | char **values = a->args + 1; | ||
| 330 | return toggle_option_values(e, option_name, global, false, values, nr_values); | ||
| 331 | } | ||
| 332 | |||
| 333 | static bool cmd_word_bwd(EditorState *e, const CommandArgs *a) | ||
| 334 | { | ||
| 335 | BUG_ON(a->nr_args); | ||
| 336 | CommandLine *c = &e->cmdline; | ||
| 337 | if (c->pos <= 1) { | ||
| 338 | c->pos = 0; | ||
| 339 | return true; | ||
| 340 | } | ||
| 341 | |||
| 342 | const unsigned char *const buf = c->buf.buffer; | ||
| 343 | size_t i = c->pos - 1; | ||
| 344 | |||
| 345 | while (i > 0 && !is_word_byte(buf[i])) { | ||
| 346 | i--; | ||
| 347 | } | ||
| 348 | |||
| 349 | while (i > 0 && is_word_byte(buf[i])) { | ||
| 350 | i--; | ||
| 351 | } | ||
| 352 | |||
| 353 | if (i > 0) { | ||
| 354 | i++; | ||
| 355 | } | ||
| 356 | |||
| 357 | c->pos = i; | ||
| 358 | reset_completion(c); | ||
| 359 | return true; | ||
| 360 | } | ||
| 361 | |||
| 362 | static bool cmd_word_fwd(EditorState *e, const CommandArgs *a) | ||
| 363 | { | ||
| 364 | BUG_ON(a->nr_args); | ||
| 365 | CommandLine *c = &e->cmdline; | ||
| 366 | const unsigned char *buf = c->buf.buffer; | ||
| 367 | const size_t len = c->buf.len; | ||
| 368 | size_t i = c->pos; | ||
| 369 | |||
| 370 | while (i < len && is_word_byte(buf[i])) { | ||
| 371 | i++; | ||
| 372 | } | ||
| 373 | |||
| 374 | while (i < len && !is_word_byte(buf[i])) { | ||
| 375 | i++; | ||
| 376 | } | ||
| 377 | |||
| 378 | c->pos = i; | ||
| 379 | reset_completion(c); | ||
| 380 | return true; | ||
| 381 | } | ||
| 382 | |||
| 383 | static bool cmd_complete_next(EditorState *e, const CommandArgs *a) | ||
| 384 | { | ||
| 385 | BUG_ON(a->nr_args); | ||
| 386 | complete_command_next(e); | ||
| 387 | return true; | ||
| 388 | } | ||
| 389 | |||
| 390 | static bool cmd_complete_prev(EditorState *e, const CommandArgs *a) | ||
| 391 | { | ||
| 392 | BUG_ON(a->nr_args); | ||
| 393 | complete_command_prev(e); | ||
| 394 | return true; | ||
| 395 | } | ||
| 396 | |||
| 397 | static bool cmd_direction(EditorState *e, const CommandArgs *a) | ||
| 398 | { | ||
| 399 | BUG_ON(a->nr_args); | ||
| 400 | toggle_search_direction(&e->search); | ||
| 401 | return true; | ||
| 402 | } | ||
| 403 | |||
| 404 | static bool cmd_command_mode_accept(EditorState *e, const CommandArgs *a) | ||
| 405 | { | ||
| 406 | BUG_ON(a->nr_args); | ||
| 407 | CommandLine *c = &e->cmdline; | ||
| 408 | reset_completion(c); | ||
| 409 | set_input_mode(e, INPUT_NORMAL); | ||
| 410 | |||
| 411 | const char *str = string_borrow_cstring(&c->buf); | ||
| 412 | cmdline_clear(c); | ||
| 413 | if (!cmdargs_has_flag(a, 'H') && str[0] != ' ') { | ||
| 414 | // This is done before handle_command() because "command [text]" | ||
| 415 | // can modify the contents of the command-line | ||
| 416 | history_add(&e->command_history, str); | ||
| 417 | } | ||
| 418 | |||
| 419 | current_command = NULL; | ||
| 420 | return handle_normal_command(e, str, true); | ||
| 421 | } | ||
| 422 | |||
| 423 | static bool cmd_search_mode_accept(EditorState *e, const CommandArgs *a) | ||
| 424 | { | ||
| 425 | CommandLine *c = &e->cmdline; | ||
| 426 | if (cmdargs_has_flag(a, 'e')) { | ||
| 427 | if (c->buf.len == 0) { | ||
| 428 | return true; | ||
| 429 | } | ||
| 430 | // Escape the regex; to match as plain text | ||
| 431 | char *original = string_clone_cstring(&c->buf); | ||
| 432 | size_t len = c->buf.len; | ||
| 433 | string_clear(&c->buf); | ||
| 434 | for (size_t i = 0; i < len; i++) { | ||
| 435 | char ch = original[i]; | ||
| 436 | if (is_regex_special_char(ch)) { | ||
| 437 | string_append_byte(&c->buf, '\\'); | ||
| 438 | } | ||
| 439 | string_append_byte(&c->buf, ch); | ||
| 440 | } | ||
| 441 | free(original); | ||
| 442 | } | ||
| 443 | |||
| 444 | const char *str = NULL; | ||
| 445 | bool add_to_history = !cmdargs_has_flag(a, 'H'); | ||
| 446 | if (c->buf.len > 0) { | ||
| 447 | str = string_borrow_cstring(&c->buf); | ||
| 448 | BUG_ON(!str); | ||
| 449 | search_set_regexp(&e->search, str); | ||
| 450 | if (add_to_history) { | ||
| 451 | history_add(&e->search_history, str); | ||
| 452 | } | ||
| 453 | } | ||
| 454 | |||
| 455 | if (e->macro.recording) { | ||
| 456 | const char *args[5]; | ||
| 457 | size_t i = 0; | ||
| 458 | if (str) { | ||
| 459 | if (e->search.reverse) { | ||
| 460 | args[i++] = "-r"; | ||
| 461 | } | ||
| 462 | if (!add_to_history) { | ||
| 463 | args[i++] = "-H"; | ||
| 464 | } | ||
| 465 | if (unlikely(str[0] == '-')) { | ||
| 466 | args[i++] = "--"; | ||
| 467 | } | ||
| 468 | args[i++] = str; | ||
| 469 | } else { | ||
| 470 | args[i++] = e->search.reverse ? "-p" : "-n"; | ||
| 471 | } | ||
| 472 | args[i] = NULL; | ||
| 473 | macro_command_hook(&e->macro, "search", (char**)args); | ||
| 474 | } | ||
| 475 | |||
| 476 | current_command = NULL; | ||
| 477 | bool found = search_next(e->view, &e->search, e->options.case_sensitive_search); | ||
| 478 | cmdline_clear(c); | ||
| 479 | set_input_mode(e, INPUT_NORMAL); | ||
| 480 | return found; | ||
| 481 | } | ||
| 482 | |||
| 483 | IGNORE_WARNING("-Wincompatible-pointer-types") | ||
| 484 | |||
| 485 | static const Command common_cmds[] = { | ||
| 486 | {"bol", "", false, 0, 0, cmd_bol}, | ||
| 487 | {"cancel", "", false, 0, 0, cmd_cancel}, | ||
| 488 | {"clear", "", false, 0, 0, cmd_clear}, | ||
| 489 | {"copy", "bip", false, 0, 0, cmd_copy}, | ||
| 490 | {"delete", "", false, 0, 0, cmd_delete}, | ||
| 491 | {"delete-eol", "", false, 0, 0, cmd_delete_eol}, | ||
| 492 | {"delete-word", "", false, 0, 0, cmd_delete_word}, | ||
| 493 | {"eol", "", false, 0, 0, cmd_eol}, | ||
| 494 | {"erase", "", false, 0, 0, cmd_erase}, | ||
| 495 | {"erase-bol", "", false, 0, 0, cmd_erase_bol}, | ||
| 496 | {"erase-word", "", false, 0, 0, cmd_erase_word}, | ||
| 497 | {"left", "", false, 0, 0, cmd_left}, | ||
| 498 | {"paste", "m", false, 0, 0, cmd_paste}, | ||
| 499 | {"right", "", false, 0, 0, cmd_right}, | ||
| 500 | {"toggle", "g", false, 1, -1, cmd_toggle}, | ||
| 501 | {"word-bwd", "", false, 0, 0, cmd_word_bwd}, | ||
| 502 | {"word-fwd", "", false, 0, 0, cmd_word_fwd}, | ||
| 503 | }; | ||
| 504 | |||
| 505 | static const Command search_cmds[] = { | ||
| 506 | {"accept", "eH", false, 0, 0, cmd_search_mode_accept}, | ||
| 507 | {"direction", "", false, 0, 0, cmd_direction}, | ||
| 508 | {"history-next", "", false, 0, 0, cmd_search_history_next}, | ||
| 509 | {"history-prev", "", false, 0, 0, cmd_search_history_prev}, | ||
| 510 | }; | ||
| 511 | |||
| 512 | static const Command command_cmds[] = { | ||
| 513 | {"accept", "H", false, 0, 0, cmd_command_mode_accept}, | ||
| 514 | {"complete-next", "", false, 0, 0, cmd_complete_next}, | ||
| 515 | {"complete-prev", "", false, 0, 0, cmd_complete_prev}, | ||
| 516 | {"history-next", "", false, 0, 0, cmd_command_history_next}, | ||
| 517 | {"history-prev", "", false, 0, 0, cmd_command_history_prev}, | ||
| 518 | }; | ||
| 519 | |||
| 520 | UNIGNORE_WARNINGS | ||
| 521 | |||
| 522 | static const Command *find_cmd_mode_command(const char *name) | ||
| 523 | { | ||
| 524 | const Command *cmd = BSEARCH(name, common_cmds, command_cmp); | ||
| 525 | return cmd ? cmd : BSEARCH(name, command_cmds, command_cmp); | ||
| 526 | } | ||
| 527 | |||
| 528 | static const Command *find_search_mode_command(const char *name) | ||
| 529 | { | ||
| 530 | const Command *cmd = BSEARCH(name, common_cmds, command_cmp); | ||
| 531 | return cmd ? cmd : BSEARCH(name, search_cmds, command_cmp); | ||
| 532 | } | ||
| 533 | |||
| 534 | const CommandSet cmd_mode_commands = { | ||
| 535 | .lookup = find_cmd_mode_command | ||
| 536 | }; | ||
| 537 | |||
| 538 | const CommandSet search_mode_commands = { | ||
| 539 | .lookup = find_search_mode_command | ||
| 540 | }; | ||
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 @@ | |||
| 1 | #ifndef CMDLINE_H | ||
| 2 | #define CMDLINE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <sys/types.h> | ||
| 6 | #include "command/run.h" | ||
| 7 | #include "history.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/ptr-array.h" | ||
| 10 | #include "util/string-view.h" | ||
| 11 | #include "util/string.h" | ||
| 12 | |||
| 13 | typedef struct { | ||
| 14 | char *orig; // Full cmdline string (backing buffer for `escaped` and `tail`) | ||
| 15 | char *parsed; // Result of passing `escaped` through parse_command_arg() | ||
| 16 | StringView escaped; // Middle part of `orig` (string to be replaced) | ||
| 17 | StringView tail; // Suffix part of `orig` (after `escaped`) | ||
| 18 | size_t head_len; // Length of prefix part of `orig` (before `escaped`) | ||
| 19 | PointerArray completions; // Array of completion candidates | ||
| 20 | size_t idx; // Index of currently selected completion | ||
| 21 | bool add_space_after_single_match; | ||
| 22 | bool tilde_expanded; | ||
| 23 | } CompletionState; | ||
| 24 | |||
| 25 | typedef struct { | ||
| 26 | String buf; | ||
| 27 | size_t pos; | ||
| 28 | const HistoryEntry *search_pos; | ||
| 29 | char *search_text; | ||
| 30 | CompletionState completion; | ||
| 31 | } CommandLine; | ||
| 32 | |||
| 33 | extern const CommandSet cmd_mode_commands; | ||
| 34 | extern const CommandSet search_mode_commands; | ||
| 35 | |||
| 36 | void cmdline_set_text(CommandLine *c, const char *text) NONNULL_ARGS; | ||
| 37 | void cmdline_clear(CommandLine *c) NONNULL_ARGS; | ||
| 38 | void cmdline_free(CommandLine *c) NONNULL_ARGS; | ||
| 39 | |||
| 40 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <fcntl.h> | ||
| 3 | #include <glob.h> | ||
| 4 | #include <signal.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <sys/stat.h> | ||
| 8 | #include <unistd.h> | ||
| 9 | #include "commands.h" | ||
| 10 | #include "bind.h" | ||
| 11 | #include "bookmark.h" | ||
| 12 | #include "buffer.h" | ||
| 13 | #include "change.h" | ||
| 14 | #include "cmdline.h" | ||
| 15 | #include "command/alias.h" | ||
| 16 | #include "command/args.h" | ||
| 17 | #include "command/macro.h" | ||
| 18 | #include "compiler.h" | ||
| 19 | #include "config.h" | ||
| 20 | #include "convert.h" | ||
| 21 | #include "copy.h" | ||
| 22 | #include "editor.h" | ||
| 23 | #include "encoding.h" | ||
| 24 | #include "error.h" | ||
| 25 | #include "exec.h" | ||
| 26 | #include "file-option.h" | ||
| 27 | #include "filetype.h" | ||
| 28 | #include "frame.h" | ||
| 29 | #include "history.h" | ||
| 30 | #include "load-save.h" | ||
| 31 | #include "lock.h" | ||
| 32 | #include "misc.h" | ||
| 33 | #include "move.h" | ||
| 34 | #include "msg.h" | ||
| 35 | #include "regexp.h" | ||
| 36 | #include "replace.h" | ||
| 37 | #include "screen.h" | ||
| 38 | #include "search.h" | ||
| 39 | #include "selection.h" | ||
| 40 | #include "shift.h" | ||
| 41 | #include "show.h" | ||
| 42 | #include "spawn.h" | ||
| 43 | #include "syntax/color.h" | ||
| 44 | #include "syntax/state.h" | ||
| 45 | #include "syntax/syntax.h" | ||
| 46 | #include "tag.h" | ||
| 47 | #include "terminal/cursor.h" | ||
| 48 | #include "terminal/mode.h" | ||
| 49 | #include "terminal/osc52.h" | ||
| 50 | #include "terminal/style.h" | ||
| 51 | #include "terminal/terminal.h" | ||
| 52 | #include "util/arith.h" | ||
| 53 | #include "util/array.h" | ||
| 54 | #include "util/ascii.h" | ||
| 55 | #include "util/bit.h" | ||
| 56 | #include "util/bsearch.h" | ||
| 57 | #include "util/debug.h" | ||
| 58 | #include "util/log.h" | ||
| 59 | #include "util/path.h" | ||
| 60 | #include "util/str-util.h" | ||
| 61 | #include "util/strtonum.h" | ||
| 62 | #include "util/time-util.h" | ||
| 63 | #include "util/xmalloc.h" | ||
| 64 | #include "util/xsnprintf.h" | ||
| 65 | #include "vars.h" | ||
| 66 | #include "view.h" | ||
| 67 | #include "window.h" | ||
| 68 | |||
| 69 | NOINLINE | ||
| 70 | static void do_selection_noinline(View *view, SelectionType sel) | ||
| 71 | { | ||
| 72 | // Should only be called from do_selection() | ||
| 73 | BUG_ON(sel == view->selection); | ||
| 74 | |||
| 75 | if (sel == SELECT_NONE) { | ||
| 76 | unselect(view); | ||
| 77 | return; | ||
| 78 | } | ||
| 79 | |||
| 80 | if (view->selection) { | ||
| 81 | if (view->selection != sel) { | ||
| 82 | view->selection = sel; | ||
| 83 | // TODO: be less brute force about this; only the first/last | ||
| 84 | // line of the selection can change in this case | ||
| 85 | mark_all_lines_changed(view->buffer); | ||
| 86 | } | ||
| 87 | return; | ||
| 88 | } | ||
| 89 | |||
| 90 | view->sel_so = block_iter_get_offset(&view->cursor); | ||
| 91 | view->sel_eo = SEL_EO_RECALC; | ||
| 92 | view->selection = sel; | ||
| 93 | |||
| 94 | // Need to mark current line changed because cursor might | ||
| 95 | // move up or down before screen is updated | ||
| 96 | view_update_cursor_y(view); | ||
| 97 | buffer_mark_lines_changed(view->buffer, view->cy, view->cy); | ||
| 98 | } | ||
| 99 | |||
| 100 | static void do_selection(View *view, SelectionType sel) | ||
| 101 | { | ||
| 102 | if (likely(sel == view->selection)) { | ||
| 103 | // If `sel` is SELECT_NONE here, it's always equal to select_mode | ||
| 104 | BUG_ON(!sel && view->select_mode); | ||
| 105 | return; | ||
| 106 | } | ||
| 107 | |||
| 108 | do_selection_noinline(view, sel); | ||
| 109 | } | ||
| 110 | |||
| 111 | static char last_flag_or_default(const CommandArgs *a, char def) | ||
| 112 | { | ||
| 113 | size_t n = a->nr_flags; | ||
| 114 | return n ? a->flags[n - 1] : def; | ||
| 115 | } | ||
| 116 | |||
| 117 | static char last_flag(const CommandArgs *a) | ||
| 118 | { | ||
| 119 | return last_flag_or_default(a, 0); | ||
| 120 | } | ||
| 121 | |||
| 122 | static bool has_flag(const CommandArgs *a, unsigned char flag) | ||
| 123 | { | ||
| 124 | return cmdargs_has_flag(a, flag); | ||
| 125 | } | ||
| 126 | |||
| 127 | static void handle_select_chars_or_lines_flags(View *view, const CommandArgs *a) | ||
| 128 | { | ||
| 129 | SelectionType sel; | ||
| 130 | if (has_flag(a, 'l')) { | ||
| 131 | sel = SELECT_LINES; | ||
| 132 | } else if (has_flag(a, 'c')) { | ||
| 133 | static_assert(SELECT_CHARS < SELECT_LINES); | ||
| 134 | sel = MAX(SELECT_CHARS, view->select_mode); | ||
| 135 | } else { | ||
| 136 | sel = view->select_mode; | ||
| 137 | } | ||
| 138 | do_selection(view, sel); | ||
| 139 | } | ||
| 140 | |||
| 141 | static void handle_select_chars_flag(View *view, const CommandArgs *a) | ||
| 142 | { | ||
| 143 | BUG_ON(has_flag(a, 'l')); | ||
| 144 | handle_select_chars_or_lines_flags(view, a); | ||
| 145 | } | ||
| 146 | |||
| 147 | static bool cmd_alias(EditorState *e, const CommandArgs *a) | ||
| 148 | { | ||
| 149 | const char *const name = a->args[0]; | ||
| 150 | const char *const cmd = a->args[1]; | ||
| 151 | |||
| 152 | if (unlikely(name[0] == '\0')) { | ||
| 153 | return error_msg("Empty alias name not allowed"); | ||
| 154 | } | ||
| 155 | if (unlikely(name[0] == '-')) { | ||
| 156 | // Disallowing this simplifies auto-completion for "alias " | ||
| 157 | return error_msg("Alias name cannot begin with '-'"); | ||
| 158 | } | ||
| 159 | |||
| 160 | for (size_t i = 0; name[i]; i++) { | ||
| 161 | unsigned char c = name[i]; | ||
| 162 | if (unlikely(!(is_word_byte(c) || c == '-' || c == '?' || c == '!'))) { | ||
| 163 | return error_msg("Invalid byte in alias name: %c (0x%02hhX)", c, c); | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | if (unlikely(find_normal_command(name))) { | ||
| 168 | return error_msg("Can't replace existing command %s with an alias", name); | ||
| 169 | } | ||
| 170 | |||
| 171 | if (likely(cmd)) { | ||
| 172 | add_alias(&e->aliases, name, cmd); | ||
| 173 | } else { | ||
| 174 | remove_alias(&e->aliases, name); | ||
| 175 | } | ||
| 176 | |||
| 177 | return true; | ||
| 178 | } | ||
| 179 | |||
| 180 | static bool cmd_bind(EditorState *e, const CommandArgs *a) | ||
| 181 | { | ||
| 182 | const char *keystr = a->args[0]; | ||
| 183 | const char *cmd = a->args[1]; | ||
| 184 | KeyCode key; | ||
| 185 | if (unlikely(!parse_key_string(&key, keystr))) { | ||
| 186 | return error_msg("invalid key string: %s", keystr); | ||
| 187 | } | ||
| 188 | |||
| 189 | const bool modes[] = { | ||
| 190 | [INPUT_NORMAL] = a->nr_flags == 0 || has_flag(a, 'n'), | ||
| 191 | [INPUT_COMMAND] = has_flag(a, 'c'), | ||
| 192 | [INPUT_SEARCH] = has_flag(a, 's'), | ||
| 193 | }; | ||
| 194 | |||
| 195 | static_assert(ARRAYLEN(modes) == ARRAYLEN(e->modes)); | ||
| 196 | |||
| 197 | for (InputMode i = 0; i < ARRAYLEN(modes); i++) { | ||
| 198 | if (!modes[i]) { | ||
| 199 | continue; | ||
| 200 | } | ||
| 201 | IntMap *bindings = &e->modes[i].key_bindings; | ||
| 202 | if (likely(cmd)) { | ||
| 203 | CommandRunner runner = cmdrunner_for_mode(e, i, false); | ||
| 204 | add_binding(bindings, key, cached_command_new(&runner, cmd)); | ||
| 205 | } else { | ||
| 206 | remove_binding(bindings, key); | ||
| 207 | } | ||
| 208 | } | ||
| 209 | |||
| 210 | return true; | ||
| 211 | } | ||
| 212 | |||
| 213 | static bool cmd_bof(EditorState *e, const CommandArgs *a) | ||
| 214 | { | ||
| 215 | handle_select_chars_or_lines_flags(e->view, a); | ||
| 216 | move_bof(e->view); | ||
| 217 | return true; | ||
| 218 | } | ||
| 219 | |||
| 220 | static bool cmd_bol(EditorState *e, const CommandArgs *a) | ||
| 221 | { | ||
| 222 | static const FlagMapping map[] = { | ||
| 223 | {'s', BOL_SMART}, | ||
| 224 | {'t', BOL_SMART | BOL_SMART_TOGGLE}, | ||
| 225 | }; | ||
| 226 | |||
| 227 | SmartBolFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)); | ||
| 228 | handle_select_chars_flag(e->view, a); | ||
| 229 | move_bol_smart(e->view, flags); | ||
| 230 | return true; | ||
| 231 | } | ||
| 232 | |||
| 233 | static bool cmd_bolsf(EditorState *e, const CommandArgs *a) | ||
| 234 | { | ||
| 235 | BUG_ON(a->nr_args); | ||
| 236 | View *view = e->view; | ||
| 237 | handle_select_chars_or_lines_flags(view, a); | ||
| 238 | |||
| 239 | if (!block_iter_bol(&view->cursor)) { | ||
| 240 | unsigned int margin = e->options.scroll_margin; | ||
| 241 | long top = view->vy + window_get_scroll_margin(e->window, margin); | ||
| 242 | if (view->cy > top) { | ||
| 243 | move_up(view, view->cy - top); | ||
| 244 | } else { | ||
| 245 | block_iter_bof(&view->cursor); | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | view_reset_preferred_x(view); | ||
| 250 | return true; | ||
| 251 | } | ||
| 252 | |||
| 253 | static bool cmd_bookmark(EditorState *e, const CommandArgs *a) | ||
| 254 | { | ||
| 255 | if (has_flag(a, 'r')) { | ||
| 256 | bookmark_pop(e->window, &e->bookmarks); | ||
| 257 | return true; | ||
| 258 | } | ||
| 259 | |||
| 260 | bookmark_push(&e->bookmarks, get_current_file_location(e->view)); | ||
| 261 | return true; | ||
| 262 | } | ||
| 263 | |||
| 264 | static bool cmd_case(EditorState *e, const CommandArgs *a) | ||
| 265 | { | ||
| 266 | change_case(e->view, last_flag_or_default(a, 't')); | ||
| 267 | return true; | ||
| 268 | } | ||
| 269 | |||
| 270 | static void mark_tabbar_changed(Window *window, void* UNUSED_ARG(data)) | ||
| 271 | { | ||
| 272 | window->update_tabbar = true; | ||
| 273 | } | ||
| 274 | |||
| 275 | static bool cmd_cd(EditorState *e, const CommandArgs *a) | ||
| 276 | { | ||
| 277 | const char *dir = a->args[0]; | ||
| 278 | if (unlikely(dir[0] == '\0')) { | ||
| 279 | return error_msg("directory argument cannot be empty"); | ||
| 280 | } | ||
| 281 | |||
| 282 | if (streq(dir, "-")) { | ||
| 283 | dir = xgetenv("OLDPWD"); | ||
| 284 | if (!dir) { | ||
| 285 | return error_msg("OLDPWD not set"); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | |||
| 289 | char buf[8192]; | ||
| 290 | const char *cwd = getcwd(buf, sizeof(buf)); | ||
| 291 | if (chdir(dir) != 0) { | ||
| 292 | return error_msg_errno("changing directory failed"); | ||
| 293 | } | ||
| 294 | |||
| 295 | if (likely(cwd)) { | ||
| 296 | int r = setenv("OLDPWD", cwd, 1); | ||
| 297 | if (unlikely(r != 0)) { | ||
| 298 | LOG_WARNING("failed to set OLDPWD: %s", strerror(errno)); | ||
| 299 | } | ||
| 300 | } | ||
| 301 | |||
| 302 | cwd = getcwd(buf, sizeof(buf)); | ||
| 303 | if (likely(cwd)) { | ||
| 304 | int r = setenv("PWD", cwd, 1); | ||
| 305 | if (unlikely(r != 0)) { | ||
| 306 | LOG_WARNING("failed to set PWD: %s", strerror(errno)); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | for (size_t i = 0, n = e->buffers.count; i < n; i++) { | ||
| 311 | Buffer *buffer = e->buffers.ptrs[i]; | ||
| 312 | update_short_filename_cwd(buffer, &e->home_dir, cwd); | ||
| 313 | } | ||
| 314 | |||
| 315 | frame_for_each_window(e->root_frame, mark_tabbar_changed, NULL); | ||
| 316 | return true; | ||
| 317 | } | ||
| 318 | |||
| 319 | static bool cmd_center_view(EditorState *e, const CommandArgs *a) | ||
| 320 | { | ||
| 321 | BUG_ON(a->nr_args); | ||
| 322 | e->view->force_center = true; | ||
| 323 | return true; | ||
| 324 | } | ||
| 325 | |||
| 326 | static bool cmd_clear(EditorState *e, const CommandArgs *a) | ||
| 327 | { | ||
| 328 | bool auto_indent = e->buffer->options.auto_indent && !has_flag(a, 'i'); | ||
| 329 | clear_lines(e->view, auto_indent); | ||
| 330 | return true; | ||
| 331 | } | ||
| 332 | |||
| 333 | static bool cmd_close(EditorState *e, const CommandArgs *a) | ||
| 334 | { | ||
| 335 | bool force = has_flag(a, 'f'); | ||
| 336 | if (!force && !view_can_close(e->view)) { | ||
| 337 | bool prompt = has_flag(a, 'p'); | ||
| 338 | if (!prompt) { | ||
| 339 | return error_msg ( | ||
| 340 | "The buffer is modified; " | ||
| 341 | "save or run 'close -f' to close without saving" | ||
| 342 | ); | ||
| 343 | } | ||
| 344 | static const char str[] = "Close without saving changes? [y/N]"; | ||
| 345 | if (dialog_prompt(e, str, "ny") != 'y') { | ||
| 346 | return false; | ||
| 347 | } | ||
| 348 | } | ||
| 349 | |||
| 350 | bool allow_quit = has_flag(a, 'q'); | ||
| 351 | if (allow_quit && e->buffers.count == 1 && e->root_frame->frames.count <= 1) { | ||
| 352 | e->status = EDITOR_EXIT_OK; | ||
| 353 | return true; | ||
| 354 | } | ||
| 355 | |||
| 356 | bool allow_wclose = has_flag(a, 'w'); | ||
| 357 | if (allow_wclose && e->window->views.count <= 1) { | ||
| 358 | window_close(e->window); | ||
| 359 | return true; | ||
| 360 | } | ||
| 361 | |||
| 362 | window_close_current_view(e->window); | ||
| 363 | set_view(e->window->view); | ||
| 364 | return true; | ||
| 365 | } | ||
| 366 | |||
| 367 | static bool cmd_command(EditorState *e, const CommandArgs *a) | ||
| 368 | { | ||
| 369 | const char *text = a->args[0]; | ||
| 370 | set_input_mode(e, INPUT_COMMAND); | ||
| 371 | if (text) { | ||
| 372 | cmdline_set_text(&e->cmdline, text); | ||
| 373 | } | ||
| 374 | return true; | ||
| 375 | } | ||
| 376 | |||
| 377 | static bool cmd_compile(EditorState *e, const CommandArgs *a) | ||
| 378 | { | ||
| 379 | static const FlagMapping map[] = { | ||
| 380 | {'1', SPAWN_READ_STDOUT}, | ||
| 381 | {'p', SPAWN_PROMPT}, | ||
| 382 | {'s', SPAWN_QUIET}, | ||
| 383 | }; | ||
| 384 | |||
| 385 | Compiler *c = find_compiler(&e->compilers, a->args[0]); | ||
| 386 | if (unlikely(!c)) { | ||
| 387 | return error_msg("No such error parser %s", a->args[0]); | ||
| 388 | } | ||
| 389 | |||
| 390 | SpawnContext ctx = { | ||
| 391 | .editor = e, | ||
| 392 | .argv = (const char **)a->args + 1, | ||
| 393 | .flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)), | ||
| 394 | }; | ||
| 395 | |||
| 396 | clear_messages(&e->messages); | ||
| 397 | bool ok = spawn_compiler(&ctx, c, &e->messages); | ||
| 398 | if (e->messages.array.count) { | ||
| 399 | activate_current_message_save(e); | ||
| 400 | } | ||
| 401 | return ok; | ||
| 402 | } | ||
| 403 | |||
| 404 | static bool cmd_copy(EditorState *e, const CommandArgs *a) | ||
| 405 | { | ||
| 406 | View *view = e->view; | ||
| 407 | const BlockIter save = view->cursor; | ||
| 408 | size_t size; | ||
| 409 | bool line_copy; | ||
| 410 | if (view->selection) { | ||
| 411 | size = prepare_selection(view); | ||
| 412 | line_copy = (view->selection == SELECT_LINES); | ||
| 413 | } else { | ||
| 414 | block_iter_bol(&view->cursor); | ||
| 415 | BlockIter tmp = view->cursor; | ||
| 416 | size = block_iter_eat_line(&tmp); | ||
| 417 | line_copy = true; | ||
| 418 | } | ||
| 419 | |||
| 420 | if (unlikely(size == 0)) { | ||
| 421 | return true; | ||
| 422 | } | ||
| 423 | |||
| 424 | bool internal = has_flag(a, 'i'); | ||
| 425 | bool clipboard = has_flag(a, 'b'); | ||
| 426 | bool primary = has_flag(a, 'p'); | ||
| 427 | if (!(internal || clipboard || primary)) { | ||
| 428 | internal = true; | ||
| 429 | } | ||
| 430 | |||
| 431 | if (internal) { | ||
| 432 | copy(&e->clipboard, view, size, line_copy); | ||
| 433 | } | ||
| 434 | |||
| 435 | Terminal *term = &e->terminal; | ||
| 436 | if ((clipboard || primary) && term->features & TFLAG_OSC52_COPY) { | ||
| 437 | if (internal) { | ||
| 438 | view->cursor = save; | ||
| 439 | if (view->selection) { | ||
| 440 | size = prepare_selection(view); | ||
| 441 | } | ||
| 442 | } | ||
| 443 | char *buf = block_iter_get_bytes(&view->cursor, size); | ||
| 444 | if (!term_osc52_copy(&term->obuf, buf, size, clipboard, primary)) { | ||
| 445 | error_msg_errno("OSC 52 copy failed"); | ||
| 446 | } | ||
| 447 | free(buf); | ||
| 448 | } | ||
| 449 | |||
| 450 | if (!has_flag(a, 'k')) { | ||
| 451 | unselect(view); | ||
| 452 | } | ||
| 453 | |||
| 454 | view->cursor = save; | ||
| 455 | // TODO: return false if term_osc52_copy() failed? | ||
| 456 | return true; | ||
| 457 | } | ||
| 458 | |||
| 459 | static bool cmd_cursor(EditorState *e, const CommandArgs *a) | ||
| 460 | { | ||
| 461 | if (unlikely(a->nr_args == 0)) { | ||
| 462 | // Reset all cursor styles | ||
| 463 | for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) { | ||
| 464 | e->cursor_styles[m] = get_default_cursor_style(m); | ||
| 465 | } | ||
| 466 | e->cursor_style_changed = true; | ||
| 467 | return true; | ||
| 468 | } | ||
| 469 | |||
| 470 | CursorInputMode mode = cursor_mode_from_str(a->args[0]); | ||
| 471 | if (unlikely(mode >= NR_CURSOR_MODES)) { | ||
| 472 | return error_msg("invalid mode argument: %s", a->args[0]); | ||
| 473 | } | ||
| 474 | |||
| 475 | TermCursorStyle style = get_default_cursor_style(mode); | ||
| 476 | if (a->nr_args >= 2) { | ||
| 477 | style.type = cursor_type_from_str(a->args[1]); | ||
| 478 | if (unlikely(style.type == CURSOR_INVALID)) { | ||
| 479 | return error_msg("invalid cursor type: %s", a->args[1]); | ||
| 480 | } | ||
| 481 | } | ||
| 482 | |||
| 483 | if (a->nr_args >= 3) { | ||
| 484 | style.color = cursor_color_from_str(a->args[2]); | ||
| 485 | if (unlikely(style.color == COLOR_INVALID)) { | ||
| 486 | return error_msg("invalid cursor color: %s", a->args[2]); | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | e->cursor_styles[mode] = style; | ||
| 491 | e->cursor_style_changed = true; | ||
| 492 | return true; | ||
| 493 | } | ||
| 494 | |||
| 495 | static bool cmd_cut(EditorState *e, const CommandArgs *a) | ||
| 496 | { | ||
| 497 | BUG_ON(a->nr_args); | ||
| 498 | View *view = e->view; | ||
| 499 | const long x = view_get_preferred_x(view); | ||
| 500 | if (view->selection) { | ||
| 501 | bool is_lines = view->selection == SELECT_LINES; | ||
| 502 | cut(&e->clipboard, view, prepare_selection(view), is_lines); | ||
| 503 | if (view->selection == SELECT_LINES) { | ||
| 504 | move_to_preferred_x(view, x); | ||
| 505 | } | ||
| 506 | unselect(view); | ||
| 507 | } else { | ||
| 508 | BlockIter tmp; | ||
| 509 | block_iter_bol(&view->cursor); | ||
| 510 | tmp = view->cursor; | ||
| 511 | cut(&e->clipboard, view, block_iter_eat_line(&tmp), true); | ||
| 512 | move_to_preferred_x(view, x); | ||
| 513 | } | ||
| 514 | return true; | ||
| 515 | } | ||
| 516 | |||
| 517 | static bool cmd_delete(EditorState *e, const CommandArgs *a) | ||
| 518 | { | ||
| 519 | BUG_ON(a->nr_args); | ||
| 520 | delete_ch(e->view); | ||
| 521 | return true; | ||
| 522 | } | ||
| 523 | |||
| 524 | static bool cmd_delete_eol(EditorState *e, const CommandArgs *a) | ||
| 525 | { | ||
| 526 | View *view = e->view; | ||
| 527 | if (view->selection) { | ||
| 528 | // TODO: return false? | ||
| 529 | return true; | ||
| 530 | } | ||
| 531 | |||
| 532 | bool delete_newline_if_at_eol = has_flag(a, 'n'); | ||
| 533 | BlockIter bi = view->cursor; | ||
| 534 | if (delete_newline_if_at_eol) { | ||
| 535 | CodePoint ch; | ||
| 536 | if (block_iter_get_char(&view->cursor, &ch) == 1 && ch == '\n') { | ||
| 537 | delete_ch(view); | ||
| 538 | return true; | ||
| 539 | } | ||
| 540 | } | ||
| 541 | |||
| 542 | buffer_delete_bytes(view, block_iter_eol(&bi)); | ||
| 543 | return true; | ||
| 544 | } | ||
| 545 | |||
| 546 | static bool cmd_delete_line(EditorState *e, const CommandArgs *a) | ||
| 547 | { | ||
| 548 | BUG_ON(a->nr_args); | ||
| 549 | delete_lines(e->view); | ||
| 550 | return true; | ||
| 551 | } | ||
| 552 | |||
| 553 | static bool cmd_delete_word(EditorState *e, const CommandArgs *a) | ||
| 554 | { | ||
| 555 | bool skip_non_word = has_flag(a, 's'); | ||
| 556 | BlockIter bi = e->view->cursor; | ||
| 557 | buffer_delete_bytes(e->view, word_fwd(&bi, skip_non_word)); | ||
| 558 | return true; | ||
| 559 | } | ||
| 560 | |||
| 561 | static bool cmd_down(EditorState *e, const CommandArgs *a) | ||
| 562 | { | ||
| 563 | handle_select_chars_or_lines_flags(e->view, a); | ||
| 564 | move_down(e->view, 1); | ||
| 565 | return true; | ||
| 566 | } | ||
| 567 | |||
| 568 | static bool cmd_eof(EditorState *e, const CommandArgs *a) | ||
| 569 | { | ||
| 570 | handle_select_chars_or_lines_flags(e->view, a); | ||
| 571 | move_eof(e->view); | ||
| 572 | return true; | ||
| 573 | } | ||
| 574 | |||
| 575 | static bool cmd_eol(EditorState *e, const CommandArgs *a) | ||
| 576 | { | ||
| 577 | handle_select_chars_flag(e->view, a); | ||
| 578 | move_eol(e->view); | ||
| 579 | return true; | ||
| 580 | } | ||
| 581 | |||
| 582 | static bool cmd_eolsf(EditorState *e, const CommandArgs *a) | ||
| 583 | { | ||
| 584 | BUG_ON(a->nr_args); | ||
| 585 | View *view = e->view; | ||
| 586 | handle_select_chars_or_lines_flags(view, a); | ||
| 587 | |||
| 588 | if (!block_iter_eol(&view->cursor)) { | ||
| 589 | Window *window = e->window; | ||
| 590 | long margin = window_get_scroll_margin(window, e->options.scroll_margin); | ||
| 591 | long bottom = view->vy + window->edit_h - 1 - margin; | ||
| 592 | if (view->cy < bottom) { | ||
| 593 | move_down(view, bottom - view->cy); | ||
| 594 | } else { | ||
| 595 | block_iter_eof(&view->cursor); | ||
| 596 | } | ||
| 597 | } | ||
| 598 | |||
| 599 | view_reset_preferred_x(view); | ||
| 600 | return true; | ||
| 601 | } | ||
| 602 | |||
| 603 | static bool cmd_erase(EditorState *e, const CommandArgs *a) | ||
| 604 | { | ||
| 605 | BUG_ON(a->nr_args); | ||
| 606 | erase(e->view); | ||
| 607 | return true; | ||
| 608 | } | ||
| 609 | |||
| 610 | static bool cmd_erase_bol(EditorState *e, const CommandArgs *a) | ||
| 611 | { | ||
| 612 | BUG_ON(a->nr_args); | ||
| 613 | buffer_erase_bytes(e->view, block_iter_bol(&e->view->cursor)); | ||
| 614 | return true; | ||
| 615 | } | ||
| 616 | |||
| 617 | static bool cmd_erase_word(EditorState *e, const CommandArgs *a) | ||
| 618 | { | ||
| 619 | View *view = e->view; | ||
| 620 | bool skip_non_word = has_flag(a, 's'); | ||
| 621 | buffer_erase_bytes(view, word_bwd(&view->cursor, skip_non_word)); | ||
| 622 | return true; | ||
| 623 | } | ||
| 624 | |||
| 625 | static bool cmd_errorfmt(EditorState *e, const CommandArgs *a) | ||
| 626 | { | ||
| 627 | BUG_ON(a->nr_args == 0); | ||
| 628 | const char *name = a->args[0]; | ||
| 629 | if (a->nr_args == 1) { | ||
| 630 | remove_compiler(&e->compilers, name); | ||
| 631 | return true; | ||
| 632 | } | ||
| 633 | |||
| 634 | bool ignore = has_flag(a, 'i'); | ||
| 635 | return add_error_fmt(&e->compilers, name, ignore, a->args[1], a->args + 2); | ||
| 636 | } | ||
| 637 | |||
| 638 | static bool cmd_exec(EditorState *e, const CommandArgs *a) | ||
| 639 | { | ||
| 640 | ExecAction actions[3] = {EXEC_TTY, EXEC_TTY, EXEC_TTY}; | ||
| 641 | SpawnFlags spawn_flags = 0; | ||
| 642 | bool lflag = false; | ||
| 643 | bool move_after_insert = false; | ||
| 644 | bool strip_nl = false; | ||
| 645 | |||
| 646 | for (size_t i = 0, n = a->nr_flags, argidx = 0, fd; i < n; i++) { | ||
| 647 | switch (a->flags[i]) { | ||
| 648 | case 'e': fd = STDERR_FILENO; break; | ||
| 649 | case 'i': fd = STDIN_FILENO; break; | ||
| 650 | case 'o': fd = STDOUT_FILENO; break; | ||
| 651 | case 'p': spawn_flags |= SPAWN_PROMPT; continue; | ||
| 652 | case 's': spawn_flags |= SPAWN_QUIET; continue; | ||
| 653 | case 't': spawn_flags &= ~SPAWN_QUIET; continue; | ||
| 654 | case 'l': lflag = true; continue; | ||
| 655 | case 'm': move_after_insert = true; continue; | ||
| 656 | case 'n': strip_nl = true; continue; | ||
| 657 | default: BUG("unexpected flag"); return false; | ||
| 658 | } | ||
| 659 | const char *action_name = a->args[argidx++]; | ||
| 660 | ExecAction action = lookup_exec_action(action_name, fd); | ||
| 661 | if (unlikely(action == EXEC_INVALID)) { | ||
| 662 | return error_msg("invalid action for -%c: '%s'", a->flags[i], action_name); | ||
| 663 | } | ||
| 664 | actions[fd] = action; | ||
| 665 | } | ||
| 666 | |||
| 667 | if (lflag && actions[STDIN_FILENO] == EXEC_BUFFER) { | ||
| 668 | // For compat. with old "filter" and "pipe-to" commands | ||
| 669 | actions[STDIN_FILENO] = EXEC_LINE; | ||
| 670 | } | ||
| 671 | |||
| 672 | const char **argv = (const char **)a->args + a->nr_flag_args; | ||
| 673 | ssize_t outlen = handle_exec(e, argv, actions, spawn_flags, strip_nl); | ||
| 674 | if (outlen <= 0) { | ||
| 675 | return outlen == 0; | ||
| 676 | } | ||
| 677 | |||
| 678 | if (move_after_insert && actions[STDOUT_FILENO] == EXEC_BUFFER) { | ||
| 679 | block_iter_skip_bytes(&e->view->cursor, outlen); | ||
| 680 | } | ||
| 681 | return true; | ||
| 682 | } | ||
| 683 | |||
| 684 | static bool cmd_ft(EditorState *e, const CommandArgs *a) | ||
| 685 | { | ||
| 686 | char **args = a->args; | ||
| 687 | const char *filetype = args[0]; | ||
| 688 | if (unlikely(!is_valid_filetype_name(filetype))) { | ||
| 689 | return error_msg("Invalid filetype name: '%s'", filetype); | ||
| 690 | } | ||
| 691 | |||
| 692 | FileDetectionType dt = FT_EXTENSION; | ||
| 693 | switch (last_flag(a)) { | ||
| 694 | case 'b': | ||
| 695 | dt = FT_BASENAME; | ||
| 696 | break; | ||
| 697 | case 'c': | ||
| 698 | dt = FT_CONTENT; | ||
| 699 | break; | ||
| 700 | case 'f': | ||
| 701 | dt = FT_FILENAME; | ||
| 702 | break; | ||
| 703 | case 'i': | ||
| 704 | dt = FT_INTERPRETER; | ||
| 705 | break; | ||
| 706 | } | ||
| 707 | |||
| 708 | size_t nfailed = 0; | ||
| 709 | for (size_t i = 1, n = a->nr_args; i < n; i++) { | ||
| 710 | if (!add_filetype(&e->filetypes, filetype, args[i], dt)) { | ||
| 711 | nfailed++; | ||
| 712 | } | ||
| 713 | } | ||
| 714 | |||
| 715 | return nfailed == 0; | ||
| 716 | } | ||
| 717 | |||
| 718 | static bool cmd_hi(EditorState *e, const CommandArgs *a) | ||
| 719 | { | ||
| 720 | if (unlikely(a->nr_args == 0)) { | ||
| 721 | exec_builtin_color_reset(e); | ||
| 722 | goto update; | ||
| 723 | } | ||
| 724 | |||
| 725 | char **strs = a->args + 1; | ||
| 726 | size_t strs_len = a->nr_args - 1; | ||
| 727 | TermColor color; | ||
| 728 | ssize_t n = parse_term_color(&color, strs, strs_len); | ||
| 729 | if (unlikely(n != strs_len)) { | ||
| 730 | if (n < 0) { | ||
| 731 | return error_msg("too many colors"); | ||
| 732 | } | ||
| 733 | BUG_ON(n > strs_len); | ||
| 734 | return error_msg("invalid color or attribute: '%s'", strs[n]); | ||
| 735 | } | ||
| 736 | |||
| 737 | TermColorCapabilityType color_type = e->terminal.color_type; | ||
| 738 | bool optimize = e->options.optimize_true_color; | ||
| 739 | int32_t fg = color_to_nearest(color.fg, color_type, optimize); | ||
| 740 | int32_t bg = color_to_nearest(color.bg, color_type, optimize); | ||
| 741 | if ( | ||
| 742 | color_type != TERM_TRUE_COLOR | ||
| 743 | && has_flag(a, 'c') | ||
| 744 | && (fg != color.fg || bg != color.bg) | ||
| 745 | ) { | ||
| 746 | return true; | ||
| 747 | } | ||
| 748 | |||
| 749 | color.fg = fg; | ||
| 750 | color.bg = bg; | ||
| 751 | set_highlight_color(&e->colors, a->args[0], &color); | ||
| 752 | |||
| 753 | update: | ||
| 754 | // Don't call update_all_syntax_colors() needlessly; it's called | ||
| 755 | // right after config has been loaded | ||
| 756 | if (e->status != EDITOR_INITIALIZING) { | ||
| 757 | update_all_syntax_colors(&e->syntaxes, &e->colors); | ||
| 758 | mark_everything_changed(e); | ||
| 759 | } | ||
| 760 | return true; | ||
| 761 | } | ||
| 762 | |||
| 763 | static bool cmd_include(EditorState *e, const CommandArgs *a) | ||
| 764 | { | ||
| 765 | ConfigFlags flags = has_flag(a, 'q') ? CFG_NOFLAGS : CFG_MUST_EXIST; | ||
| 766 | if (has_flag(a, 'b')) { | ||
| 767 | flags |= CFG_BUILTIN; | ||
| 768 | } | ||
| 769 | int err = read_normal_config(e, a->args[0], flags); | ||
| 770 | // TODO: Clean up read_normal_config() so this can be simplified to `err == 0` | ||
| 771 | return err == 0 || (err == ENOENT && !(flags & CFG_MUST_EXIST)); | ||
| 772 | } | ||
| 773 | |||
| 774 | static bool cmd_insert(EditorState *e, const CommandArgs *a) | ||
| 775 | { | ||
| 776 | const char *str = a->args[0]; | ||
| 777 | if (has_flag(a, 'k')) { | ||
| 778 | for (size_t i = 0; str[i]; i++) { | ||
| 779 | insert_ch(e->view, str[i]); | ||
| 780 | } | ||
| 781 | return true; | ||
| 782 | } | ||
| 783 | |||
| 784 | bool move_after = has_flag(a, 'm'); | ||
| 785 | insert_text(e->view, str, strlen(str), move_after); | ||
| 786 | return true; | ||
| 787 | } | ||
| 788 | |||
| 789 | static bool cmd_join(EditorState *e, const CommandArgs *a) | ||
| 790 | { | ||
| 791 | BUG_ON(a->nr_args); | ||
| 792 | join_lines(e->view); | ||
| 793 | return true; | ||
| 794 | } | ||
| 795 | |||
| 796 | static bool cmd_left(EditorState *e, const CommandArgs *a) | ||
| 797 | { | ||
| 798 | handle_select_chars_flag(e->view, a); | ||
| 799 | move_cursor_left(e->view); | ||
| 800 | return true; | ||
| 801 | } | ||
| 802 | |||
| 803 | static bool cmd_line(EditorState *e, const CommandArgs *a) | ||
| 804 | { | ||
| 805 | const char *str = a->args[0]; | ||
| 806 | size_t line, column; | ||
| 807 | if (unlikely(!str_to_xfilepos(str, &line, &column))) { | ||
| 808 | return error_msg("Invalid line number: %s", str); | ||
| 809 | } | ||
| 810 | |||
| 811 | View *view = e->view; | ||
| 812 | long x = view_get_preferred_x(view); | ||
| 813 | unselect(view); | ||
| 814 | |||
| 815 | if (column >= 1) { | ||
| 816 | // Column was specified; move to exact position | ||
| 817 | move_to_filepos(view, line, column); | ||
| 818 | } else { | ||
| 819 | // Column was omitted; move to line while preserving current column | ||
| 820 | move_to_line(view, line); | ||
| 821 | move_to_preferred_x(view, x); | ||
| 822 | } | ||
| 823 | |||
| 824 | return true; | ||
| 825 | } | ||
| 826 | |||
| 827 | static bool cmd_load_syntax(EditorState *e, const CommandArgs *a) | ||
| 828 | { | ||
| 829 | const char *arg = a->args[0]; | ||
| 830 | const char *slash = strrchr(arg, '/'); | ||
| 831 | if (!slash) { | ||
| 832 | const char *filetype = arg; | ||
| 833 | if (find_syntax(&e->syntaxes, filetype)) { | ||
| 834 | return true; | ||
| 835 | } | ||
| 836 | return !!load_syntax_by_filetype(e, filetype); | ||
| 837 | } | ||
| 838 | |||
| 839 | const char *filetype = slash + 1; | ||
| 840 | if (find_syntax(&e->syntaxes, filetype)) { | ||
| 841 | return error_msg("Syntax for filetype %s already loaded", filetype); | ||
| 842 | } | ||
| 843 | |||
| 844 | int err; | ||
| 845 | return !!load_syntax_file(e, arg, CFG_MUST_EXIST, &err); | ||
| 846 | } | ||
| 847 | |||
| 848 | static bool cmd_macro(EditorState *e, const CommandArgs *a) | ||
| 849 | { | ||
| 850 | CommandMacroState *m = &e->macro; | ||
| 851 | const char *action = a->args[0]; | ||
| 852 | |||
| 853 | if (streq(action, "play") || streq(action, "run")) { | ||
| 854 | for (size_t i = 0, n = m->macro.count; i < n; i++) { | ||
| 855 | const char *cmd_str = m->macro.ptrs[i]; | ||
| 856 | if (!handle_normal_command(e, cmd_str, false)) { | ||
| 857 | return false; | ||
| 858 | } | ||
| 859 | } | ||
| 860 | return true; | ||
| 861 | } | ||
| 862 | |||
| 863 | const char *msg; | ||
| 864 | if (streq(action, "toggle")) { | ||
| 865 | if (m->recording) { | ||
| 866 | goto stop; | ||
| 867 | } | ||
| 868 | goto record; | ||
| 869 | } | ||
| 870 | |||
| 871 | if (streq(action, "record")) { | ||
| 872 | record: | ||
| 873 | msg = macro_record(m) ? "Recording macro" : "Already recording"; | ||
| 874 | goto message; | ||
| 875 | } | ||
| 876 | |||
| 877 | if (streq(action, "stop")) { | ||
| 878 | stop: | ||
| 879 | if (!macro_stop(m)) { | ||
| 880 | msg = "Not recording"; | ||
| 881 | goto message; | ||
| 882 | } | ||
| 883 | size_t count = m->macro.count; | ||
| 884 | const char *plural = (count != 1) ? "s" : ""; | ||
| 885 | info_msg("Macro recording stopped; %zu command%s saved", count, plural); | ||
| 886 | return true; | ||
| 887 | } | ||
| 888 | |||
| 889 | if (streq(action, "cancel")) { | ||
| 890 | msg = macro_cancel(m) ? "Macro recording cancelled" : "Not recording"; | ||
| 891 | goto message; | ||
| 892 | } | ||
| 893 | |||
| 894 | return error_msg("Unknown action '%s'", action); | ||
| 895 | |||
| 896 | message: | ||
| 897 | info_msg("%s", msg); | ||
| 898 | // TODO: make this conditional? | ||
| 899 | return true; | ||
| 900 | } | ||
| 901 | |||
| 902 | static bool cmd_match_bracket(EditorState *e, const CommandArgs *a) | ||
| 903 | { | ||
| 904 | BUG_ON(a->nr_args); | ||
| 905 | View *view = e->view; | ||
| 906 | CodePoint cursor_char; | ||
| 907 | if (!block_iter_get_char(&view->cursor, &cursor_char)) { | ||
| 908 | return error_msg("No character under cursor"); | ||
| 909 | } | ||
| 910 | |||
| 911 | CodePoint target = cursor_char; | ||
| 912 | BlockIter bi = view->cursor; | ||
| 913 | size_t level = 0; | ||
| 914 | CodePoint u = 0; | ||
| 915 | |||
| 916 | switch (cursor_char) { | ||
| 917 | case '<': | ||
| 918 | case '[': | ||
| 919 | case '{': | ||
| 920 | target++; | ||
| 921 | // Fallthrough | ||
| 922 | case '(': | ||
| 923 | target++; | ||
| 924 | goto search_fwd; | ||
| 925 | case '>': | ||
| 926 | case ']': | ||
| 927 | case '}': | ||
| 928 | target--; | ||
| 929 | // Fallthrough | ||
| 930 | case ')': | ||
| 931 | target--; | ||
| 932 | goto search_bwd; | ||
| 933 | default: | ||
| 934 | return error_msg("Character under cursor not matchable"); | ||
| 935 | } | ||
| 936 | |||
| 937 | search_fwd: | ||
| 938 | block_iter_next_char(&bi, &u); | ||
| 939 | BUG_ON(u != cursor_char); | ||
| 940 | while (block_iter_next_char(&bi, &u)) { | ||
| 941 | if (u == target) { | ||
| 942 | if (level == 0) { | ||
| 943 | block_iter_prev_char(&bi, &u); | ||
| 944 | view->cursor = bi; | ||
| 945 | return true; // Found | ||
| 946 | } | ||
| 947 | level--; | ||
| 948 | } else if (u == cursor_char) { | ||
| 949 | level++; | ||
| 950 | } | ||
| 951 | } | ||
| 952 | goto not_found; | ||
| 953 | |||
| 954 | search_bwd: | ||
| 955 | while (block_iter_prev_char(&bi, &u)) { | ||
| 956 | if (u == target) { | ||
| 957 | if (level == 0) { | ||
| 958 | view->cursor = bi; | ||
| 959 | return true; // Found | ||
| 960 | } | ||
| 961 | level--; | ||
| 962 | } else if (u == cursor_char) { | ||
| 963 | level++; | ||
| 964 | } | ||
| 965 | } | ||
| 966 | |||
| 967 | not_found: | ||
| 968 | return error_msg("No matching bracket found"); | ||
| 969 | } | ||
| 970 | |||
| 971 | static bool cmd_move_tab(EditorState *e, const CommandArgs *a) | ||
| 972 | { | ||
| 973 | Window *window = e->window; | ||
| 974 | const size_t ntabs = window->views.count; | ||
| 975 | const char *str = a->args[0]; | ||
| 976 | size_t to, from = ptr_array_idx(&window->views, e->view); | ||
| 977 | BUG_ON(from >= ntabs); | ||
| 978 | if (streq(str, "left")) { | ||
| 979 | to = size_decrement_wrapped(from, ntabs); | ||
| 980 | } else if (streq(str, "right")) { | ||
| 981 | to = size_increment_wrapped(from, ntabs); | ||
| 982 | } else { | ||
| 983 | if (!str_to_size(str, &to) || to == 0) { | ||
| 984 | return error_msg("Invalid tab position %s", str); | ||
| 985 | } | ||
| 986 | to = MIN(to, ntabs) - 1; | ||
| 987 | } | ||
| 988 | ptr_array_move(&window->views, from, to); | ||
| 989 | window->update_tabbar = true; | ||
| 990 | return true; | ||
| 991 | } | ||
| 992 | |||
| 993 | static bool cmd_msg(EditorState *e, const CommandArgs *a) | ||
| 994 | { | ||
| 995 | const char *str = a->args[0]; | ||
| 996 | uint_least64_t np = cmdargs_flagset_value('n') | cmdargs_flagset_value('p'); | ||
| 997 | if (u64_popcount(a->flag_set & np) + !!str >= 2) { | ||
| 998 | return error_msg("flags [-n|-p] and [number] argument are mutually exclusive"); | ||
| 999 | } | ||
| 1000 | |||
| 1001 | MessageArray *msgs = &e->messages; | ||
| 1002 | size_t count = msgs->array.count; | ||
| 1003 | if (count == 0) { | ||
| 1004 | return true; | ||
| 1005 | } | ||
| 1006 | |||
| 1007 | size_t p = msgs->pos; | ||
| 1008 | BUG_ON(p >= count); | ||
| 1009 | if (has_flag(a, 'n')) { | ||
| 1010 | p = MIN(p + 1, count - 1); | ||
| 1011 | } else if (has_flag(a, 'p')) { | ||
| 1012 | p = p ? p - 1 : 0; | ||
| 1013 | } else if (str) { | ||
| 1014 | if (!str_to_size(str, &p) || p == 0) { | ||
| 1015 | return error_msg("invalid message index: %s", str); | ||
| 1016 | } | ||
| 1017 | p = MIN(p - 1, count - 1); | ||
| 1018 | } | ||
| 1019 | |||
| 1020 | msgs->pos = p; | ||
| 1021 | return activate_current_message(e); | ||
| 1022 | } | ||
| 1023 | |||
| 1024 | static bool cmd_new_line(EditorState *e, const CommandArgs *a) | ||
| 1025 | { | ||
| 1026 | new_line(e->view, has_flag(a, 'a')); | ||
| 1027 | return true; | ||
| 1028 | } | ||
| 1029 | |||
| 1030 | static bool cmd_next(EditorState *e, const CommandArgs *a) | ||
| 1031 | { | ||
| 1032 | BUG_ON(a->nr_args); | ||
| 1033 | size_t i = ptr_array_idx(&e->window->views, e->view); | ||
| 1034 | size_t n = e->window->views.count; | ||
| 1035 | BUG_ON(i >= n); | ||
| 1036 | set_view(e->window->views.ptrs[size_increment_wrapped(i, n)]); | ||
| 1037 | return true; | ||
| 1038 | } | ||
| 1039 | |||
| 1040 | static bool xglob(char **args, glob_t *globbuf) | ||
| 1041 | { | ||
| 1042 | BUG_ON(!args); | ||
| 1043 | BUG_ON(!args[0]); | ||
| 1044 | int err = glob(*args, GLOB_NOCHECK, NULL, globbuf); | ||
| 1045 | while (err == 0 && *++args) { | ||
| 1046 | err = glob(*args, GLOB_NOCHECK | GLOB_APPEND, NULL, globbuf); | ||
| 1047 | } | ||
| 1048 | |||
| 1049 | if (likely(err == 0)) { | ||
| 1050 | BUG_ON(globbuf->gl_pathc == 0); | ||
| 1051 | BUG_ON(!globbuf->gl_pathv); | ||
| 1052 | BUG_ON(!globbuf->gl_pathv[0]); | ||
| 1053 | return true; | ||
| 1054 | } | ||
| 1055 | |||
| 1056 | BUG_ON(err == GLOB_NOMATCH); | ||
| 1057 | globfree(globbuf); | ||
| 1058 | return error_msg("glob: %s", (err == GLOB_NOSPACE) ? strerror(ENOMEM) : "failed"); | ||
| 1059 | } | ||
| 1060 | |||
| 1061 | static bool cmd_open(EditorState *e, const CommandArgs *a) | ||
| 1062 | { | ||
| 1063 | bool temporary = has_flag(a, 't'); | ||
| 1064 | if (unlikely(temporary && a->nr_args > 0)) { | ||
| 1065 | return error_msg("'open -t' can't be used with filename arguments"); | ||
| 1066 | } | ||
| 1067 | |||
| 1068 | const char *requested_encoding = NULL; | ||
| 1069 | char **args = a->args; | ||
| 1070 | if (unlikely(a->nr_flag_args > 0)) { | ||
| 1071 | // The "-e" flag is the only one that takes an argument, so the | ||
| 1072 | // above condition implies it was used | ||
| 1073 | BUG_ON(!has_flag(a, 'e')); | ||
| 1074 | requested_encoding = args[a->nr_flag_args - 1]; | ||
| 1075 | args += a->nr_flag_args; | ||
| 1076 | } | ||
| 1077 | |||
| 1078 | Encoding encoding = {.type = ENCODING_AUTODETECT}; | ||
| 1079 | if (requested_encoding) { | ||
| 1080 | EncodingType enctype = lookup_encoding(requested_encoding); | ||
| 1081 | if (enctype == UTF8) { | ||
| 1082 | encoding = encoding_from_type(enctype); | ||
| 1083 | } else if (conversion_supported_by_iconv(requested_encoding, "UTF-8")) { | ||
| 1084 | encoding = encoding_from_name(requested_encoding); | ||
| 1085 | } else { | ||
| 1086 | if (errno == EINVAL) { | ||
| 1087 | return error_msg("Unsupported encoding '%s'", requested_encoding); | ||
| 1088 | } | ||
| 1089 | return error_msg ( | ||
| 1090 | "iconv conversion from '%s' failed: %s", | ||
| 1091 | requested_encoding, | ||
| 1092 | strerror(errno) | ||
| 1093 | ); | ||
| 1094 | } | ||
| 1095 | } | ||
| 1096 | |||
| 1097 | if (a->nr_args == 0) { | ||
| 1098 | View *view = window_open_new_file(e->window); | ||
| 1099 | view->buffer->temporary = temporary; | ||
| 1100 | if (requested_encoding) { | ||
| 1101 | buffer_set_encoding(view->buffer, encoding, e->options.utf8_bom); | ||
| 1102 | } | ||
| 1103 | return true; | ||
| 1104 | } | ||
| 1105 | |||
| 1106 | char **paths = args; | ||
| 1107 | glob_t globbuf; | ||
| 1108 | bool use_glob = has_flag(a, 'g'); | ||
| 1109 | if (use_glob) { | ||
| 1110 | if (!xglob(args, &globbuf)) { | ||
| 1111 | return false; | ||
| 1112 | } | ||
| 1113 | paths = globbuf.gl_pathv; | ||
| 1114 | } | ||
| 1115 | |||
| 1116 | View *first_opened; | ||
| 1117 | if (!paths[1]) { | ||
| 1118 | // Previous view is remembered when opening single file | ||
| 1119 | first_opened = window_open_file(e->window, paths[0], &encoding); | ||
| 1120 | } else { | ||
| 1121 | // It makes no sense to remember previous view when opening multiple files | ||
| 1122 | first_opened = window_open_files(e->window, paths, &encoding); | ||
| 1123 | } | ||
| 1124 | |||
| 1125 | if (use_glob) { | ||
| 1126 | globfree(&globbuf); | ||
| 1127 | } | ||
| 1128 | |||
| 1129 | return !!first_opened; | ||
| 1130 | } | ||
| 1131 | |||
| 1132 | static bool cmd_option(EditorState *e, const CommandArgs *a) | ||
| 1133 | { | ||
| 1134 | BUG_ON(a->nr_args < 3); | ||
| 1135 | size_t nstrs = a->nr_args - 1; | ||
| 1136 | if (unlikely(nstrs & 1)) { | ||
| 1137 | return error_msg("Missing option value"); | ||
| 1138 | } | ||
| 1139 | |||
| 1140 | char **strs = a->args + 1; | ||
| 1141 | if (unlikely(!validate_local_options(strs))) { | ||
| 1142 | return false; | ||
| 1143 | } | ||
| 1144 | |||
| 1145 | PointerArray *opts = &e->file_options; | ||
| 1146 | if (has_flag(a, 'r')) { | ||
| 1147 | const StringView pattern = strview_from_cstring(a->args[0]); | ||
| 1148 | return add_file_options(opts, FOPTS_FILENAME, pattern, strs, nstrs); | ||
| 1149 | } | ||
| 1150 | |||
| 1151 | const char *ft_list = a->args[0]; | ||
| 1152 | size_t errors = 0; | ||
| 1153 | for (size_t pos = 0, len = strlen(ft_list); pos < len; ) { | ||
| 1154 | const StringView filetype = get_delim(ft_list, &pos, len, ','); | ||
| 1155 | if (!add_file_options(opts, FOPTS_FILETYPE, filetype, strs, nstrs)) { | ||
| 1156 | errors++; | ||
| 1157 | } | ||
| 1158 | } | ||
| 1159 | |||
| 1160 | return !errors; | ||
| 1161 | } | ||
| 1162 | |||
| 1163 | static bool cmd_blkdown(EditorState *e, const CommandArgs *a) | ||
| 1164 | { | ||
| 1165 | View *view = e->view; | ||
| 1166 | handle_select_chars_or_lines_flags(view, a); | ||
| 1167 | |||
| 1168 | // If current line is blank, skip past consecutive blank lines | ||
| 1169 | StringView line; | ||
| 1170 | fetch_this_line(&view->cursor, &line); | ||
| 1171 | if (strview_isblank(&line)) { | ||
| 1172 | while (block_iter_next_line(&view->cursor)) { | ||
| 1173 | fill_line_ref(&view->cursor, &line); | ||
| 1174 | if (!strview_isblank(&line)) { | ||
| 1175 | break; | ||
| 1176 | } | ||
| 1177 | } | ||
| 1178 | } | ||
| 1179 | |||
| 1180 | // Skip past non-blank lines | ||
| 1181 | while (block_iter_next_line(&view->cursor)) { | ||
| 1182 | fill_line_ref(&view->cursor, &line); | ||
| 1183 | if (strview_isblank(&line)) { | ||
| 1184 | break; | ||
| 1185 | } | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | // If we reach the last populated line in the buffer, move down one line | ||
| 1189 | BlockIter tmp = view->cursor; | ||
| 1190 | block_iter_eol(&tmp); | ||
| 1191 | block_iter_skip_bytes(&tmp, 1); | ||
| 1192 | if (block_iter_is_eof(&tmp)) { | ||
| 1193 | view->cursor = tmp; | ||
| 1194 | } | ||
| 1195 | |||
| 1196 | return true; | ||
| 1197 | } | ||
| 1198 | |||
| 1199 | static bool cmd_blkup(EditorState *e, const CommandArgs *a) | ||
| 1200 | { | ||
| 1201 | View *view = e->view; | ||
| 1202 | handle_select_chars_or_lines_flags(view, a); | ||
| 1203 | |||
| 1204 | // If cursor is on the first line, just move to bol | ||
| 1205 | if (view->cy == 0) { | ||
| 1206 | block_iter_bol(&view->cursor); | ||
| 1207 | return true; | ||
| 1208 | } | ||
| 1209 | |||
| 1210 | // If current line is blank, skip past consecutive blank lines | ||
| 1211 | StringView line; | ||
| 1212 | fetch_this_line(&view->cursor, &line); | ||
| 1213 | if (strview_isblank(&line)) { | ||
| 1214 | while (block_iter_prev_line(&view->cursor)) { | ||
| 1215 | fill_line_ref(&view->cursor, &line); | ||
| 1216 | if (!strview_isblank(&line)) { | ||
| 1217 | break; | ||
| 1218 | } | ||
| 1219 | } | ||
| 1220 | } | ||
| 1221 | |||
| 1222 | // Skip past non-blank lines | ||
| 1223 | while (block_iter_prev_line(&view->cursor)) { | ||
| 1224 | fill_line_ref(&view->cursor, &line); | ||
| 1225 | if (strview_isblank(&line)) { | ||
| 1226 | break; | ||
| 1227 | } | ||
| 1228 | } | ||
| 1229 | |||
| 1230 | return true; | ||
| 1231 | } | ||
| 1232 | |||
| 1233 | static bool cmd_paste(EditorState *e, const CommandArgs *a) | ||
| 1234 | { | ||
| 1235 | bool move_after = has_flag(a, 'm'); | ||
| 1236 | bool above_cursor = has_flag(a, 'a'); | ||
| 1237 | bool at_cursor = has_flag(a, 'c'); | ||
| 1238 | PasteLinesType type = PASTE_LINES_BELOW_CURSOR; | ||
| 1239 | |||
| 1240 | if (above_cursor && at_cursor) { | ||
| 1241 | return error_msg("flags -a and -c are mutually exclusive"); | ||
| 1242 | } else if (above_cursor) { | ||
| 1243 | type = PASTE_LINES_ABOVE_CURSOR; | ||
| 1244 | } else if (at_cursor) { | ||
| 1245 | type = PASTE_LINES_INLINE; | ||
| 1246 | } | ||
| 1247 | |||
| 1248 | paste(&e->clipboard, e->view, type, move_after); | ||
| 1249 | return true; | ||
| 1250 | } | ||
| 1251 | |||
| 1252 | static bool cmd_pgdown(EditorState *e, const CommandArgs *a) | ||
| 1253 | { | ||
| 1254 | View *view = e->view; | ||
| 1255 | handle_select_chars_or_lines_flags(view, a); | ||
| 1256 | |||
| 1257 | Window *window = e->window; | ||
| 1258 | long margin = window_get_scroll_margin(window, e->options.scroll_margin); | ||
| 1259 | long bottom = view->vy + window->edit_h - 1 - margin; | ||
| 1260 | long count; | ||
| 1261 | |||
| 1262 | if (view->cy < bottom) { | ||
| 1263 | count = bottom - view->cy; | ||
| 1264 | } else { | ||
| 1265 | count = window->edit_h - 1 - margin * 2; | ||
| 1266 | } | ||
| 1267 | |||
| 1268 | move_down(view, count); | ||
| 1269 | return true; | ||
| 1270 | } | ||
| 1271 | |||
| 1272 | static bool cmd_pgup(EditorState *e, const CommandArgs *a) | ||
| 1273 | { | ||
| 1274 | View *view = e->view; | ||
| 1275 | handle_select_chars_or_lines_flags(view, a); | ||
| 1276 | |||
| 1277 | Window *window = e->window; | ||
| 1278 | long margin = window_get_scroll_margin(window, e->options.scroll_margin); | ||
| 1279 | long top = view->vy + margin; | ||
| 1280 | long count; | ||
| 1281 | |||
| 1282 | if (view->cy > top) { | ||
| 1283 | count = view->cy - top; | ||
| 1284 | } else { | ||
| 1285 | count = window->edit_h - 1 - margin * 2; | ||
| 1286 | } | ||
| 1287 | |||
| 1288 | move_up(view, count); | ||
| 1289 | return true; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | static bool cmd_prev(EditorState *e, const CommandArgs *a) | ||
| 1293 | { | ||
| 1294 | BUG_ON(a->nr_args); | ||
| 1295 | size_t i = ptr_array_idx(&e->window->views, e->view); | ||
| 1296 | size_t n = e->window->views.count; | ||
| 1297 | BUG_ON(i >= n); | ||
| 1298 | set_view(e->window->views.ptrs[size_decrement_wrapped(i, n)]); | ||
| 1299 | return true; | ||
| 1300 | } | ||
| 1301 | |||
| 1302 | static View *window_find_modified_view(Window *window) | ||
| 1303 | { | ||
| 1304 | if (buffer_modified(window->view->buffer)) { | ||
| 1305 | return window->view; | ||
| 1306 | } | ||
| 1307 | for (size_t i = 0, n = window->views.count; i < n; i++) { | ||
| 1308 | View *view = window->views.ptrs[i]; | ||
| 1309 | if (buffer_modified(view->buffer)) { | ||
| 1310 | return view; | ||
| 1311 | } | ||
| 1312 | } | ||
| 1313 | return NULL; | ||
| 1314 | } | ||
| 1315 | |||
| 1316 | static size_t count_modified_buffers(const PointerArray *buffers, View **first) | ||
| 1317 | { | ||
| 1318 | View *modified = NULL; | ||
| 1319 | size_t nr_modified = 0; | ||
| 1320 | for (size_t i = 0, n = buffers->count; i < n; i++) { | ||
| 1321 | Buffer *buffer = buffers->ptrs[i]; | ||
| 1322 | if (!buffer_modified(buffer)) { | ||
| 1323 | continue; | ||
| 1324 | } | ||
| 1325 | nr_modified++; | ||
| 1326 | if (!modified) { | ||
| 1327 | modified = buffer->views.ptrs[0]; | ||
| 1328 | } | ||
| 1329 | } | ||
| 1330 | |||
| 1331 | BUG_ON(nr_modified > 0 && !modified); | ||
| 1332 | *first = modified; | ||
| 1333 | return nr_modified; | ||
| 1334 | } | ||
| 1335 | |||
| 1336 | static bool cmd_quit(EditorState *e, const CommandArgs *a) | ||
| 1337 | { | ||
| 1338 | int exit_code = EDITOR_EXIT_OK; | ||
| 1339 | if (a->nr_args) { | ||
| 1340 | if (!str_to_int(a->args[0], &exit_code)) { | ||
| 1341 | return error_msg("Not a valid integer argument: '%s'", a->args[0]); | ||
| 1342 | } | ||
| 1343 | int max = EDITOR_EXIT_MAX; | ||
| 1344 | if (exit_code < 0 || exit_code > max) { | ||
| 1345 | return error_msg("Exit code should be between 0 and %d", max); | ||
| 1346 | } | ||
| 1347 | } | ||
| 1348 | |||
| 1349 | View *first_modified = NULL; | ||
| 1350 | size_t n = count_modified_buffers(&e->buffers, &first_modified); | ||
| 1351 | if (n == 0) { | ||
| 1352 | goto exit; | ||
| 1353 | } | ||
| 1354 | |||
| 1355 | BUG_ON(!first_modified); | ||
| 1356 | const char *plural = (n > 1) ? "s" : ""; | ||
| 1357 | if (has_flag(a, 'f')) { | ||
| 1358 | LOG_INFO("force quitting with %zu modified buffer%s", n, plural); | ||
| 1359 | goto exit; | ||
| 1360 | } | ||
| 1361 | |||
| 1362 | // Activate a modified view (giving preference to the current view or | ||
| 1363 | // a view in the current window) | ||
| 1364 | View *view = window_find_modified_view(e->window); | ||
| 1365 | set_view(view ? view : first_modified); | ||
| 1366 | |||
| 1367 | if (!has_flag(a, 'p')) { | ||
| 1368 | return error_msg("Save modified files or run 'quit -f' to quit without saving"); | ||
| 1369 | } | ||
| 1370 | |||
| 1371 | char question[128]; | ||
| 1372 | xsnprintf ( | ||
| 1373 | question, sizeof question, | ||
| 1374 | "Quit without saving %zu modified buffer%s? [y/N]", | ||
| 1375 | n, plural | ||
| 1376 | ); | ||
| 1377 | |||
| 1378 | if (dialog_prompt(e, question, "ny") != 'y') { | ||
| 1379 | return false; | ||
| 1380 | } | ||
| 1381 | |||
| 1382 | LOG_INFO("quit prompt accepted with %zu modified buffer%s", n, plural); | ||
| 1383 | |||
| 1384 | exit: | ||
| 1385 | e->status = exit_code; | ||
| 1386 | return true; | ||
| 1387 | } | ||
| 1388 | |||
| 1389 | static bool cmd_redo(EditorState *e, const CommandArgs *a) | ||
| 1390 | { | ||
| 1391 | char *arg = a->args[0]; | ||
| 1392 | unsigned long change_id = 0; | ||
| 1393 | if (arg) { | ||
| 1394 | if (!str_to_ulong(arg, &change_id) || change_id == 0) { | ||
| 1395 | return error_msg("Invalid change id: %s", arg); | ||
| 1396 | } | ||
| 1397 | } | ||
| 1398 | if (!redo(e->view, change_id)) { | ||
| 1399 | return false; | ||
| 1400 | } | ||
| 1401 | |||
| 1402 | unselect(e->view); | ||
| 1403 | return true; | ||
| 1404 | } | ||
| 1405 | |||
| 1406 | static bool cmd_refresh(EditorState *e, const CommandArgs *a) | ||
| 1407 | { | ||
| 1408 | BUG_ON(a->nr_args); | ||
| 1409 | mark_everything_changed(e); | ||
| 1410 | return true; | ||
| 1411 | } | ||
| 1412 | |||
| 1413 | static bool repeat_insert(EditorState *e, const char *str, unsigned int count, bool move_after) | ||
| 1414 | { | ||
| 1415 | size_t str_len = strlen(str); | ||
| 1416 | size_t bufsize; | ||
| 1417 | if (unlikely(size_multiply_overflows(count, str_len, &bufsize))) { | ||
| 1418 | return error_msg("Repeated insert would overflow"); | ||
| 1419 | } | ||
| 1420 | if (unlikely(bufsize == 0)) { | ||
| 1421 | return true; | ||
| 1422 | } | ||
| 1423 | |||
| 1424 | char *buf = malloc(bufsize); | ||
| 1425 | if (unlikely(!buf)) { | ||
| 1426 | return error_msg_errno("malloc"); | ||
| 1427 | } | ||
| 1428 | |||
| 1429 | char tmp[4096]; | ||
| 1430 | if (str_len == 1) { | ||
| 1431 | memset(buf, str[0], bufsize); | ||
| 1432 | goto insert; | ||
| 1433 | } else if (bufsize < 2 * sizeof(tmp) || str_len > sizeof(tmp) / 8) { | ||
| 1434 | for (size_t i = 0; i < count; i++) { | ||
| 1435 | memcpy(buf + (i * str_len), str, str_len); | ||
| 1436 | } | ||
| 1437 | goto insert; | ||
| 1438 | } | ||
| 1439 | |||
| 1440 | size_t strs_per_tmp = sizeof(tmp) / str_len; | ||
| 1441 | size_t tmp_len = strs_per_tmp * str_len; | ||
| 1442 | size_t tmps_per_buf = bufsize / tmp_len; | ||
| 1443 | size_t remainder = bufsize % tmp_len; | ||
| 1444 | |||
| 1445 | // Create a block of text containing `strs_per_tmp` concatenated strs | ||
| 1446 | for (size_t i = 0; i < strs_per_tmp; i++) { | ||
| 1447 | memcpy(tmp + (i * str_len), str, str_len); | ||
| 1448 | } | ||
| 1449 | |||
| 1450 | // Copy `tmps_per_buf` copies of `tmp` into `buf` | ||
| 1451 | for (size_t i = 0; i < tmps_per_buf; i++) { | ||
| 1452 | memcpy(buf + (i * tmp_len), tmp, tmp_len); | ||
| 1453 | } | ||
| 1454 | |||
| 1455 | // Copy the remainder into `buf` (if any) | ||
| 1456 | if (remainder) { | ||
| 1457 | memcpy(buf + (tmps_per_buf * tmp_len), tmp, remainder); | ||
| 1458 | } | ||
| 1459 | |||
| 1460 | LOG_DEBUG ( | ||
| 1461 | "Optimized %u inserts of %zu bytes into %zu inserts of %zu bytes", | ||
| 1462 | count, str_len, | ||
| 1463 | tmps_per_buf, tmp_len | ||
| 1464 | ); | ||
| 1465 | |||
| 1466 | insert: | ||
| 1467 | insert_text(e->view, buf, bufsize, move_after); | ||
| 1468 | free(buf); | ||
| 1469 | return true; | ||
| 1470 | } | ||
| 1471 | |||
| 1472 | static bool cmd_repeat(EditorState *e, const CommandArgs *a) | ||
| 1473 | { | ||
| 1474 | unsigned int count; | ||
| 1475 | if (unlikely(!str_to_uint(a->args[0], &count))) { | ||
| 1476 | return error_msg("Not a valid repeat count: %s", a->args[0]); | ||
| 1477 | } | ||
| 1478 | if (unlikely(count == 0)) { | ||
| 1479 | return true; | ||
| 1480 | } | ||
| 1481 | |||
| 1482 | const Command *cmd = find_normal_command(a->args[1]); | ||
| 1483 | if (unlikely(!cmd)) { | ||
| 1484 | return error_msg("No such command: %s", a->args[1]); | ||
| 1485 | } | ||
| 1486 | |||
| 1487 | CommandArgs a2 = cmdargs_new(a->args + 2); | ||
| 1488 | current_command = cmd; | ||
| 1489 | bool ok = parse_args(cmd, &a2); | ||
| 1490 | current_command = NULL; | ||
| 1491 | if (unlikely(!ok)) { | ||
| 1492 | return false; | ||
| 1493 | } | ||
| 1494 | |||
| 1495 | CommandFunc fn = cmd->cmd; | ||
| 1496 | if (fn == (CommandFunc)cmd_insert && !has_flag(&a2, 'k')) { | ||
| 1497 | // Use optimized implementation for repeated "insert" | ||
| 1498 | return repeat_insert(e, a2.args[0], count, has_flag(&a2, 'm')); | ||
| 1499 | } | ||
| 1500 | |||
| 1501 | while (count--) { | ||
| 1502 | fn(e, &a2); | ||
| 1503 | } | ||
| 1504 | // TODO: return false if fn() fails? | ||
| 1505 | return true; | ||
| 1506 | } | ||
| 1507 | |||
| 1508 | static bool cmd_replace(EditorState *e, const CommandArgs *a) | ||
| 1509 | { | ||
| 1510 | static const FlagMapping map[] = { | ||
| 1511 | {'b', REPLACE_BASIC}, | ||
| 1512 | {'c', REPLACE_CONFIRM}, | ||
| 1513 | {'g', REPLACE_GLOBAL}, | ||
| 1514 | {'i', REPLACE_IGNORE_CASE}, | ||
| 1515 | }; | ||
| 1516 | |||
| 1517 | ReplaceFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)); | ||
| 1518 | return reg_replace(e->view, a->args[0], a->args[1], flags); | ||
| 1519 | } | ||
| 1520 | |||
| 1521 | static bool cmd_right(EditorState *e, const CommandArgs *a) | ||
| 1522 | { | ||
| 1523 | handle_select_chars_flag(e->view, a); | ||
| 1524 | move_cursor_right(e->view); | ||
| 1525 | return true; | ||
| 1526 | } | ||
| 1527 | |||
| 1528 | static bool stat_changed(const FileInfo *file, const struct stat *st) | ||
| 1529 | { | ||
| 1530 | // Don't compare st_mode because we allow chmod 755 etc. | ||
| 1531 | return !timespecs_equal(get_stat_mtime(st), &file->mtime) | ||
| 1532 | || st->st_dev != file->dev | ||
| 1533 | || st->st_ino != file->ino | ||
| 1534 | || st->st_size != file->size; | ||
| 1535 | } | ||
| 1536 | |||
| 1537 | static bool save_unmodified_buffer(Buffer *buffer, const char *filename) | ||
| 1538 | { | ||
| 1539 | SaveUnmodifiedType type = buffer->options.save_unmodified; | ||
| 1540 | if (type == SAVE_NONE) { | ||
| 1541 | LOG_INFO("buffer unchanged; leaving file untouched"); | ||
| 1542 | return true; | ||
| 1543 | } | ||
| 1544 | |||
| 1545 | BUG_ON(type != SAVE_TOUCH); | ||
| 1546 | struct timespec times[2]; | ||
| 1547 | if (unlikely(clock_gettime(CLOCK_REALTIME, ×[0]) != 0)) { | ||
| 1548 | LOG_ERRNO("aborting partial save; clock_gettime() failed"); | ||
| 1549 | return false; | ||
| 1550 | } | ||
| 1551 | |||
| 1552 | times[1] = times[0]; | ||
| 1553 | if (unlikely(utimensat(AT_FDCWD, filename, times, 0) != 0)) { | ||
| 1554 | LOG_ERRNO("aborting partial save; utimensat() failed"); | ||
| 1555 | return false; | ||
| 1556 | } | ||
| 1557 | |||
| 1558 | buffer->file.mtime = times[0]; | ||
| 1559 | LOG_INFO("buffer unchanged; mtime/atime updated"); | ||
| 1560 | return true; | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | static bool cmd_save(EditorState *e, const CommandArgs *a) | ||
| 1564 | { | ||
| 1565 | Buffer *buffer = e->buffer; | ||
| 1566 | if (unlikely(buffer->stdout_buffer)) { | ||
| 1567 | const char *f = buffer_filename(buffer); | ||
| 1568 | info_msg("%s can't be saved; it will be piped to stdout on exit", f); | ||
| 1569 | return true; | ||
| 1570 | } | ||
| 1571 | |||
| 1572 | bool dos_nl = has_flag(a, 'd'); | ||
| 1573 | bool unix_nl = has_flag(a, 'u'); | ||
| 1574 | bool crlf = buffer->crlf_newlines; | ||
| 1575 | if (unlikely(dos_nl && unix_nl)) { | ||
| 1576 | return error_msg("flags -d and -u can't be used together"); | ||
| 1577 | } else if (dos_nl) { | ||
| 1578 | crlf = true; | ||
| 1579 | } else if (unix_nl) { | ||
| 1580 | crlf = false; | ||
| 1581 | } | ||
| 1582 | |||
| 1583 | const char *requested_encoding = NULL; | ||
| 1584 | char **args = a->args; | ||
| 1585 | if (unlikely(a->nr_flag_args > 0)) { | ||
| 1586 | BUG_ON(!has_flag(a, 'e')); | ||
| 1587 | requested_encoding = args[a->nr_flag_args - 1]; | ||
| 1588 | args += a->nr_flag_args; | ||
| 1589 | } | ||
| 1590 | |||
| 1591 | Encoding encoding = buffer->encoding; | ||
| 1592 | bool bom = buffer->bom; | ||
| 1593 | if (requested_encoding) { | ||
| 1594 | EncodingType et = lookup_encoding(requested_encoding); | ||
| 1595 | if (et == UTF8) { | ||
| 1596 | if (encoding.type != UTF8) { | ||
| 1597 | // Encoding changed | ||
| 1598 | encoding = encoding_from_type(et); | ||
| 1599 | bom = e->options.utf8_bom; | ||
| 1600 | } | ||
| 1601 | } else if (conversion_supported_by_iconv("UTF-8", requested_encoding)) { | ||
| 1602 | encoding = encoding_from_name(requested_encoding); | ||
| 1603 | if (encoding.name != buffer->encoding.name) { | ||
| 1604 | // Encoding changed | ||
| 1605 | bom = !!get_bom_for_encoding(encoding.type); | ||
| 1606 | } | ||
| 1607 | } else { | ||
| 1608 | if (errno == EINVAL) { | ||
| 1609 | return error_msg("Unsupported encoding '%s'", requested_encoding); | ||
| 1610 | } | ||
| 1611 | return error_msg ( | ||
| 1612 | "iconv conversion to '%s' failed: %s", | ||
| 1613 | requested_encoding, | ||
| 1614 | strerror(errno) | ||
| 1615 | ); | ||
| 1616 | } | ||
| 1617 | } | ||
| 1618 | |||
| 1619 | bool b = has_flag(a, 'b'); | ||
| 1620 | bool B = has_flag(a, 'B'); | ||
| 1621 | if (unlikely(b && B)) { | ||
| 1622 | return error_msg("flags -b and -B can't be used together"); | ||
| 1623 | } else if (b) { | ||
| 1624 | bom = true; | ||
| 1625 | } else if (B) { | ||
| 1626 | bom = false; | ||
| 1627 | } | ||
| 1628 | |||
| 1629 | char *absolute = buffer->abs_filename; | ||
| 1630 | bool force = has_flag(a, 'f'); | ||
| 1631 | bool new_locked = false; | ||
| 1632 | if (a->nr_args > 0) { | ||
| 1633 | if (args[0][0] == '\0') { | ||
| 1634 | return error_msg("Empty filename not allowed"); | ||
| 1635 | } | ||
| 1636 | char *tmp = path_absolute(args[0]); | ||
| 1637 | if (!tmp) { | ||
| 1638 | return error_msg_errno("Failed to make absolute path"); | ||
| 1639 | } | ||
| 1640 | if (absolute && streq(tmp, absolute)) { | ||
| 1641 | free(tmp); | ||
| 1642 | } else { | ||
| 1643 | absolute = tmp; | ||
| 1644 | } | ||
| 1645 | } else { | ||
| 1646 | if (!absolute) { | ||
| 1647 | if (!has_flag(a, 'p')) { | ||
| 1648 | return error_msg("No filename"); | ||
| 1649 | } | ||
| 1650 | set_input_mode(e, INPUT_COMMAND); | ||
| 1651 | cmdline_set_text(&e->cmdline, "save "); | ||
| 1652 | return true; | ||
| 1653 | } | ||
| 1654 | if (buffer->readonly && !force) { | ||
| 1655 | return error_msg("Use -f to force saving read-only file"); | ||
| 1656 | } | ||
| 1657 | } | ||
| 1658 | |||
| 1659 | mode_t old_mode = buffer->file.mode; | ||
| 1660 | bool hardlinks = false; | ||
| 1661 | struct stat st; | ||
| 1662 | bool stat_ok = !stat(absolute, &st); | ||
| 1663 | if (!stat_ok) { | ||
| 1664 | if (errno != ENOENT) { | ||
| 1665 | error_msg("stat failed for %s: %s", absolute, strerror(errno)); | ||
| 1666 | goto error; | ||
| 1667 | } | ||
| 1668 | } else { | ||
| 1669 | if ( | ||
| 1670 | absolute == buffer->abs_filename | ||
| 1671 | && !force | ||
| 1672 | && stat_changed(&buffer->file, &st) | ||
| 1673 | ) { | ||
| 1674 | error_msg ( | ||
| 1675 | "File has been modified by another process; " | ||
| 1676 | "use 'save -f' to force overwrite" | ||
| 1677 | ); | ||
| 1678 | goto error; | ||
| 1679 | } | ||
| 1680 | if (S_ISDIR(st.st_mode)) { | ||
| 1681 | error_msg("Will not overwrite directory %s", absolute); | ||
| 1682 | goto error; | ||
| 1683 | } | ||
| 1684 | hardlinks = (st.st_nlink >= 2); | ||
| 1685 | } | ||
| 1686 | |||
| 1687 | if (e->options.lock_files) { | ||
| 1688 | if (absolute == buffer->abs_filename) { | ||
| 1689 | if (!buffer->locked) { | ||
| 1690 | if (!lock_file(absolute)) { | ||
| 1691 | if (!force) { | ||
| 1692 | error_msg("Can't lock file %s", absolute); | ||
| 1693 | goto error; | ||
| 1694 | } | ||
| 1695 | } else { | ||
| 1696 | buffer->locked = true; | ||
| 1697 | } | ||
| 1698 | } | ||
| 1699 | } else { | ||
| 1700 | if (!lock_file(absolute)) { | ||
| 1701 | if (!force) { | ||
| 1702 | error_msg("Can't lock file %s", absolute); | ||
| 1703 | goto error; | ||
| 1704 | } | ||
| 1705 | } else { | ||
| 1706 | new_locked = true; | ||
| 1707 | } | ||
| 1708 | } | ||
| 1709 | } | ||
| 1710 | |||
| 1711 | if (stat_ok) { | ||
| 1712 | if (absolute != buffer->abs_filename && !force) { | ||
| 1713 | error_msg("Use -f to overwrite %s", absolute); | ||
| 1714 | goto error; | ||
| 1715 | } | ||
| 1716 | // Allow chmod 755 etc. | ||
| 1717 | buffer->file.mode = st.st_mode; | ||
| 1718 | } | ||
| 1719 | |||
| 1720 | if ( | ||
| 1721 | stat_ok | ||
| 1722 | && buffer->options.save_unmodified != SAVE_FULL | ||
| 1723 | && !stat_changed(&buffer->file, &st) | ||
| 1724 | && st.st_uid == buffer->file.uid | ||
| 1725 | && st.st_gid == buffer->file.gid | ||
| 1726 | && !buffer_modified(buffer) | ||
| 1727 | && absolute == buffer->abs_filename | ||
| 1728 | && encoding.name == buffer->encoding.name | ||
| 1729 | && crlf == buffer->crlf_newlines | ||
| 1730 | && bom == buffer->bom | ||
| 1731 | && save_unmodified_buffer(buffer, absolute) | ||
| 1732 | ) { | ||
| 1733 | BUG_ON(new_locked); | ||
| 1734 | return true; | ||
| 1735 | } | ||
| 1736 | |||
| 1737 | if (!save_buffer(buffer, absolute, &encoding, crlf, bom, hardlinks)) { | ||
| 1738 | goto error; | ||
| 1739 | } | ||
| 1740 | |||
| 1741 | buffer->saved_change = buffer->cur_change; | ||
| 1742 | buffer->readonly = false; | ||
| 1743 | buffer->temporary = false; | ||
| 1744 | buffer->crlf_newlines = crlf; | ||
| 1745 | buffer->bom = bom; | ||
| 1746 | if (requested_encoding) { | ||
| 1747 | buffer->encoding = encoding; | ||
| 1748 | } | ||
| 1749 | |||
| 1750 | if (absolute != buffer->abs_filename) { | ||
| 1751 | if (buffer->locked) { | ||
| 1752 | // Filename changes, release old file lock | ||
| 1753 | unlock_file(buffer->abs_filename); | ||
| 1754 | } | ||
| 1755 | buffer->locked = new_locked; | ||
| 1756 | |||
| 1757 | free(buffer->abs_filename); | ||
| 1758 | buffer->abs_filename = absolute; | ||
| 1759 | update_short_filename(buffer, &e->home_dir); | ||
| 1760 | |||
| 1761 | // Filename change is not detected (only buffer_modified() change) | ||
| 1762 | mark_buffer_tabbars_changed(buffer); | ||
| 1763 | } | ||
| 1764 | if (!old_mode && streq(buffer->options.filetype, "none")) { | ||
| 1765 | // New file and most likely user has not changed the filetype | ||
| 1766 | if (buffer_detect_filetype(buffer, &e->filetypes)) { | ||
| 1767 | set_file_options(e, buffer); | ||
| 1768 | set_editorconfig_options(buffer); | ||
| 1769 | buffer_update_syntax(e, buffer); | ||
| 1770 | } | ||
| 1771 | } | ||
| 1772 | |||
| 1773 | return true; | ||
| 1774 | |||
| 1775 | error: | ||
| 1776 | if (new_locked) { | ||
| 1777 | unlock_file(absolute); | ||
| 1778 | } | ||
| 1779 | if (absolute != buffer->abs_filename) { | ||
| 1780 | free(absolute); | ||
| 1781 | } | ||
| 1782 | return false; | ||
| 1783 | } | ||
| 1784 | |||
| 1785 | static bool cmd_scroll_down(EditorState *e, const CommandArgs *a) | ||
| 1786 | { | ||
| 1787 | BUG_ON(a->nr_args); | ||
| 1788 | View *view = e->view; | ||
| 1789 | view->vy++; | ||
| 1790 | if (view->cy < view->vy) { | ||
| 1791 | move_down(view, 1); | ||
| 1792 | } | ||
| 1793 | return true; | ||
| 1794 | } | ||
| 1795 | |||
| 1796 | static bool cmd_scroll_pgdown(EditorState *e, const CommandArgs *a) | ||
| 1797 | { | ||
| 1798 | BUG_ON(a->nr_args); | ||
| 1799 | Window *window = e->window; | ||
| 1800 | View *view = e->view; | ||
| 1801 | long max = view->buffer->nl - window->edit_h + 1; | ||
| 1802 | if (view->vy < max && max > 0) { | ||
| 1803 | long count = window->edit_h - 1; | ||
| 1804 | if (view->vy + count > max) { | ||
| 1805 | count = max - view->vy; | ||
| 1806 | } | ||
| 1807 | view->vy += count; | ||
| 1808 | move_down(view, count); | ||
| 1809 | } else if (view->cy < view->buffer->nl) { | ||
| 1810 | move_down(view, view->buffer->nl - view->cy); | ||
| 1811 | } | ||
| 1812 | return true; | ||
| 1813 | } | ||
| 1814 | |||
| 1815 | static bool cmd_scroll_pgup(EditorState *e, const CommandArgs *a) | ||
| 1816 | { | ||
| 1817 | BUG_ON(a->nr_args); | ||
| 1818 | Window *window = e->window; | ||
| 1819 | View *view = e->view; | ||
| 1820 | if (view->vy > 0) { | ||
| 1821 | long count = MIN(window->edit_h - 1, view->vy); | ||
| 1822 | view->vy -= count; | ||
| 1823 | move_up(view, count); | ||
| 1824 | } else if (view->cy > 0) { | ||
| 1825 | move_up(view, view->cy); | ||
| 1826 | } | ||
| 1827 | return true; | ||
| 1828 | } | ||
| 1829 | |||
| 1830 | static bool cmd_scroll_up(EditorState *e, const CommandArgs *a) | ||
| 1831 | { | ||
| 1832 | BUG_ON(a->nr_args); | ||
| 1833 | Window *window = e->window; | ||
| 1834 | View *view = e->view; | ||
| 1835 | if (view->vy) { | ||
| 1836 | view->vy--; | ||
| 1837 | } | ||
| 1838 | if (view->vy + window->edit_h <= view->cy) { | ||
| 1839 | move_up(view, 1); | ||
| 1840 | } | ||
| 1841 | return true; | ||
| 1842 | } | ||
| 1843 | |||
| 1844 | static uint_least64_t get_flagset_npw(void) | ||
| 1845 | { | ||
| 1846 | uint_least64_t npw = 0; | ||
| 1847 | npw |= cmdargs_flagset_value('n'); | ||
| 1848 | npw |= cmdargs_flagset_value('p'); | ||
| 1849 | npw |= cmdargs_flagset_value('w'); | ||
| 1850 | return npw; | ||
| 1851 | } | ||
| 1852 | |||
| 1853 | static bool cmd_search(EditorState *e, const CommandArgs *a) | ||
| 1854 | { | ||
| 1855 | const char *pattern = a->args[0]; | ||
| 1856 | if (u64_popcount(a->flag_set & get_flagset_npw()) + !!pattern >= 2) { | ||
| 1857 | return error_msg("flags [-n|-p|-w] and [pattern] argument are mutually exclusive"); | ||
| 1858 | } | ||
| 1859 | |||
| 1860 | View *view = e->view; | ||
| 1861 | char pattbuf[4096]; | ||
| 1862 | bool use_word_under_cursor = has_flag(a, 'w'); | ||
| 1863 | |||
| 1864 | if (use_word_under_cursor) { | ||
| 1865 | StringView word = view_get_word_under_cursor(view); | ||
| 1866 | if (word.length == 0) { | ||
| 1867 | // Error message would not be very useful here | ||
| 1868 | return false; | ||
| 1869 | } | ||
| 1870 | const RegexpWordBoundaryTokens *rwbt = &e->regexp_word_tokens; | ||
| 1871 | const size_t bmax = sizeof(rwbt->start); | ||
| 1872 | static_assert_compatible_types(rwbt->start, char[8]); | ||
| 1873 | if (unlikely(word.length >= sizeof(pattbuf) - (bmax * 2))) { | ||
| 1874 | return error_msg("word under cursor too long"); | ||
| 1875 | } | ||
| 1876 | char *ptr = stpncpy(pattbuf, rwbt->start, bmax); | ||
| 1877 | memcpy(ptr, word.data, word.length); | ||
| 1878 | memcpy(ptr + word.length, rwbt->end, bmax); | ||
| 1879 | pattern = pattbuf; | ||
| 1880 | } | ||
| 1881 | |||
| 1882 | SearchState *search = &e->search; | ||
| 1883 | SearchCaseSensitivity cs = e->options.case_sensitive_search; | ||
| 1884 | unselect(view); | ||
| 1885 | |||
| 1886 | if (has_flag(a, 'n')) { | ||
| 1887 | return search_next(view, search, cs); | ||
| 1888 | } | ||
| 1889 | if (has_flag(a, 'p')) { | ||
| 1890 | return search_prev(view, search, cs); | ||
| 1891 | } | ||
| 1892 | |||
| 1893 | search->reverse = has_flag(a, 'r'); | ||
| 1894 | if (!pattern) { | ||
| 1895 | set_input_mode(e, INPUT_SEARCH); | ||
| 1896 | return true; | ||
| 1897 | } | ||
| 1898 | |||
| 1899 | bool found; | ||
| 1900 | search_set_regexp(search, pattern); | ||
| 1901 | if (use_word_under_cursor) { | ||
| 1902 | found = search_next_word(view, search, cs); | ||
| 1903 | } else { | ||
| 1904 | found = search_next(view, search, cs); | ||
| 1905 | } | ||
| 1906 | |||
| 1907 | if (!has_flag(a, 'H')) { | ||
| 1908 | history_add(&e->search_history, pattern); | ||
| 1909 | } | ||
| 1910 | |||
| 1911 | return found; | ||
| 1912 | } | ||
| 1913 | |||
| 1914 | static bool cmd_select_block(EditorState *e, const CommandArgs *a) | ||
| 1915 | { | ||
| 1916 | BUG_ON(a->nr_args); | ||
| 1917 | select_block(e->view); | ||
| 1918 | |||
| 1919 | // TODO: return false if select_block() doesn't select anything? | ||
| 1920 | return true; | ||
| 1921 | } | ||
| 1922 | |||
| 1923 | static bool cmd_select(EditorState *e, const CommandArgs *a) | ||
| 1924 | { | ||
| 1925 | View *view = e->view; | ||
| 1926 | SelectionType sel = has_flag(a, 'l') ? SELECT_LINES : SELECT_CHARS; | ||
| 1927 | bool keep = has_flag(a, 'k'); | ||
| 1928 | if (!keep && view->selection && view->selection == sel) { | ||
| 1929 | sel = SELECT_NONE; | ||
| 1930 | } | ||
| 1931 | |||
| 1932 | view->select_mode = sel; | ||
| 1933 | do_selection(view, sel); | ||
| 1934 | return true; | ||
| 1935 | } | ||
| 1936 | |||
| 1937 | static bool cmd_set(EditorState *e, const CommandArgs *a) | ||
| 1938 | { | ||
| 1939 | bool global = has_flag(a, 'g'); | ||
| 1940 | bool local = has_flag(a, 'l'); | ||
| 1941 | if (!e->buffer) { | ||
| 1942 | if (unlikely(local)) { | ||
| 1943 | return error_msg("Flag -l makes no sense in config file"); | ||
| 1944 | } | ||
| 1945 | global = true; | ||
| 1946 | } | ||
| 1947 | |||
| 1948 | char **args = a->args; | ||
| 1949 | size_t count = a->nr_args; | ||
| 1950 | if (count == 1) { | ||
| 1951 | return set_bool_option(e, args[0], local, global); | ||
| 1952 | } | ||
| 1953 | if (count & 1) { | ||
| 1954 | return error_msg("One or even number of arguments expected"); | ||
| 1955 | } | ||
| 1956 | |||
| 1957 | size_t errors = 0; | ||
| 1958 | for (size_t i = 0; i < count; i += 2) { | ||
| 1959 | if (!set_option(e, args[i], args[i + 1], local, global)) { | ||
| 1960 | errors++; | ||
| 1961 | } | ||
| 1962 | } | ||
| 1963 | |||
| 1964 | return !errors; | ||
| 1965 | } | ||
| 1966 | |||
| 1967 | static bool cmd_setenv(EditorState* UNUSED_ARG(e), const CommandArgs *a) | ||
| 1968 | { | ||
| 1969 | const char *name = a->args[0]; | ||
| 1970 | if (unlikely(streq(name, "DTE_VERSION"))) { | ||
| 1971 | return error_msg("$DTE_VERSION cannot be changed"); | ||
| 1972 | } | ||
| 1973 | |||
| 1974 | const size_t nr_args = a->nr_args; | ||
| 1975 | int res; | ||
| 1976 | if (nr_args == 2) { | ||
| 1977 | res = setenv(name, a->args[1], true); | ||
| 1978 | } else { | ||
| 1979 | BUG_ON(nr_args != 1); | ||
| 1980 | res = unsetenv(name); | ||
| 1981 | } | ||
| 1982 | |||
| 1983 | if (likely(res == 0)) { | ||
| 1984 | return true; | ||
| 1985 | } | ||
| 1986 | |||
| 1987 | if (errno == EINVAL) { | ||
| 1988 | return error_msg("Invalid environment variable name '%s'", name); | ||
| 1989 | } | ||
| 1990 | |||
| 1991 | return error_msg_errno(nr_args == 2 ? "setenv" : "unsetenv"); | ||
| 1992 | } | ||
| 1993 | |||
| 1994 | static bool cmd_shift(EditorState *e, const CommandArgs *a) | ||
| 1995 | { | ||
| 1996 | const char *arg = a->args[0]; | ||
| 1997 | int count; | ||
| 1998 | if (!str_to_int(arg, &count)) { | ||
| 1999 | return error_msg("Invalid number: %s", arg); | ||
| 2000 | } | ||
| 2001 | if (count == 0) { | ||
| 2002 | return error_msg("Count must be non-zero"); | ||
| 2003 | } | ||
| 2004 | shift_lines(e->view, count); | ||
| 2005 | return true; | ||
| 2006 | } | ||
| 2007 | |||
| 2008 | static bool cmd_show(EditorState *e, const CommandArgs *a) | ||
| 2009 | { | ||
| 2010 | bool write_to_cmdline = has_flag(a, 'c'); | ||
| 2011 | if (write_to_cmdline && a->nr_args < 2) { | ||
| 2012 | return error_msg("\"show -c\" requires 2 arguments"); | ||
| 2013 | } | ||
| 2014 | return show(e, a->args[0], a->args[1], write_to_cmdline); | ||
| 2015 | } | ||
| 2016 | |||
| 2017 | static bool cmd_suspend(EditorState *e, const CommandArgs *a) | ||
| 2018 | { | ||
| 2019 | BUG_ON(a->nr_args); | ||
| 2020 | if (e->status == EDITOR_INITIALIZING) { | ||
| 2021 | LOG_WARNING("suspend request ignored"); | ||
| 2022 | return false; | ||
| 2023 | } | ||
| 2024 | |||
| 2025 | if (e->session_leader) { | ||
| 2026 | return error_msg("Session leader can't suspend"); | ||
| 2027 | } | ||
| 2028 | |||
| 2029 | ui_end(e); | ||
| 2030 | bool suspended = !kill(0, SIGSTOP); | ||
| 2031 | if (!suspended) { | ||
| 2032 | error_msg_errno("kill"); | ||
| 2033 | } | ||
| 2034 | |||
| 2035 | term_raw(); | ||
| 2036 | ui_start(e); | ||
| 2037 | return suspended; | ||
| 2038 | } | ||
| 2039 | |||
| 2040 | static bool cmd_tag(EditorState *e, const CommandArgs *a) | ||
| 2041 | { | ||
| 2042 | if (has_flag(a, 'r')) { | ||
| 2043 | bookmark_pop(e->window, &e->bookmarks); | ||
| 2044 | return true; | ||
| 2045 | } | ||
| 2046 | |||
| 2047 | StringView name; | ||
| 2048 | if (a->args[0]) { | ||
| 2049 | name = strview_from_cstring(a->args[0]); | ||
| 2050 | } else { | ||
| 2051 | name = view_get_word_under_cursor(e->view); | ||
| 2052 | if (name.length == 0) { | ||
| 2053 | return false; | ||
| 2054 | } | ||
| 2055 | } | ||
| 2056 | |||
| 2057 | const char *filename = e->buffer->abs_filename; | ||
| 2058 | size_t ntags = tag_lookup(&e->tagfile, &name, filename, &e->messages); | ||
| 2059 | activate_current_message_save(e); | ||
| 2060 | return (ntags > 0); | ||
| 2061 | } | ||
| 2062 | |||
| 2063 | static bool cmd_title(EditorState *e, const CommandArgs *a) | ||
| 2064 | { | ||
| 2065 | Buffer *buffer = e->buffer; | ||
| 2066 | if (buffer->abs_filename) { | ||
| 2067 | return error_msg("saved buffers can't be retitled"); | ||
| 2068 | } | ||
| 2069 | set_display_filename(buffer, xstrdup(a->args[0])); | ||
| 2070 | mark_buffer_tabbars_changed(buffer); | ||
| 2071 | return true; | ||
| 2072 | } | ||
| 2073 | |||
| 2074 | static bool cmd_toggle(EditorState *e, const CommandArgs *a) | ||
| 2075 | { | ||
| 2076 | bool global = has_flag(a, 'g'); | ||
| 2077 | bool verbose = has_flag(a, 'v'); | ||
| 2078 | const char *option_name = a->args[0]; | ||
| 2079 | size_t nr_values = a->nr_args - 1; | ||
| 2080 | if (nr_values == 0) { | ||
| 2081 | return toggle_option(e, option_name, global, verbose); | ||
| 2082 | } | ||
| 2083 | |||
| 2084 | char **values = a->args + 1; | ||
| 2085 | return toggle_option_values(e, option_name, global, verbose, values, nr_values); | ||
| 2086 | } | ||
| 2087 | |||
| 2088 | static bool cmd_undo(EditorState *e, const CommandArgs *a) | ||
| 2089 | { | ||
| 2090 | View *view = e->view; | ||
| 2091 | bool move_only = has_flag(a, 'm'); | ||
| 2092 | if (move_only) { | ||
| 2093 | const Change *change = view->buffer->cur_change; | ||
| 2094 | if (!change->next) { | ||
| 2095 | // If there's only 1 change, there's nothing meaningful to move to | ||
| 2096 | return false; | ||
| 2097 | } | ||
| 2098 | block_iter_goto_offset(&view->cursor, change->offset); | ||
| 2099 | view_reset_preferred_x(view); | ||
| 2100 | return true; | ||
| 2101 | } | ||
| 2102 | |||
| 2103 | if (!undo(view)) { | ||
| 2104 | return false; | ||
| 2105 | } | ||
| 2106 | |||
| 2107 | unselect(view); | ||
| 2108 | return true; | ||
| 2109 | } | ||
| 2110 | |||
| 2111 | static bool cmd_unselect(EditorState *e, const CommandArgs *a) | ||
| 2112 | { | ||
| 2113 | BUG_ON(a->nr_args); | ||
| 2114 | unselect(e->view); | ||
| 2115 | return true; | ||
| 2116 | } | ||
| 2117 | |||
| 2118 | static bool cmd_up(EditorState *e, const CommandArgs *a) | ||
| 2119 | { | ||
| 2120 | handle_select_chars_or_lines_flags(e->view, a); | ||
| 2121 | move_up(e->view, 1); | ||
| 2122 | return true; | ||
| 2123 | } | ||
| 2124 | |||
| 2125 | static bool cmd_view(EditorState *e, const CommandArgs *a) | ||
| 2126 | { | ||
| 2127 | Window *window = e->window; | ||
| 2128 | BUG_ON(window->views.count == 0); | ||
| 2129 | const char *arg = a->args[0]; | ||
| 2130 | size_t idx; | ||
| 2131 | if (streq(arg, "last")) { | ||
| 2132 | idx = window->views.count - 1; | ||
| 2133 | } else { | ||
| 2134 | if (!str_to_size(arg, &idx) || idx == 0) { | ||
| 2135 | return error_msg("Invalid view index: %s", arg); | ||
| 2136 | } | ||
| 2137 | idx = MIN(idx, window->views.count) - 1; | ||
| 2138 | } | ||
| 2139 | set_view(window->views.ptrs[idx]); | ||
| 2140 | return true; | ||
| 2141 | } | ||
| 2142 | |||
| 2143 | static bool cmd_wclose(EditorState *e, const CommandArgs *a) | ||
| 2144 | { | ||
| 2145 | View *view = window_find_unclosable_view(e->window); | ||
| 2146 | bool force = has_flag(a, 'f'); | ||
| 2147 | if (!view || force) { | ||
| 2148 | goto close; | ||
| 2149 | } | ||
| 2150 | |||
| 2151 | bool prompt = has_flag(a, 'p'); | ||
| 2152 | set_view(view); | ||
| 2153 | if (!prompt) { | ||
| 2154 | return error_msg ( | ||
| 2155 | "Save modified files or run 'wclose -f' to close " | ||
| 2156 | "window without saving" | ||
| 2157 | ); | ||
| 2158 | } | ||
| 2159 | |||
| 2160 | if (dialog_prompt(e, "Close window without saving? [y/N]", "ny") != 'y') { | ||
| 2161 | return false; | ||
| 2162 | } | ||
| 2163 | |||
| 2164 | close: | ||
| 2165 | window_close(e->window); | ||
| 2166 | return true; | ||
| 2167 | } | ||
| 2168 | |||
| 2169 | static bool cmd_wflip(EditorState *e, const CommandArgs *a) | ||
| 2170 | { | ||
| 2171 | BUG_ON(a->nr_args); | ||
| 2172 | Frame *frame = e->window->frame; | ||
| 2173 | if (!frame->parent) { | ||
| 2174 | return false; | ||
| 2175 | } | ||
| 2176 | frame->parent->vertical ^= 1; | ||
| 2177 | mark_everything_changed(e); | ||
| 2178 | return true; | ||
| 2179 | } | ||
| 2180 | |||
| 2181 | static bool cmd_wnext(EditorState *e, const CommandArgs *a) | ||
| 2182 | { | ||
| 2183 | BUG_ON(a->nr_args); | ||
| 2184 | e->window = next_window(e->window); | ||
| 2185 | set_view(e->window->view); | ||
| 2186 | mark_everything_changed(e); | ||
| 2187 | debug_frame(e->root_frame); | ||
| 2188 | return true; | ||
| 2189 | } | ||
| 2190 | |||
| 2191 | static bool cmd_word_bwd(EditorState *e, const CommandArgs *a) | ||
| 2192 | { | ||
| 2193 | handle_select_chars_flag(e->view, a); | ||
| 2194 | bool skip_non_word = has_flag(a, 's'); | ||
| 2195 | word_bwd(&e->view->cursor, skip_non_word); | ||
| 2196 | view_reset_preferred_x(e->view); | ||
| 2197 | return true; | ||
| 2198 | } | ||
| 2199 | |||
| 2200 | static bool cmd_word_fwd(EditorState *e, const CommandArgs *a) | ||
| 2201 | { | ||
| 2202 | handle_select_chars_flag(e->view, a); | ||
| 2203 | bool skip_non_word = has_flag(a, 's'); | ||
| 2204 | word_fwd(&e->view->cursor, skip_non_word); | ||
| 2205 | view_reset_preferred_x(e->view); | ||
| 2206 | return true; | ||
| 2207 | } | ||
| 2208 | |||
| 2209 | static bool cmd_wprev(EditorState *e, const CommandArgs *a) | ||
| 2210 | { | ||
| 2211 | BUG_ON(a->nr_args); | ||
| 2212 | e->window = prev_window(e->window); | ||
| 2213 | set_view(e->window->view); | ||
| 2214 | mark_everything_changed(e); | ||
| 2215 | debug_frame(e->root_frame); | ||
| 2216 | return true; | ||
| 2217 | } | ||
| 2218 | |||
| 2219 | static bool cmd_wrap_paragraph(EditorState *e, const CommandArgs *a) | ||
| 2220 | { | ||
| 2221 | const char *arg = a->args[0]; | ||
| 2222 | unsigned int width = e->buffer->options.text_width; | ||
| 2223 | if (arg) { | ||
| 2224 | if (!str_to_uint(arg, &width)) { | ||
| 2225 | return error_msg("invalid paragraph width: %s", arg); | ||
| 2226 | } | ||
| 2227 | unsigned int max = TEXT_WIDTH_MAX; | ||
| 2228 | if (width < 1 || width > max) { | ||
| 2229 | return error_msg("width must be between 1 and %u", max); | ||
| 2230 | } | ||
| 2231 | } | ||
| 2232 | format_paragraph(e->view, width); | ||
| 2233 | return true; | ||
| 2234 | } | ||
| 2235 | |||
| 2236 | static bool cmd_wresize(EditorState *e, const CommandArgs *a) | ||
| 2237 | { | ||
| 2238 | Window *window = e->window; | ||
| 2239 | if (!window->frame->parent) { | ||
| 2240 | // Only window | ||
| 2241 | return false; | ||
| 2242 | } | ||
| 2243 | |||
| 2244 | ResizeDirection dir = RESIZE_DIRECTION_AUTO; | ||
| 2245 | switch (last_flag(a)) { | ||
| 2246 | case 'h': | ||
| 2247 | dir = RESIZE_DIRECTION_HORIZONTAL; | ||
| 2248 | break; | ||
| 2249 | case 'v': | ||
| 2250 | dir = RESIZE_DIRECTION_VERTICAL; | ||
| 2251 | break; | ||
| 2252 | } | ||
| 2253 | |||
| 2254 | const char *arg = a->args[0]; | ||
| 2255 | if (arg) { | ||
| 2256 | int n; | ||
| 2257 | if (!str_to_int(arg, &n)) { | ||
| 2258 | return error_msg("Invalid resize value: %s", arg); | ||
| 2259 | } | ||
| 2260 | if (arg[0] == '+' || arg[0] == '-') { | ||
| 2261 | add_to_frame_size(window->frame, dir, n); | ||
| 2262 | } else { | ||
| 2263 | resize_frame(window->frame, dir, n); | ||
| 2264 | } | ||
| 2265 | } else { | ||
| 2266 | equalize_frame_sizes(window->frame->parent); | ||
| 2267 | } | ||
| 2268 | |||
| 2269 | mark_everything_changed(e); | ||
| 2270 | debug_frame(e->root_frame); | ||
| 2271 | // TODO: return false if resize failed? | ||
| 2272 | return true; | ||
| 2273 | } | ||
| 2274 | |||
| 2275 | static bool cmd_wsplit(EditorState *e, const CommandArgs *a) | ||
| 2276 | { | ||
| 2277 | bool before = has_flag(a, 'b'); | ||
| 2278 | bool use_glob = has_flag(a, 'g') && a->nr_args > 0; | ||
| 2279 | bool vertical = has_flag(a, 'h'); | ||
| 2280 | bool root = has_flag(a, 'r'); | ||
| 2281 | bool temporary = has_flag(a, 't'); | ||
| 2282 | bool empty = temporary || has_flag(a, 'n'); | ||
| 2283 | |||
| 2284 | if (unlikely(empty && a->nr_args > 0)) { | ||
| 2285 | return error_msg("flags -n and -t can't be used with filename arguments"); | ||
| 2286 | } | ||
| 2287 | |||
| 2288 | char **paths = a->args; | ||
| 2289 | glob_t globbuf; | ||
| 2290 | if (use_glob) { | ||
| 2291 | if (!xglob(a->args, &globbuf)) { | ||
| 2292 | return false; | ||
| 2293 | } | ||
| 2294 | paths = globbuf.gl_pathv; | ||
| 2295 | } | ||
| 2296 | |||
| 2297 | Frame *frame; | ||
| 2298 | if (root) { | ||
| 2299 | frame = split_root_frame(e, vertical, before); | ||
| 2300 | } else { | ||
| 2301 | frame = split_frame(e->window, vertical, before); | ||
| 2302 | } | ||
| 2303 | |||
| 2304 | View *save = e->view; | ||
| 2305 | e->window = frame->window; | ||
| 2306 | e->view = NULL; | ||
| 2307 | e->buffer = NULL; | ||
| 2308 | mark_everything_changed(e); | ||
| 2309 | |||
| 2310 | View *view; | ||
| 2311 | if (empty) { | ||
| 2312 | view = window_open_new_file(e->window); | ||
| 2313 | view->buffer->temporary = temporary; | ||
| 2314 | } else if (paths[0]) { | ||
| 2315 | view = window_open_files(e->window, paths, NULL); | ||
| 2316 | } else { | ||
| 2317 | view = window_add_buffer(e->window, save->buffer); | ||
| 2318 | view->cursor = save->cursor; | ||
| 2319 | set_view(view); | ||
| 2320 | } | ||
| 2321 | |||
| 2322 | if (use_glob) { | ||
| 2323 | globfree(&globbuf); | ||
| 2324 | } | ||
| 2325 | |||
| 2326 | if (!view) { | ||
| 2327 | // Open failed, remove new window | ||
| 2328 | remove_frame(e, e->window->frame); | ||
| 2329 | e->view = save; | ||
| 2330 | e->buffer = save->buffer; | ||
| 2331 | e->window = save->window; | ||
| 2332 | } | ||
| 2333 | |||
| 2334 | debug_frame(e->root_frame); | ||
| 2335 | return !!view; | ||
| 2336 | } | ||
| 2337 | |||
| 2338 | static bool cmd_wswap(EditorState *e, const CommandArgs *a) | ||
| 2339 | { | ||
| 2340 | BUG_ON(a->nr_args); | ||
| 2341 | Frame *frame = e->window->frame; | ||
| 2342 | Frame *parent = frame->parent; | ||
| 2343 | if (!parent) { | ||
| 2344 | return false; | ||
| 2345 | } | ||
| 2346 | |||
| 2347 | size_t count = parent->frames.count; | ||
| 2348 | size_t current = ptr_array_idx(&parent->frames, frame); | ||
| 2349 | BUG_ON(current >= count); | ||
| 2350 | size_t next = size_increment_wrapped(current, count); | ||
| 2351 | |||
| 2352 | void **ptrs = parent->frames.ptrs; | ||
| 2353 | Frame *tmp = ptrs[current]; | ||
| 2354 | ptrs[current] = ptrs[next]; | ||
| 2355 | ptrs[next] = tmp; | ||
| 2356 | mark_everything_changed(e); | ||
| 2357 | return true; | ||
| 2358 | } | ||
| 2359 | |||
| 2360 | IGNORE_WARNING("-Wincompatible-pointer-types") | ||
| 2361 | |||
| 2362 | static const Command cmds[] = { | ||
| 2363 | {"alias", "-", true, 1, 2, cmd_alias}, | ||
| 2364 | {"bind", "-cns", true, 1, 2, cmd_bind}, | ||
| 2365 | {"blkdown", "cl", false, 0, 0, cmd_blkdown}, | ||
| 2366 | {"blkup", "cl", false, 0, 0, cmd_blkup}, | ||
| 2367 | {"bof", "cl", false, 0, 0, cmd_bof}, | ||
| 2368 | {"bol", "cst", false, 0, 0, cmd_bol}, | ||
| 2369 | {"bolsf", "cl", false, 0, 0, cmd_bolsf}, | ||
| 2370 | {"bookmark", "r", false, 0, 0, cmd_bookmark}, | ||
| 2371 | {"case", "lu", false, 0, 0, cmd_case}, | ||
| 2372 | {"cd", "", true, 1, 1, cmd_cd}, | ||
| 2373 | {"center-view", "", false, 0, 0, cmd_center_view}, | ||
| 2374 | {"clear", "i", false, 0, 0, cmd_clear}, | ||
| 2375 | {"close", "fpqw", false, 0, 0, cmd_close}, | ||
| 2376 | {"command", "-", false, 0, 1, cmd_command}, | ||
| 2377 | {"compile", "-1ps", false, 2, -1, cmd_compile}, | ||
| 2378 | {"copy", "bikp", false, 0, 0, cmd_copy}, | ||
| 2379 | {"cursor", "", true, 0, 3, cmd_cursor}, | ||
| 2380 | {"cut", "", false, 0, 0, cmd_cut}, | ||
| 2381 | {"delete", "", false, 0, 0, cmd_delete}, | ||
| 2382 | {"delete-eol", "n", false, 0, 0, cmd_delete_eol}, | ||
| 2383 | {"delete-line", "", false, 0, 0, cmd_delete_line}, | ||
| 2384 | {"delete-word", "s", false, 0, 0, cmd_delete_word}, | ||
| 2385 | {"down", "cl", false, 0, 0, cmd_down}, | ||
| 2386 | {"eof", "cl", false, 0, 0, cmd_eof}, | ||
| 2387 | {"eol", "c", false, 0, 0, cmd_eol}, | ||
| 2388 | {"eolsf", "cl", false, 0, 0, cmd_eolsf}, | ||
| 2389 | {"erase", "", false, 0, 0, cmd_erase}, | ||
| 2390 | {"erase-bol", "", false, 0, 0, cmd_erase_bol}, | ||
| 2391 | {"erase-word", "s", false, 0, 0, cmd_erase_word}, | ||
| 2392 | {"errorfmt", "i", true, 1, 2 + ERRORFMT_CAPTURE_MAX, cmd_errorfmt}, | ||
| 2393 | {"exec", "-e=i=o=lmnpst", false, 1, -1, cmd_exec}, | ||
| 2394 | {"ft", "-bcfi", true, 2, -1, cmd_ft}, | ||
| 2395 | {"hi", "-c", true, 0, -1, cmd_hi}, | ||
| 2396 | {"include", "bq", true, 1, 1, cmd_include}, | ||
| 2397 | {"insert", "km", false, 1, 1, cmd_insert}, | ||
| 2398 | {"join", "", false, 0, 0, cmd_join}, | ||
| 2399 | {"left", "c", false, 0, 0, cmd_left}, | ||
| 2400 | {"line", "", false, 1, 1, cmd_line}, | ||
| 2401 | {"load-syntax", "", true, 1, 1, cmd_load_syntax}, | ||
| 2402 | {"macro", "", false, 1, 1, cmd_macro}, | ||
| 2403 | {"match-bracket", "", false, 0, 0, cmd_match_bracket}, | ||
| 2404 | {"move-tab", "", false, 1, 1, cmd_move_tab}, | ||
| 2405 | {"msg", "np", false, 0, 1, cmd_msg}, | ||
| 2406 | {"new-line", "a", false, 0, 0, cmd_new_line}, | ||
| 2407 | {"next", "", false, 0, 0, cmd_next}, | ||
| 2408 | {"open", "e=gt", false, 0, -1, cmd_open}, | ||
| 2409 | {"option", "-r", true, 3, -1, cmd_option}, | ||
| 2410 | {"paste", "acm", false, 0, 0, cmd_paste}, | ||
| 2411 | {"pgdown", "cl", false, 0, 0, cmd_pgdown}, | ||
| 2412 | {"pgup", "cl", false, 0, 0, cmd_pgup}, | ||
| 2413 | {"prev", "", false, 0, 0, cmd_prev}, | ||
| 2414 | {"quit", "fp", false, 0, 1, cmd_quit}, | ||
| 2415 | {"redo", "", false, 0, 1, cmd_redo}, | ||
| 2416 | {"refresh", "", false, 0, 0, cmd_refresh}, | ||
| 2417 | {"repeat", "-", false, 2, -1, cmd_repeat}, | ||
| 2418 | {"replace", "bcgi", false, 2, 2, cmd_replace}, | ||
| 2419 | {"right", "c", false, 0, 0, cmd_right}, | ||
| 2420 | {"save", "Bbde=fpu", false, 0, 1, cmd_save}, | ||
| 2421 | {"scroll-down", "", false, 0, 0, cmd_scroll_down}, | ||
| 2422 | {"scroll-pgdown", "", false, 0, 0, cmd_scroll_pgdown}, | ||
| 2423 | {"scroll-pgup", "", false, 0, 0, cmd_scroll_pgup}, | ||
| 2424 | {"scroll-up", "", false, 0, 0, cmd_scroll_up}, | ||
| 2425 | {"search", "Hnprw", false, 0, 1, cmd_search}, | ||
| 2426 | {"select", "kl", false, 0, 0, cmd_select}, | ||
| 2427 | {"select-block", "", false, 0, 0, cmd_select_block}, | ||
| 2428 | {"set", "gl", true, 1, -1, cmd_set}, | ||
| 2429 | {"setenv", "", true, 1, 2, cmd_setenv}, | ||
| 2430 | {"shift", "", false, 1, 1, cmd_shift}, | ||
| 2431 | {"show", "c", false, 1, 2, cmd_show}, | ||
| 2432 | {"suspend", "", false, 0, 0, cmd_suspend}, | ||
| 2433 | {"tag", "r", false, 0, 1, cmd_tag}, | ||
| 2434 | {"title", "", false, 1, 1, cmd_title}, | ||
| 2435 | {"toggle", "gv", false, 1, -1, cmd_toggle}, | ||
| 2436 | {"undo", "m", false, 0, 0, cmd_undo}, | ||
| 2437 | {"unselect", "", false, 0, 0, cmd_unselect}, | ||
| 2438 | {"up", "cl", false, 0, 0, cmd_up}, | ||
| 2439 | {"view", "", false, 1, 1, cmd_view}, | ||
| 2440 | {"wclose", "fp", false, 0, 0, cmd_wclose}, | ||
| 2441 | {"wflip", "", false, 0, 0, cmd_wflip}, | ||
| 2442 | {"wnext", "", false, 0, 0, cmd_wnext}, | ||
| 2443 | {"word-bwd", "cs", false, 0, 0, cmd_word_bwd}, | ||
| 2444 | {"word-fwd", "cs", false, 0, 0, cmd_word_fwd}, | ||
| 2445 | {"wprev", "", false, 0, 0, cmd_wprev}, | ||
| 2446 | {"wrap-paragraph", "", false, 0, 1, cmd_wrap_paragraph}, | ||
| 2447 | {"wresize", "hv", false, 0, 1, cmd_wresize}, | ||
| 2448 | {"wsplit", "bghnrt", false, 0, -1, cmd_wsplit}, | ||
| 2449 | {"wswap", "", false, 0, 0, cmd_wswap}, | ||
| 2450 | }; | ||
| 2451 | |||
| 2452 | UNIGNORE_WARNINGS | ||
| 2453 | |||
| 2454 | static bool allow_macro_recording(const Command *cmd, char **args) | ||
| 2455 | { | ||
| 2456 | CommandFunc fn = cmd->cmd; | ||
| 2457 | if (fn == (CommandFunc)cmd_macro || fn == (CommandFunc)cmd_command) { | ||
| 2458 | return false; | ||
| 2459 | } | ||
| 2460 | |||
| 2461 | if (fn == (CommandFunc)cmd_search) { | ||
| 2462 | char **args_copy = copy_string_array(args, string_array_length(args)); | ||
| 2463 | CommandArgs a = cmdargs_new(args_copy); | ||
| 2464 | bool ret = true; | ||
| 2465 | if (do_parse_args(cmd, &a) == ARGERR_NONE) { | ||
| 2466 | if (a.nr_args == 0 && !(a.flag_set & get_flagset_npw())) { | ||
| 2467 | // If command is "search" with no pattern argument and without | ||
| 2468 | // flags -n, -p or -w, the command would put the editor into | ||
| 2469 | // search mode, which shouldn't be recorded. | ||
| 2470 | ret = false; | ||
| 2471 | } | ||
| 2472 | } | ||
| 2473 | free_string_array(args_copy); | ||
| 2474 | return ret; | ||
| 2475 | } | ||
| 2476 | |||
| 2477 | if (fn == (CommandFunc)cmd_exec) { | ||
| 2478 | // TODO: don't record -o with open/tag/eval/msg | ||
| 2479 | } | ||
| 2480 | |||
| 2481 | return true; | ||
| 2482 | } | ||
| 2483 | |||
| 2484 | UNITTEST { | ||
| 2485 | const char *args[4] = {NULL}; | ||
| 2486 | char **argp = (char**)args; | ||
| 2487 | const Command *cmd = find_normal_command("left"); | ||
| 2488 | BUG_ON(!cmd); | ||
| 2489 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2490 | |||
| 2491 | cmd = find_normal_command("exec"); | ||
| 2492 | BUG_ON(!cmd); | ||
| 2493 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2494 | |||
| 2495 | cmd = find_normal_command("command"); | ||
| 2496 | BUG_ON(!cmd); | ||
| 2497 | BUG_ON(allow_macro_recording(cmd, argp)); | ||
| 2498 | |||
| 2499 | cmd = find_normal_command("macro"); | ||
| 2500 | BUG_ON(!cmd); | ||
| 2501 | BUG_ON(allow_macro_recording(cmd, argp)); | ||
| 2502 | |||
| 2503 | cmd = find_normal_command("search"); | ||
| 2504 | BUG_ON(!cmd); | ||
| 2505 | BUG_ON(allow_macro_recording(cmd, argp)); | ||
| 2506 | args[0] = "xyz"; | ||
| 2507 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2508 | args[0] = "-n"; | ||
| 2509 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2510 | args[0] = "-p"; | ||
| 2511 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2512 | args[0] = "-w"; | ||
| 2513 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2514 | args[0] = "-Hr"; | ||
| 2515 | BUG_ON(allow_macro_recording(cmd, argp)); | ||
| 2516 | args[1] = "str"; | ||
| 2517 | BUG_ON(!allow_macro_recording(cmd, argp)); | ||
| 2518 | } | ||
| 2519 | |||
| 2520 | static void record_command(const Command *cmd, char **args, void *userdata) | ||
| 2521 | { | ||
| 2522 | if (!allow_macro_recording(cmd, args)) { | ||
| 2523 | return; | ||
| 2524 | } | ||
| 2525 | EditorState *e = userdata; | ||
| 2526 | macro_command_hook(&e->macro, cmd->name, args); | ||
| 2527 | } | ||
| 2528 | |||
| 2529 | const Command *find_normal_command(const char *name) | ||
| 2530 | { | ||
| 2531 | return BSEARCH(name, cmds, command_cmp); | ||
| 2532 | } | ||
| 2533 | |||
| 2534 | const CommandSet normal_commands = { | ||
| 2535 | .lookup = find_normal_command, | ||
| 2536 | .macro_record = record_command, | ||
| 2537 | .expand_variable = expand_normal_var, | ||
| 2538 | .expand_env_vars = true, | ||
| 2539 | }; | ||
| 2540 | |||
| 2541 | const char *find_normal_alias(const char *name, void *userdata) | ||
| 2542 | { | ||
| 2543 | EditorState *e = userdata; | ||
| 2544 | return find_alias(&e->aliases, name); | ||
| 2545 | } | ||
| 2546 | |||
| 2547 | bool handle_normal_command(EditorState *e, const char *cmd, bool allow_recording) | ||
| 2548 | { | ||
| 2549 | CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, allow_recording); | ||
| 2550 | return handle_command(&runner, cmd); | ||
| 2551 | } | ||
| 2552 | |||
| 2553 | void exec_normal_config(EditorState *e, StringView config) | ||
| 2554 | { | ||
| 2555 | CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); | ||
| 2556 | exec_config(&runner, config); | ||
| 2557 | } | ||
| 2558 | |||
| 2559 | int read_normal_config(EditorState *e, const char *filename, ConfigFlags flags) | ||
| 2560 | { | ||
| 2561 | CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); | ||
| 2562 | return read_config(&runner, filename, flags); | ||
| 2563 | } | ||
| 2564 | |||
| 2565 | void collect_normal_commands(PointerArray *a, const char *prefix) | ||
| 2566 | { | ||
| 2567 | COLLECT_STRING_FIELDS(cmds, name, a, prefix); | ||
| 2568 | } | ||
| 2569 | |||
| 2570 | UNITTEST { | ||
| 2571 | CHECK_BSEARCH_ARRAY(cmds, name, strcmp); | ||
| 2572 | |||
| 2573 | for (size_t i = 0, n = ARRAYLEN(cmds); i < n; i++) { | ||
| 2574 | // Check that flags arrays is null-terminated within bounds | ||
| 2575 | const char *const flags = cmds[i].flags; | ||
| 2576 | BUG_ON(flags[ARRAYLEN(cmds[0].flags) - 1] != '\0'); | ||
| 2577 | |||
| 2578 | // Count number of real flags (i.e. not including '-' or '=') | ||
| 2579 | size_t nr_real_flags = 0; | ||
| 2580 | for (size_t j = (flags[0] == '-' ? 1 : 0); flags[j]; j++) { | ||
| 2581 | unsigned char flag = flags[j]; | ||
| 2582 | if (ascii_isalnum(flag)) { | ||
| 2583 | nr_real_flags++; | ||
| 2584 | } else if (flag != '=') { | ||
| 2585 | BUG("invalid command flag: 0x%02hhX", flag); | ||
| 2586 | } | ||
| 2587 | } | ||
| 2588 | |||
| 2589 | // Check that max. number of real flags fits in CommandArgs::flags | ||
| 2590 | // array (and also leaves 1 byte for null-terminator) | ||
| 2591 | CommandArgs a; | ||
| 2592 | BUG_ON(nr_real_flags >= ARRAYLEN(a.flags)); | ||
| 2593 | } | ||
| 2594 | } | ||
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 @@ | |||
| 1 | #ifndef COMMANDS_H | ||
| 2 | #define COMMANDS_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "command/run.h" | ||
| 6 | #include "config.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | #include "util/ptr-array.h" | ||
| 9 | #include "util/string-view.h" | ||
| 10 | |||
| 11 | extern const CommandSet normal_commands; | ||
| 12 | |||
| 13 | struct EditorState; | ||
| 14 | |||
| 15 | const Command *find_normal_command(const char *name) NONNULL_ARGS; | ||
| 16 | const char *find_normal_alias(const char *name, void *userdata) NONNULL_ARGS; | ||
| 17 | bool handle_normal_command(struct EditorState *e, const char *cmd, bool allow_recording) NONNULL_ARGS; | ||
| 18 | void exec_normal_config(struct EditorState *e, StringView config) NONNULL_ARGS; | ||
| 19 | int read_normal_config(struct EditorState *e, const char *filename, ConfigFlags flags) NONNULL_ARGS; | ||
| 20 | void collect_normal_commands(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 21 | |||
| 22 | #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 @@ | |||
| 1 | #include "compat.h" | ||
| 2 | |||
| 3 | const char feature_string[] = | ||
| 4 | "" | ||
| 5 | #if HAVE_DUP3 | ||
| 6 | " dup3" | ||
| 7 | #endif | ||
| 8 | #if HAVE_PIPE2 | ||
| 9 | " pipe2" | ||
| 10 | #endif | ||
| 11 | #if HAVE_FSYNC | ||
| 12 | " fsync" | ||
| 13 | #endif | ||
| 14 | #if HAVE_MEMMEM | ||
| 15 | " memmem" | ||
| 16 | #endif | ||
| 17 | #if HAVE_SIG2STR | ||
| 18 | " sig2str" | ||
| 19 | #endif | ||
| 20 | #if HAVE_SIGABBREV_NP && !HAVE_SIG2STR | ||
| 21 | " sigabbrev_np" | ||
| 22 | #endif | ||
| 23 | #if HAVE_TIOCGWINSZ | ||
| 24 | " TIOCGWINSZ" | ||
| 25 | #endif | ||
| 26 | #if HAVE_TCGETWINSIZE && !HAVE_TIOCGWINSZ | ||
| 27 | " tcgetwinsize" | ||
| 28 | #endif | ||
| 29 | #if HAVE_TIOCNOTTY | ||
| 30 | " TIOCNOTTY" | ||
| 31 | #endif | ||
| 32 | #if HAVE_POSIX_MADVISE | ||
| 33 | " posix_madvise" | ||
| 34 | #endif | ||
| 35 | ; | ||
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 @@ | |||
| 1 | #ifndef COMPAT_H | ||
| 2 | #define COMPAT_H | ||
| 3 | |||
| 4 | #include "../build/feature.h" | ||
| 5 | |||
| 6 | extern const char feature_string[]; | ||
| 7 | |||
| 8 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "compiler.h" | ||
| 4 | #include "command/serialize.h" | ||
| 5 | #include "error.h" | ||
| 6 | #include "regexp.h" | ||
| 7 | #include "util/array.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | #include "util/intern.h" | ||
| 10 | #include "util/str-util.h" | ||
| 11 | #include "util/xmalloc.h" | ||
| 12 | |||
| 13 | static const char capture_names[][8] = { | ||
| 14 | [ERRFMT_FILE] = "file", | ||
| 15 | [ERRFMT_LINE] = "line", | ||
| 16 | [ERRFMT_COLUMN] = "column", | ||
| 17 | [ERRFMT_MESSAGE] = "message" | ||
| 18 | }; | ||
| 19 | |||
| 20 | UNITTEST { | ||
| 21 | CHECK_STRING_ARRAY(capture_names); | ||
| 22 | } | ||
| 23 | |||
| 24 | static Compiler *find_or_add_compiler(HashMap *compilers, const char *name) | ||
| 25 | { | ||
| 26 | Compiler *c = find_compiler(compilers, name); | ||
| 27 | return c ? c : hashmap_insert(compilers, xstrdup(name), xnew0(Compiler, 1)); | ||
| 28 | } | ||
| 29 | |||
| 30 | Compiler *find_compiler(const HashMap *compilers, const char *name) | ||
| 31 | { | ||
| 32 | return hashmap_get(compilers, name); | ||
| 33 | } | ||
| 34 | |||
| 35 | bool add_error_fmt ( | ||
| 36 | HashMap *compilers, | ||
| 37 | const char *name, | ||
| 38 | bool ignore, | ||
| 39 | const char *format, | ||
| 40 | char **desc | ||
| 41 | ) { | ||
| 42 | int8_t idx[] = { | ||
| 43 | [ERRFMT_FILE] = -1, | ||
| 44 | [ERRFMT_LINE] = -1, | ||
| 45 | [ERRFMT_COLUMN] = -1, | ||
| 46 | [ERRFMT_MESSAGE] = 0, | ||
| 47 | }; | ||
| 48 | |||
| 49 | size_t max_idx = 0; | ||
| 50 | for (size_t i = 0, j = 0, n = ARRAYLEN(capture_names); desc[i]; i++) { | ||
| 51 | BUG_ON(i >= ERRORFMT_CAPTURE_MAX); | ||
| 52 | if (streq(desc[i], "_")) { | ||
| 53 | continue; | ||
| 54 | } | ||
| 55 | for (j = 0; j < n; j++) { | ||
| 56 | if (streq(desc[i], capture_names[j])) { | ||
| 57 | max_idx = i + 1; | ||
| 58 | idx[j] = max_idx; | ||
| 59 | break; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | if (unlikely(j == n)) { | ||
| 63 | return error_msg("unknown substring name %s", desc[i]); | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | ErrorFormat *f = xnew(ErrorFormat, 1); | ||
| 68 | f->ignore = ignore; | ||
| 69 | static_assert_compatible_types(f->capture_index, idx); | ||
| 70 | memcpy(f->capture_index, idx, sizeof(idx)); | ||
| 71 | |||
| 72 | if (unlikely(!regexp_compile(&f->re, format, 0))) { | ||
| 73 | free(f); | ||
| 74 | return false; | ||
| 75 | } | ||
| 76 | |||
| 77 | if (unlikely(max_idx > f->re.re_nsub)) { | ||
| 78 | regfree(&f->re); | ||
| 79 | free(f); | ||
| 80 | return error_msg("invalid substring count"); | ||
| 81 | } | ||
| 82 | |||
| 83 | Compiler *compiler = find_or_add_compiler(compilers, name); | ||
| 84 | f->pattern = str_intern(format); | ||
| 85 | ptr_array_append(&compiler->error_formats, f); | ||
| 86 | return true; | ||
| 87 | } | ||
| 88 | |||
| 89 | static void free_error_format(ErrorFormat *f) | ||
| 90 | { | ||
| 91 | regfree(&f->re); | ||
| 92 | free(f); | ||
| 93 | } | ||
| 94 | |||
| 95 | void free_compiler(Compiler *c) | ||
| 96 | { | ||
| 97 | ptr_array_free_cb(&c->error_formats, FREE_FUNC(free_error_format)); | ||
| 98 | free(c); | ||
| 99 | } | ||
| 100 | |||
| 101 | void remove_compiler(HashMap *compilers, const char *name) | ||
| 102 | { | ||
| 103 | Compiler *c = hashmap_remove(compilers, name); | ||
| 104 | if (c) { | ||
| 105 | free_compiler(c); | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | void collect_errorfmt_capture_names(PointerArray *a, const char *prefix) | ||
| 110 | { | ||
| 111 | COLLECT_STRINGS(capture_names, a, prefix); | ||
| 112 | if (str_has_prefix("_", prefix)) { | ||
| 113 | ptr_array_append(a, xstrdup("_")); | ||
| 114 | } | ||
| 115 | } | ||
| 116 | |||
| 117 | void dump_compiler(const Compiler *c, const char *name, String *s) | ||
| 118 | { | ||
| 119 | for (size_t i = 0, n = c->error_formats.count; i < n; i++) { | ||
| 120 | ErrorFormat *e = c->error_formats.ptrs[i]; | ||
| 121 | string_append_literal(s, "errorfmt "); | ||
| 122 | if (e->ignore) { | ||
| 123 | string_append_literal(s, "-i "); | ||
| 124 | } | ||
| 125 | if (unlikely(name[0] == '-' || e->pattern[0] == '-')) { | ||
| 126 | string_append_literal(s, "-- "); | ||
| 127 | } | ||
| 128 | string_append_escaped_arg(s, name, true); | ||
| 129 | string_append_byte(s, ' '); | ||
| 130 | string_append_escaped_arg(s, e->pattern, true); | ||
| 131 | |||
| 132 | static_assert(ARRAYLEN(e->capture_index) == 4); | ||
| 133 | const int8_t *a = e->capture_index; | ||
| 134 | int max_idx = MAX4(a[0], a[1], a[2], a[3]); | ||
| 135 | BUG_ON(max_idx > ERRORFMT_CAPTURE_MAX); | ||
| 136 | |||
| 137 | for (int j = 1; j <= max_idx; j++) { | ||
| 138 | const char *capname = "_"; | ||
| 139 | for (size_t k = 0; k < ARRAYLEN(capture_names); k++) { | ||
| 140 | if (j == a[k]) { | ||
| 141 | capname = capture_names[k]; | ||
| 142 | break; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | string_append_byte(s, ' '); | ||
| 146 | string_append_cstring(s, capname); | ||
| 147 | } | ||
| 148 | |||
| 149 | string_append_byte(s, '\n'); | ||
| 150 | } | ||
| 151 | } | ||
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 @@ | |||
| 1 | #ifndef COMPILER_H | ||
| 2 | #define COMPILER_H | ||
| 3 | |||
| 4 | #include <regex.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stdint.h> | ||
| 7 | #include "util/hashmap.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/ptr-array.h" | ||
| 10 | #include "util/string.h" | ||
| 11 | |||
| 12 | enum { | ||
| 13 | ERRORFMT_CAPTURE_MAX = 16 | ||
| 14 | }; | ||
| 15 | |||
| 16 | enum { | ||
| 17 | ERRFMT_FILE, | ||
| 18 | ERRFMT_LINE, | ||
| 19 | ERRFMT_COLUMN, | ||
| 20 | ERRFMT_MESSAGE, | ||
| 21 | }; | ||
| 22 | |||
| 23 | typedef struct { | ||
| 24 | int8_t capture_index[4]; | ||
| 25 | bool ignore; | ||
| 26 | const char *pattern; // Original pattern string (interned) | ||
| 27 | regex_t re; // Compiled pattern | ||
| 28 | } ErrorFormat; | ||
| 29 | |||
| 30 | typedef struct { | ||
| 31 | PointerArray error_formats; | ||
| 32 | } Compiler; | ||
| 33 | |||
| 34 | Compiler *find_compiler(const HashMap *compilers, const char *name) NONNULL_ARGS; | ||
| 35 | void remove_compiler(HashMap *compilers, const char *name) NONNULL_ARGS; | ||
| 36 | void free_compiler(Compiler *c) NONNULL_ARGS; | ||
| 37 | void collect_errorfmt_capture_names(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 38 | void dump_compiler(const Compiler *c, const char *name, String *s) NONNULL_ARGS; | ||
| 39 | |||
| 40 | NONNULL_ARGS WARN_UNUSED_RESULT | ||
| 41 | bool add_error_fmt ( | ||
| 42 | HashMap *compilers, | ||
| 43 | const char *name, | ||
| 44 | bool ignore, | ||
| 45 | const char *format, | ||
| 46 | char **desc | ||
| 47 | ); | ||
| 48 | |||
| 49 | #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 @@ | |||
| 1 | #include <fcntl.h> | ||
| 2 | #include <stdbool.h> | ||
| 3 | #include <stdlib.h> | ||
| 4 | #include <string.h> | ||
| 5 | #include <sys/stat.h> | ||
| 6 | #include <unistd.h> | ||
| 7 | #include "completion.h" | ||
| 8 | #include "bind.h" | ||
| 9 | #include "command/alias.h" | ||
| 10 | #include "command/args.h" | ||
| 11 | #include "command/parse.h" | ||
| 12 | #include "command/run.h" | ||
| 13 | #include "command/serialize.h" | ||
| 14 | #include "commands.h" | ||
| 15 | #include "compiler.h" | ||
| 16 | #include "config.h" | ||
| 17 | #include "filetype.h" | ||
| 18 | #include "options.h" | ||
| 19 | #include "show.h" | ||
| 20 | #include "syntax/color.h" | ||
| 21 | #include "tag.h" | ||
| 22 | #include "terminal/cursor.h" | ||
| 23 | #include "terminal/key.h" | ||
| 24 | #include "terminal/style.h" | ||
| 25 | #include "util/arith.h" | ||
| 26 | #include "util/array.h" | ||
| 27 | #include "util/ascii.h" | ||
| 28 | #include "util/bsearch.h" | ||
| 29 | #include "util/debug.h" | ||
| 30 | #include "util/intmap.h" | ||
| 31 | #include "util/log.h" | ||
| 32 | #include "util/numtostr.h" | ||
| 33 | #include "util/path.h" | ||
| 34 | #include "util/str-util.h" | ||
| 35 | #include "util/string-view.h" | ||
| 36 | #include "util/string.h" | ||
| 37 | #include "util/xdirent.h" | ||
| 38 | #include "util/xmalloc.h" | ||
| 39 | #include "vars.h" | ||
| 40 | |||
| 41 | extern char **environ; | ||
| 42 | |||
| 43 | typedef enum { | ||
| 44 | COLLECT_ALL, // (directories and files) | ||
| 45 | COLLECT_EXECUTABLES, // (directories and executable files) | ||
| 46 | COLLECT_DIRS_ONLY, | ||
| 47 | } FileCollectionType; | ||
| 48 | |||
| 49 | static bool is_executable(int dir_fd, const char *filename) | ||
| 50 | { | ||
| 51 | return faccessat(dir_fd, filename, X_OK, 0) == 0; | ||
| 52 | } | ||
| 53 | |||
| 54 | static bool do_collect_files ( | ||
| 55 | PointerArray *array, | ||
| 56 | const char *dirname, | ||
| 57 | const char *dirprefix, | ||
| 58 | const char *fileprefix, | ||
| 59 | FileCollectionType type | ||
| 60 | ) { | ||
| 61 | DIR *const dir = xopendir(dirname); | ||
| 62 | if (!dir) { | ||
| 63 | return false; | ||
| 64 | } | ||
| 65 | |||
| 66 | const int dir_fd = dirfd(dir); | ||
| 67 | if (unlikely(dir_fd < 0)) { | ||
| 68 | LOG_ERRNO("dirfd"); | ||
| 69 | xclosedir(dir); | ||
| 70 | return false; | ||
| 71 | } | ||
| 72 | |||
| 73 | size_t dlen = strlen(dirprefix); | ||
| 74 | size_t flen = strlen(fileprefix); | ||
| 75 | const struct dirent *de; | ||
| 76 | |||
| 77 | while ((de = xreaddir(dir))) { | ||
| 78 | const char *name = de->d_name; | ||
| 79 | if (streq(name, ".") || streq(name, "..") || unlikely(streq(name, ""))) { | ||
| 80 | continue; | ||
| 81 | } | ||
| 82 | |||
| 83 | // TODO: add a global option to allow dotfiles to be included | ||
| 84 | // even when there's no prefix | ||
| 85 | if (flen ? strncmp(name, fileprefix, flen) : name[0] == '.') { | ||
| 86 | continue; | ||
| 87 | } | ||
| 88 | |||
| 89 | struct stat st; | ||
| 90 | if (fstatat(dir_fd, name, &st, AT_SYMLINK_NOFOLLOW)) { | ||
| 91 | continue; | ||
| 92 | } | ||
| 93 | |||
| 94 | bool is_dir = S_ISDIR(st.st_mode); | ||
| 95 | if (S_ISLNK(st.st_mode)) { | ||
| 96 | if (!fstatat(dir_fd, name, &st, 0)) { | ||
| 97 | is_dir = S_ISDIR(st.st_mode); | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | if (!is_dir) { | ||
| 102 | switch (type) { | ||
| 103 | case COLLECT_DIRS_ONLY: | ||
| 104 | continue; | ||
| 105 | case COLLECT_ALL: | ||
| 106 | break; | ||
| 107 | case COLLECT_EXECUTABLES: | ||
| 108 | if (!is_executable(dir_fd, name)) { | ||
| 109 | continue; | ||
| 110 | } | ||
| 111 | if (!dlen) { | ||
| 112 | dirprefix = "./"; | ||
| 113 | dlen = 2; | ||
| 114 | } | ||
| 115 | break; | ||
| 116 | default: | ||
| 117 | BUG("unhandled FileCollectionType value"); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | ptr_array_append(array, path_joinx(dirprefix, name, is_dir)); | ||
| 122 | } | ||
| 123 | |||
| 124 | xclosedir(dir); | ||
| 125 | return true; | ||
| 126 | } | ||
| 127 | |||
| 128 | static void collect_files(EditorState *e, CompletionState *cs, FileCollectionType type) | ||
| 129 | { | ||
| 130 | StringView esc = cs->escaped; | ||
| 131 | if (strview_has_prefix(&esc, "~/")) { | ||
| 132 | CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); | ||
| 133 | char *str = parse_command_arg(&runner, esc.data, esc.length, false); | ||
| 134 | const char *slash = strrchr(str, '/'); | ||
| 135 | BUG_ON(!slash); | ||
| 136 | cs->tilde_expanded = true; | ||
| 137 | char *dir = path_dirname(cs->parsed); | ||
| 138 | char *dirprefix = path_dirname(str); | ||
| 139 | do_collect_files(&cs->completions, dir, dirprefix, slash + 1, type); | ||
| 140 | free(dirprefix); | ||
| 141 | free(dir); | ||
| 142 | free(str); | ||
| 143 | } else { | ||
| 144 | const char *slash = strrchr(cs->parsed, '/'); | ||
| 145 | if (!slash) { | ||
| 146 | do_collect_files(&cs->completions, ".", "", cs->parsed, type); | ||
| 147 | } else { | ||
| 148 | char *dir = path_dirname(cs->parsed); | ||
| 149 | do_collect_files(&cs->completions, dir, dir, slash + 1, type); | ||
| 150 | free(dir); | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | if (cs->completions.count == 1) { | ||
| 155 | // Add space if completed string is not a directory | ||
| 156 | const char *s = cs->completions.ptrs[0]; | ||
| 157 | size_t len = strlen(s); | ||
| 158 | if (len > 0) { | ||
| 159 | cs->add_space_after_single_match = s[len - 1] != '/'; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix) | ||
| 165 | { | ||
| 166 | collect_hashmap_keys(&e->aliases, a, prefix); | ||
| 167 | } | ||
| 168 | |||
| 169 | static void collect_bound_keys(const IntMap *bindings, PointerArray *a, const char *prefix) | ||
| 170 | { | ||
| 171 | char keystr[KEYCODE_STR_MAX]; | ||
| 172 | for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) { | ||
| 173 | size_t keylen = keycode_to_string(it.entry->key, keystr); | ||
| 174 | if (str_has_prefix(keystr, prefix)) { | ||
| 175 | ptr_array_append(a, xmemdup(keystr, keylen + 1)); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | } | ||
| 179 | |||
| 180 | void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *prefix) | ||
| 181 | { | ||
| 182 | collect_bound_keys(&e->modes[INPUT_NORMAL].key_bindings, a, prefix); | ||
| 183 | } | ||
| 184 | |||
| 185 | void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix) | ||
| 186 | { | ||
| 187 | collect_builtin_colors(a, prefix); | ||
| 188 | collect_hashmap_keys(&e->colors.other, a, prefix); | ||
| 189 | } | ||
| 190 | |||
| 191 | void collect_compilers(EditorState *e, PointerArray *a, const char *prefix) | ||
| 192 | { | ||
| 193 | collect_hashmap_keys(&e->compilers, a, prefix); | ||
| 194 | } | ||
| 195 | |||
| 196 | void collect_env(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) | ||
| 197 | { | ||
| 198 | if (strchr(prefix, '=')) { | ||
| 199 | return; | ||
| 200 | } | ||
| 201 | |||
| 202 | for (size_t i = 0; environ[i]; i++) { | ||
| 203 | const char *var = environ[i]; | ||
| 204 | if (str_has_prefix(var, prefix)) { | ||
| 205 | const char *delim = strchr(var, '='); | ||
| 206 | if (likely(delim && delim != var)) { | ||
| 207 | ptr_array_append(a, xstrcut(var, delim - var)); | ||
| 208 | } | ||
| 209 | } | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | static void complete_alias(EditorState *e, const CommandArgs *a) | ||
| 214 | { | ||
| 215 | CompletionState *cs = &e->cmdline.completion; | ||
| 216 | if (a->nr_args == 0) { | ||
| 217 | collect_normal_aliases(e, &cs->completions, cs->parsed); | ||
| 218 | } else if (a->nr_args == 1 && cs->parsed[0] == '\0') { | ||
| 219 | const char *cmd = find_alias(&e->aliases, a->args[0]); | ||
| 220 | if (cmd) { | ||
| 221 | ptr_array_append(&cs->completions, xstrdup(cmd)); | ||
| 222 | } | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | static void complete_bind(EditorState *e, const CommandArgs *a) | ||
| 227 | { | ||
| 228 | static const char flags[] = { | ||
| 229 | [INPUT_NORMAL] = 'n', | ||
| 230 | [INPUT_COMMAND] = 'c', | ||
| 231 | [INPUT_SEARCH] = 's', | ||
| 232 | }; | ||
| 233 | |||
| 234 | static_assert(ARRAYLEN(flags) == ARRAYLEN(e->modes)); | ||
| 235 | InputMode mode = INPUT_NORMAL; | ||
| 236 | for (size_t i = 0, count = 0; i < ARRAYLEN(flags); i++) { | ||
| 237 | if (cmdargs_has_flag(a, flags[i])) { | ||
| 238 | if (++count >= 2) { | ||
| 239 | return; // Don't complete bindings for multiple modes | ||
| 240 | } | ||
| 241 | mode = i; | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | const IntMap *key_bindings = &e->modes[mode].key_bindings; | ||
| 246 | CompletionState *cs = &e->cmdline.completion; | ||
| 247 | if (a->nr_args == 0) { | ||
| 248 | collect_bound_keys(key_bindings, &cs->completions, cs->parsed); | ||
| 249 | return; | ||
| 250 | } | ||
| 251 | |||
| 252 | if (a->nr_args != 1 || cs->parsed[0] != '\0') { | ||
| 253 | return; | ||
| 254 | } | ||
| 255 | |||
| 256 | KeyCode key; | ||
| 257 | if (!parse_key_string(&key, a->args[0])) { | ||
| 258 | return; | ||
| 259 | } | ||
| 260 | const CachedCommand *cmd = lookup_binding(key_bindings, key); | ||
| 261 | if (!cmd) { | ||
| 262 | return; | ||
| 263 | } | ||
| 264 | |||
| 265 | ptr_array_append(&cs->completions, xstrdup(cmd->cmd_str)); | ||
| 266 | } | ||
| 267 | |||
| 268 | static void complete_cd(EditorState *e, const CommandArgs* UNUSED_ARG(a)) | ||
| 269 | { | ||
| 270 | CompletionState *cs = &e->cmdline.completion; | ||
| 271 | collect_files(e, cs, COLLECT_DIRS_ONLY); | ||
| 272 | if (str_has_prefix("-", cs->parsed)) { | ||
| 273 | if (likely(xgetenv("OLDPWD"))) { | ||
| 274 | ptr_array_append(&cs->completions, xstrdup("-")); | ||
| 275 | } | ||
| 276 | } | ||
| 277 | } | ||
| 278 | |||
| 279 | static void complete_exec(EditorState *e, const CommandArgs *a) | ||
| 280 | { | ||
| 281 | // TODO: add completion for [-ioe] option arguments | ||
| 282 | CompletionState *cs = &e->cmdline.completion; | ||
| 283 | collect_files(e, cs, a->nr_args == 0 ? COLLECT_EXECUTABLES : COLLECT_ALL); | ||
| 284 | } | ||
| 285 | |||
| 286 | static void complete_compile(EditorState *e, const CommandArgs *a) | ||
| 287 | { | ||
| 288 | CompletionState *cs = &e->cmdline.completion; | ||
| 289 | size_t n = a->nr_args; | ||
| 290 | if (n == 0) { | ||
| 291 | collect_compilers(e, &cs->completions, cs->parsed); | ||
| 292 | } else { | ||
| 293 | collect_files(e, cs, n == 1 ? COLLECT_EXECUTABLES : COLLECT_ALL); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | |||
| 297 | static void complete_cursor(EditorState *e, const CommandArgs *a) | ||
| 298 | { | ||
| 299 | CompletionState *cs = &e->cmdline.completion; | ||
| 300 | size_t n = a->nr_args; | ||
| 301 | if (n == 0) { | ||
| 302 | collect_cursor_modes(&cs->completions, cs->parsed); | ||
| 303 | } else if (n == 1) { | ||
| 304 | collect_cursor_types(&cs->completions, cs->parsed); | ||
| 305 | } else if (n == 2) { | ||
| 306 | collect_cursor_colors(&cs->completions, cs->parsed); | ||
| 307 | // Add an example #rrggbb color, to make things more discoverable | ||
| 308 | static const char rgb_example[] = "#22AABB"; | ||
| 309 | if (str_has_prefix(rgb_example, cs->parsed)) { | ||
| 310 | ptr_array_append(&cs->completions, xstrdup(rgb_example)); | ||
| 311 | } | ||
| 312 | } | ||
| 313 | } | ||
| 314 | |||
| 315 | static void complete_errorfmt(EditorState *e, const CommandArgs *a) | ||
| 316 | { | ||
| 317 | CompletionState *cs = &e->cmdline.completion; | ||
| 318 | if (a->nr_args == 0) { | ||
| 319 | collect_compilers(e, &cs->completions, cs->parsed); | ||
| 320 | } else if (a->nr_args >= 2 && !cmdargs_has_flag(a, 'i')) { | ||
| 321 | collect_errorfmt_capture_names(&cs->completions, cs->parsed); | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | static void complete_ft(EditorState *e, const CommandArgs *a) | ||
| 326 | { | ||
| 327 | CompletionState *cs = &e->cmdline.completion; | ||
| 328 | if (a->nr_args == 0) { | ||
| 329 | collect_ft(&e->filetypes, &cs->completions, cs->parsed); | ||
| 330 | } | ||
| 331 | } | ||
| 332 | |||
| 333 | static void complete_hi(EditorState *e, const CommandArgs *a) | ||
| 334 | { | ||
| 335 | CompletionState *cs = &e->cmdline.completion; | ||
| 336 | if (a->nr_args == 0) { | ||
| 337 | collect_hl_colors(e, &cs->completions, cs->parsed); | ||
| 338 | } else { | ||
| 339 | collect_colors_and_attributes(&cs->completions, cs->parsed); | ||
| 340 | } | ||
| 341 | } | ||
| 342 | |||
| 343 | static void complete_include(EditorState *e, const CommandArgs *a) | ||
| 344 | { | ||
| 345 | CompletionState *cs = &e->cmdline.completion; | ||
| 346 | if (a->nr_args == 0) { | ||
| 347 | if (cmdargs_has_flag(a, 'b')) { | ||
| 348 | collect_builtin_includes(&cs->completions, cs->parsed); | ||
| 349 | } else { | ||
| 350 | collect_files(e, cs, COLLECT_ALL); | ||
| 351 | } | ||
| 352 | } | ||
| 353 | } | ||
| 354 | |||
| 355 | static void complete_macro(EditorState *e, const CommandArgs *a) | ||
| 356 | { | ||
| 357 | static const char verbs[][8] = { | ||
| 358 | "cancel", | ||
| 359 | "play", | ||
| 360 | "record", | ||
| 361 | "stop", | ||
| 362 | "toggle", | ||
| 363 | }; | ||
| 364 | |||
| 365 | if (a->nr_args != 0) { | ||
| 366 | return; | ||
| 367 | } | ||
| 368 | |||
| 369 | CompletionState *cs = &e->cmdline.completion; | ||
| 370 | COLLECT_STRINGS(verbs, &cs->completions, cs->parsed); | ||
| 371 | } | ||
| 372 | |||
| 373 | static void complete_move_tab(EditorState *e, const CommandArgs *a) | ||
| 374 | { | ||
| 375 | if (a->nr_args != 0) { | ||
| 376 | return; | ||
| 377 | } | ||
| 378 | |||
| 379 | static const char words[][8] = {"left", "right"}; | ||
| 380 | CompletionState *cs = &e->cmdline.completion; | ||
| 381 | COLLECT_STRINGS(words, &cs->completions, cs->parsed); | ||
| 382 | } | ||
| 383 | |||
| 384 | static void complete_open(EditorState *e, const CommandArgs *a) | ||
| 385 | { | ||
| 386 | if (!cmdargs_has_flag(a, 't')) { | ||
| 387 | collect_files(e, &e->cmdline.completion, COLLECT_ALL); | ||
| 388 | } | ||
| 389 | } | ||
| 390 | |||
| 391 | static void complete_option(EditorState *e, const CommandArgs *a) | ||
| 392 | { | ||
| 393 | CompletionState *cs = &e->cmdline.completion; | ||
| 394 | if (a->nr_args == 0) { | ||
| 395 | if (!cmdargs_has_flag(a, 'r')) { | ||
| 396 | collect_ft(&e->filetypes, &cs->completions, cs->parsed); | ||
| 397 | } | ||
| 398 | } else if (a->nr_args & 1) { | ||
| 399 | collect_auto_options(&cs->completions, cs->parsed); | ||
| 400 | } else { | ||
| 401 | collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed); | ||
| 402 | } | ||
| 403 | } | ||
| 404 | |||
| 405 | static void complete_save(EditorState *e, const CommandArgs* UNUSED_ARG(a)) | ||
| 406 | { | ||
| 407 | collect_files(e, &e->cmdline.completion, COLLECT_ALL); | ||
| 408 | } | ||
| 409 | |||
| 410 | static void complete_quit(EditorState *e, const CommandArgs* UNUSED_ARG(a)) | ||
| 411 | { | ||
| 412 | CompletionState *cs = &e->cmdline.completion; | ||
| 413 | if (str_has_prefix("0", cs->parsed)) { | ||
| 414 | ptr_array_append(&cs->completions, xstrdup("0")); | ||
| 415 | } | ||
| 416 | if (str_has_prefix("1", cs->parsed)) { | ||
| 417 | ptr_array_append(&cs->completions, xstrdup("1")); | ||
| 418 | } | ||
| 419 | } | ||
| 420 | |||
| 421 | static void complete_redo(EditorState *e, const CommandArgs* UNUSED_ARG(a)) | ||
| 422 | { | ||
| 423 | const Change *change = e->buffer->cur_change; | ||
| 424 | CompletionState *cs = &e->cmdline.completion; | ||
| 425 | for (unsigned long i = 1, n = change->nr_prev; i <= n; i++) { | ||
| 426 | ptr_array_append(&cs->completions, xstrdup(ulong_to_str(i))); | ||
| 427 | } | ||
| 428 | } | ||
| 429 | |||
| 430 | static void complete_set(EditorState *e, const CommandArgs *a) | ||
| 431 | { | ||
| 432 | CompletionState *cs = &e->cmdline.completion; | ||
| 433 | if ((a->nr_args + 1) & 1) { | ||
| 434 | bool local = cmdargs_has_flag(a, 'l'); | ||
| 435 | bool global = cmdargs_has_flag(a, 'g'); | ||
| 436 | collect_options(&cs->completions, cs->parsed, local, global); | ||
| 437 | } else { | ||
| 438 | collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed); | ||
| 439 | } | ||
| 440 | } | ||
| 441 | |||
| 442 | static void complete_setenv(EditorState *e, const CommandArgs *a) | ||
| 443 | { | ||
| 444 | CompletionState *cs = &e->cmdline.completion; | ||
| 445 | if (a->nr_args == 0) { | ||
| 446 | collect_env(e, &cs->completions, cs->parsed); | ||
| 447 | } else if (a->nr_args == 1 && cs->parsed[0] == '\0') { | ||
| 448 | BUG_ON(!a->args[0]); | ||
| 449 | const char *value = getenv(a->args[0]); | ||
| 450 | if (value) { | ||
| 451 | ptr_array_append(&cs->completions, xstrdup(value)); | ||
| 452 | } | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | static void complete_show(EditorState *e, const CommandArgs *a) | ||
| 457 | { | ||
| 458 | CompletionState *cs = &e->cmdline.completion; | ||
| 459 | if (a->nr_args == 0) { | ||
| 460 | collect_show_subcommands(&cs->completions, cs->parsed); | ||
| 461 | } else if (a->nr_args == 1) { | ||
| 462 | BUG_ON(!a->args[0]); | ||
| 463 | collect_show_subcommand_args(e, &cs->completions, a->args[0], cs->parsed); | ||
| 464 | } | ||
| 465 | } | ||
| 466 | |||
| 467 | static void complete_tag(EditorState *e, const CommandArgs *a) | ||
| 468 | { | ||
| 469 | CompletionState *cs = &e->cmdline.completion; | ||
| 470 | if (a->nr_args == 0 && !cmdargs_has_flag(a, 'r')) { | ||
| 471 | BUG_ON(!cs->parsed); | ||
| 472 | StringView prefix = strview_from_cstring(cs->parsed); | ||
| 473 | collect_tags(&e->tagfile, &cs->completions, &prefix); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | |||
| 477 | static void complete_toggle(EditorState *e, const CommandArgs *a) | ||
| 478 | { | ||
| 479 | CompletionState *cs = &e->cmdline.completion; | ||
| 480 | if (a->nr_args == 0) { | ||
| 481 | bool global = cmdargs_has_flag(a, 'g'); | ||
| 482 | collect_toggleable_options(&cs->completions, cs->parsed, global); | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | static void complete_wsplit(EditorState *e, const CommandArgs *a) | ||
| 487 | { | ||
| 488 | CompletionState *cs = &e->cmdline.completion; | ||
| 489 | if (!cmdargs_has_flag(a, 't') && !cmdargs_has_flag(a, 'n')) { | ||
| 490 | collect_files(e, cs, COLLECT_ALL); | ||
| 491 | } | ||
| 492 | } | ||
| 493 | |||
| 494 | typedef struct { | ||
| 495 | char cmd_name[12]; | ||
| 496 | void (*complete)(EditorState *e, const CommandArgs *a); | ||
| 497 | } CompletionHandler; | ||
| 498 | |||
| 499 | static const CompletionHandler completion_handlers[] = { | ||
| 500 | {"alias", complete_alias}, | ||
| 501 | {"bind", complete_bind}, | ||
| 502 | {"cd", complete_cd}, | ||
| 503 | {"compile", complete_compile}, | ||
| 504 | {"cursor", complete_cursor}, | ||
| 505 | {"errorfmt", complete_errorfmt}, | ||
| 506 | {"exec", complete_exec}, | ||
| 507 | {"ft", complete_ft}, | ||
| 508 | {"hi", complete_hi}, | ||
| 509 | {"include", complete_include}, | ||
| 510 | {"macro", complete_macro}, | ||
| 511 | {"move-tab", complete_move_tab}, | ||
| 512 | {"open", complete_open}, | ||
| 513 | {"option", complete_option}, | ||
| 514 | {"quit", complete_quit}, | ||
| 515 | {"redo", complete_redo}, | ||
| 516 | {"save", complete_save}, | ||
| 517 | {"set", complete_set}, | ||
| 518 | {"setenv", complete_setenv}, | ||
| 519 | {"show", complete_show}, | ||
| 520 | {"tag", complete_tag}, | ||
| 521 | {"toggle", complete_toggle}, | ||
| 522 | {"wsplit", complete_wsplit}, | ||
| 523 | }; | ||
| 524 | |||
| 525 | UNITTEST { | ||
| 526 | CHECK_BSEARCH_ARRAY(completion_handlers, cmd_name, strcmp); | ||
| 527 | // Ensure handlers are kept in sync with renamed/removed commands | ||
| 528 | for (size_t i = 0; i < ARRAYLEN(completion_handlers); i++) { | ||
| 529 | const char *name = completion_handlers[i].cmd_name; | ||
| 530 | if (!find_normal_command(name)) { | ||
| 531 | BUG("completion handler for non-existent command: \"%s\"", name); | ||
| 532 | } | ||
| 533 | } | ||
| 534 | } | ||
| 535 | |||
| 536 | static bool can_collect_flags ( | ||
| 537 | char **args, | ||
| 538 | size_t argc, | ||
| 539 | size_t nr_flag_args, | ||
| 540 | bool allow_flags_after_nonflags | ||
| 541 | ) { | ||
| 542 | if (allow_flags_after_nonflags) { | ||
| 543 | for (size_t i = 0; i < argc; i++) { | ||
| 544 | if (streq(args[i], "--")) { | ||
| 545 | return false; | ||
| 546 | } | ||
| 547 | } | ||
| 548 | return true; | ||
| 549 | } | ||
| 550 | |||
| 551 | for (size_t i = 0, nonflag = 0; i < argc; i++) { | ||
| 552 | if (args[i][0] != '-') { | ||
| 553 | if (++nonflag > nr_flag_args) { | ||
| 554 | return false; | ||
| 555 | } | ||
| 556 | continue; | ||
| 557 | } | ||
| 558 | if (streq(args[i], "--")) { | ||
| 559 | return false; | ||
| 560 | } | ||
| 561 | } | ||
| 562 | |||
| 563 | return true; | ||
| 564 | } | ||
| 565 | |||
| 566 | static bool collect_command_flags ( | ||
| 567 | PointerArray *array, | ||
| 568 | char **args, | ||
| 569 | size_t argc, | ||
| 570 | const Command *cmd, | ||
| 571 | const CommandArgs *a, | ||
| 572 | const char *prefix | ||
| 573 | ) { | ||
| 574 | BUG_ON(prefix[0] != '-'); | ||
| 575 | const char *flags = cmd->flags; | ||
| 576 | bool flags_after_nonflags = (flags[0] != '-'); | ||
| 577 | |||
| 578 | if (!can_collect_flags(args, argc, a->nr_flag_args, flags_after_nonflags)) { | ||
| 579 | return false; | ||
| 580 | } | ||
| 581 | |||
| 582 | flags += flags_after_nonflags ? 0 : 1; | ||
| 583 | if (ascii_isalnum(prefix[1]) && prefix[2] == '\0') { | ||
| 584 | if (strchr(flags, prefix[1])) { | ||
| 585 | ptr_array_append(array, xmemdup(prefix, 3)); | ||
| 586 | } | ||
| 587 | return true; | ||
| 588 | } | ||
| 589 | |||
| 590 | if (prefix[1] != '\0') { | ||
| 591 | return true; | ||
| 592 | } | ||
| 593 | |||
| 594 | char buf[3] = "-"; | ||
| 595 | for (size_t i = 0; flags[i]; i++) { | ||
| 596 | if (!ascii_isalnum(flags[i]) || cmdargs_has_flag(a, flags[i])) { | ||
| 597 | continue; | ||
| 598 | } | ||
| 599 | buf[1] = flags[i]; | ||
| 600 | ptr_array_append(array, xmemdup(buf, 3)); | ||
| 601 | } | ||
| 602 | |||
| 603 | return true; | ||
| 604 | } | ||
| 605 | |||
| 606 | static void collect_completions(EditorState *e, char **args, size_t argc) | ||
| 607 | { | ||
| 608 | CompletionState *cs = &e->cmdline.completion; | ||
| 609 | PointerArray *arr = &cs->completions; | ||
| 610 | const char *prefix = cs->parsed; | ||
| 611 | if (!argc) { | ||
| 612 | collect_normal_commands(arr, prefix); | ||
| 613 | collect_normal_aliases(e, arr, prefix); | ||
| 614 | return; | ||
| 615 | } | ||
| 616 | |||
| 617 | for (size_t i = 0; i < argc; i++) { | ||
| 618 | if (!args[i]) { | ||
| 619 | // Embedded NULLs indicate there are multiple commands. | ||
| 620 | // Just return early here and avoid handling this case. | ||
| 621 | return; | ||
| 622 | } | ||
| 623 | } | ||
| 624 | |||
| 625 | const Command *cmd = find_normal_command(args[0]); | ||
| 626 | if (!cmd) { | ||
| 627 | return; | ||
| 628 | } | ||
| 629 | |||
| 630 | char **args_copy = copy_string_array(args + 1, argc - 1); | ||
| 631 | CommandArgs a = cmdargs_new(args_copy); | ||
| 632 | ArgParseError err = do_parse_args(cmd, &a); | ||
| 633 | bool dash = (prefix[0] == '-'); | ||
| 634 | if ( | ||
| 635 | (err != ARGERR_NONE && err != ARGERR_TOO_FEW_ARGUMENTS) | ||
| 636 | || (a.nr_args >= cmd->max_args && cmd->max_args != 0xFF && !dash) | ||
| 637 | ) { | ||
| 638 | goto out; | ||
| 639 | } | ||
| 640 | |||
| 641 | if (dash && collect_command_flags(arr, args + 1, argc - 1, cmd, &a, prefix)) { | ||
| 642 | goto out; | ||
| 643 | } | ||
| 644 | |||
| 645 | if (cmd->max_args == 0) { | ||
| 646 | goto out; | ||
| 647 | } | ||
| 648 | |||
| 649 | const CompletionHandler *h = BSEARCH(args[0], completion_handlers, vstrcmp); | ||
| 650 | if (h) { | ||
| 651 | h->complete(e, &a); | ||
| 652 | } else if (streq(args[0], "repeat")) { | ||
| 653 | if (a.nr_args == 1) { | ||
| 654 | collect_normal_commands(arr, prefix); | ||
| 655 | } else if (a.nr_args >= 2) { | ||
| 656 | collect_completions(e, args + 2, argc - 2); | ||
| 657 | } | ||
| 658 | } | ||
| 659 | |||
| 660 | out: | ||
| 661 | free_string_array(args_copy); | ||
| 662 | } | ||
| 663 | |||
| 664 | static bool is_var(const char *str, size_t len) | ||
| 665 | { | ||
| 666 | if (len == 0 || str[0] != '$') { | ||
| 667 | return false; | ||
| 668 | } | ||
| 669 | if (len == 1) { | ||
| 670 | return true; | ||
| 671 | } | ||
| 672 | if (!is_alpha_or_underscore(str[1])) { | ||
| 673 | return false; | ||
| 674 | } | ||
| 675 | for (size_t i = 2; i < len; i++) { | ||
| 676 | if (!is_alnum_or_underscore(str[i])) { | ||
| 677 | return false; | ||
| 678 | } | ||
| 679 | } | ||
| 680 | return true; | ||
| 681 | } | ||
| 682 | |||
| 683 | UNITTEST { | ||
| 684 | BUG_ON(!is_var(STRN("$VAR"))); | ||
| 685 | BUG_ON(!is_var(STRN("$xy_190"))); | ||
| 686 | BUG_ON(!is_var(STRN("$__x_y_z"))); | ||
| 687 | BUG_ON(!is_var(STRN("$x"))); | ||
| 688 | BUG_ON(!is_var(STRN("$A"))); | ||
| 689 | BUG_ON(!is_var(STRN("$_0"))); | ||
| 690 | BUG_ON(!is_var(STRN("$"))); | ||
| 691 | BUG_ON(is_var(STRN(""))); | ||
| 692 | BUG_ON(is_var(STRN("A"))); | ||
| 693 | BUG_ON(is_var(STRN("$.a"))); | ||
| 694 | BUG_ON(is_var(STRN("$xyz!"))); | ||
| 695 | BUG_ON(is_var(STRN("$1"))); | ||
| 696 | BUG_ON(is_var(STRN("$09"))); | ||
| 697 | BUG_ON(is_var(STRN("$1a"))); | ||
| 698 | } | ||
| 699 | |||
| 700 | static int strptrcmp(const void *v1, const void *v2) | ||
| 701 | { | ||
| 702 | const char *const *s1 = v1; | ||
| 703 | const char *const *s2 = v2; | ||
| 704 | return strcmp(*s1, *s2); | ||
| 705 | } | ||
| 706 | |||
| 707 | static void init_completion(EditorState *e, const CommandLine *cmdline) | ||
| 708 | { | ||
| 709 | CompletionState *cs = &e->cmdline.completion; | ||
| 710 | const CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); | ||
| 711 | BUG_ON(cs->orig); | ||
| 712 | BUG_ON(runner.userdata != e); | ||
| 713 | BUG_ON(!runner.lookup_alias); | ||
| 714 | |||
| 715 | const size_t cmdline_pos = cmdline->pos; | ||
| 716 | char *const cmd = string_clone_cstring(&cmdline->buf); | ||
| 717 | PointerArray array = PTR_ARRAY_INIT; | ||
| 718 | ssize_t semicolon = -1; | ||
| 719 | ssize_t completion_pos = -1; | ||
| 720 | |||
| 721 | for (size_t pos = 0; true; ) { | ||
| 722 | while (ascii_isspace(cmd[pos])) { | ||
| 723 | pos++; | ||
| 724 | } | ||
| 725 | |||
| 726 | if (pos >= cmdline_pos) { | ||
| 727 | completion_pos = cmdline_pos; | ||
| 728 | break; | ||
| 729 | } | ||
| 730 | |||
| 731 | if (!cmd[pos]) { | ||
| 732 | break; | ||
| 733 | } | ||
| 734 | |||
| 735 | if (cmd[pos] == ';') { | ||
| 736 | semicolon = array.count; | ||
| 737 | ptr_array_append(&array, NULL); | ||
| 738 | pos++; | ||
| 739 | continue; | ||
| 740 | } | ||
| 741 | |||
| 742 | CommandParseError err; | ||
| 743 | size_t end = find_end(cmd, pos, &err); | ||
| 744 | if (err != CMDERR_NONE || end >= cmdline_pos) { | ||
| 745 | completion_pos = pos; | ||
| 746 | break; | ||
| 747 | } | ||
| 748 | |||
| 749 | if (semicolon + 1 == array.count) { | ||
| 750 | char *name = xstrslice(cmd, pos, end); | ||
| 751 | const char *value = runner.lookup_alias(name, runner.userdata); | ||
| 752 | if (value) { | ||
| 753 | size_t save = array.count; | ||
| 754 | if (parse_commands(&runner, &array, value) != CMDERR_NONE) { | ||
| 755 | for (size_t i = save, n = array.count; i < n; i++) { | ||
| 756 | free(array.ptrs[i]); | ||
| 757 | array.ptrs[i] = NULL; | ||
| 758 | } | ||
| 759 | array.count = save; | ||
| 760 | ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true)); | ||
| 761 | } else { | ||
| 762 | // Remove NULL | ||
| 763 | array.count--; | ||
| 764 | } | ||
| 765 | } else { | ||
| 766 | ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true)); | ||
| 767 | } | ||
| 768 | free(name); | ||
| 769 | } else { | ||
| 770 | ptr_array_append(&array, parse_command_arg(&runner, cmd + pos, end - pos, true)); | ||
| 771 | } | ||
| 772 | pos = end; | ||
| 773 | } | ||
| 774 | |||
| 775 | const char *str = cmd + completion_pos; | ||
| 776 | size_t len = cmdline_pos - completion_pos; | ||
| 777 | if (is_var(str, len)) { | ||
| 778 | char *name = xstrslice(str, 1, len); | ||
| 779 | completion_pos++; | ||
| 780 | collect_env(e, &cs->completions, name); | ||
| 781 | collect_normal_vars(&cs->completions, name); | ||
| 782 | free(name); | ||
| 783 | } else { | ||
| 784 | cs->escaped = string_view(str, len); | ||
| 785 | cs->parsed = parse_command_arg(&runner, str, len, true); | ||
| 786 | cs->add_space_after_single_match = true; | ||
| 787 | size_t count = array.count; | ||
| 788 | char **args = count ? (char**)array.ptrs + 1 + semicolon : NULL; | ||
| 789 | size_t argc = count ? array.count - semicolon - 1 : 0; | ||
| 790 | collect_completions(e, args, argc); | ||
| 791 | } | ||
| 792 | |||
| 793 | ptr_array_free(&array); | ||
| 794 | ptr_array_sort(&cs->completions, strptrcmp); | ||
| 795 | cs->orig = cmd; // (takes ownership) | ||
| 796 | cs->tail = strview_from_cstring(cmd + cmdline_pos); | ||
| 797 | cs->head_len = completion_pos; | ||
| 798 | } | ||
| 799 | |||
| 800 | static void do_complete_command(CommandLine *cmdline) | ||
| 801 | { | ||
| 802 | const CompletionState *cs = &cmdline->completion; | ||
| 803 | const PointerArray *arr = &cs->completions; | ||
| 804 | const StringView middle = strview_from_cstring(arr->ptrs[cs->idx]); | ||
| 805 | const StringView tail = cs->tail; | ||
| 806 | const size_t head_length = cs->head_len; | ||
| 807 | |||
| 808 | String buf = string_new(head_length + tail.length + middle.length + 16); | ||
| 809 | string_append_buf(&buf, cs->orig, head_length); | ||
| 810 | string_append_escaped_arg_sv(&buf, middle, !cs->tilde_expanded); | ||
| 811 | |||
| 812 | bool single_completion = (arr->count == 1); | ||
| 813 | if (single_completion && cs->add_space_after_single_match) { | ||
| 814 | string_append_byte(&buf, ' '); | ||
| 815 | } | ||
| 816 | |||
| 817 | size_t pos = buf.len; | ||
| 818 | string_append_strview(&buf, &tail); | ||
| 819 | cmdline_set_text(cmdline, string_borrow_cstring(&buf)); | ||
| 820 | cmdline->pos = pos; | ||
| 821 | string_free(&buf); | ||
| 822 | |||
| 823 | if (single_completion) { | ||
| 824 | reset_completion(cmdline); | ||
| 825 | } | ||
| 826 | } | ||
| 827 | |||
| 828 | void complete_command_next(EditorState *e) | ||
| 829 | { | ||
| 830 | CompletionState *cs = &e->cmdline.completion; | ||
| 831 | const bool init = !cs->orig; | ||
| 832 | if (init) { | ||
| 833 | init_completion(e, &e->cmdline); | ||
| 834 | } | ||
| 835 | size_t count = cs->completions.count; | ||
| 836 | if (!count) { | ||
| 837 | return; | ||
| 838 | } | ||
| 839 | if (!init) { | ||
| 840 | cs->idx = size_increment_wrapped(cs->idx, count); | ||
| 841 | } | ||
| 842 | do_complete_command(&e->cmdline); | ||
| 843 | } | ||
| 844 | |||
| 845 | void complete_command_prev(EditorState *e) | ||
| 846 | { | ||
| 847 | CompletionState *cs = &e->cmdline.completion; | ||
| 848 | const bool init = !cs->orig; | ||
| 849 | if (init) { | ||
| 850 | init_completion(e, &e->cmdline); | ||
| 851 | } | ||
| 852 | size_t count = cs->completions.count; | ||
| 853 | if (!count) { | ||
| 854 | return; | ||
| 855 | } | ||
| 856 | if (!init) { | ||
| 857 | cs->idx = size_decrement_wrapped(cs->idx, count); | ||
| 858 | } | ||
| 859 | do_complete_command(&e->cmdline); | ||
| 860 | } | ||
| 861 | |||
| 862 | void reset_completion(CommandLine *cmdline) | ||
| 863 | { | ||
| 864 | CompletionState *cs = &cmdline->completion; | ||
| 865 | free(cs->parsed); | ||
| 866 | free(cs->orig); | ||
| 867 | ptr_array_free(&cs->completions); | ||
| 868 | *cs = (CompletionState){.orig = NULL}; | ||
| 869 | } | ||
| 870 | |||
| 871 | void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix) | ||
| 872 | { | ||
| 873 | for (HashMapIter it = hashmap_iter(map); hashmap_next(&it); ) { | ||
| 874 | const char *name = it.entry->key; | ||
| 875 | if (str_has_prefix(name, prefix)) { | ||
| 876 | ptr_array_append(a, xstrdup(name)); | ||
| 877 | } | ||
| 878 | } | ||
| 879 | } | ||
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 @@ | |||
| 1 | #ifndef COMPLETION_H | ||
| 2 | #define COMPLETION_H | ||
| 3 | |||
| 4 | #include "cmdline.h" | ||
| 5 | #include "editor.h" | ||
| 6 | #include "util/hashmap.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | #include "util/ptr-array.h" | ||
| 9 | |||
| 10 | void complete_command_next(EditorState *e) NONNULL_ARGS; | ||
| 11 | void complete_command_prev(EditorState *e) NONNULL_ARGS; | ||
| 12 | void reset_completion(CommandLine *cmdline) NONNULL_ARGS; | ||
| 13 | |||
| 14 | void collect_env(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 15 | void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 16 | void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *keystr_prefix) NONNULL_ARGS; | ||
| 17 | void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 18 | void collect_compilers(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 19 | void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 20 | |||
| 21 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdbool.h> | ||
| 3 | #include <stdlib.h> | ||
| 4 | #include <string.h> | ||
| 5 | #include <sys/types.h> | ||
| 6 | #include "config.h" | ||
| 7 | #include "commands.h" | ||
| 8 | #include "editor.h" | ||
| 9 | #include "error.h" | ||
| 10 | #include "syntax/color.h" | ||
| 11 | #include "util/debug.h" | ||
| 12 | #include "util/readfile.h" | ||
| 13 | #include "util/str-util.h" | ||
| 14 | #include "../build/builtin-config.h" | ||
| 15 | |||
| 16 | ConfigState current_config; | ||
| 17 | |||
| 18 | // Odd number of backslashes at end of line? | ||
| 19 | static bool has_line_continuation(StringView line) | ||
| 20 | { | ||
| 21 | ssize_t pos = line.length - 1; | ||
| 22 | while (pos >= 0 && line.data[pos] == '\\') { | ||
| 23 | pos--; | ||
| 24 | } | ||
| 25 | return (line.length - 1 - pos) & 1; | ||
| 26 | } | ||
| 27 | |||
| 28 | UNITTEST { | ||
| 29 | BUG_ON(has_line_continuation(string_view(NULL, 0))); | ||
| 30 | BUG_ON(has_line_continuation(strview_from_cstring("0"))); | ||
| 31 | BUG_ON(!has_line_continuation(strview_from_cstring("1 \\"))); | ||
| 32 | BUG_ON(has_line_continuation(strview_from_cstring("2 \\\\"))); | ||
| 33 | BUG_ON(!has_line_continuation(strview_from_cstring("3 \\\\\\"))); | ||
| 34 | BUG_ON(has_line_continuation(strview_from_cstring("4 \\\\\\\\"))); | ||
| 35 | } | ||
| 36 | |||
| 37 | void exec_config(CommandRunner *runner, StringView config) | ||
| 38 | { | ||
| 39 | String buf = string_new(1024); | ||
| 40 | |||
| 41 | for (size_t i = 0, n = config.length; i < n; current_config.line++) { | ||
| 42 | StringView line = buf_slice_next_line(config.data, &i, n); | ||
| 43 | strview_trim_left(&line); | ||
| 44 | if (buf.len == 0 && strview_has_prefix(&line, "#")) { | ||
| 45 | // Comment line | ||
| 46 | continue; | ||
| 47 | } | ||
| 48 | if (has_line_continuation(line)) { | ||
| 49 | line.length--; | ||
| 50 | string_append_strview(&buf, &line); | ||
| 51 | } else { | ||
| 52 | string_append_strview(&buf, &line); | ||
| 53 | handle_command(runner, string_borrow_cstring(&buf)); | ||
| 54 | string_clear(&buf); | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | if (unlikely(buf.len)) { | ||
| 59 | // This can only happen if the last line had a line continuation | ||
| 60 | handle_command(runner, string_borrow_cstring(&buf)); | ||
| 61 | } | ||
| 62 | |||
| 63 | string_free(&buf); | ||
| 64 | } | ||
| 65 | |||
| 66 | String dump_builtin_configs(void) | ||
| 67 | { | ||
| 68 | String str = string_new(1024); | ||
| 69 | for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) { | ||
| 70 | string_append_cstring(&str, builtin_configs[i].name); | ||
| 71 | string_append_byte(&str, '\n'); | ||
| 72 | } | ||
| 73 | return str; | ||
| 74 | } | ||
| 75 | |||
| 76 | const BuiltinConfig *get_builtin_config(const char *name) | ||
| 77 | { | ||
| 78 | for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) { | ||
| 79 | if (streq(name, builtin_configs[i].name)) { | ||
| 80 | return &builtin_configs[i]; | ||
| 81 | } | ||
| 82 | } | ||
| 83 | return NULL; | ||
| 84 | } | ||
| 85 | |||
| 86 | const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs) | ||
| 87 | { | ||
| 88 | *nconfigs = ARRAYLEN(builtin_configs); | ||
| 89 | return &builtin_configs[0]; | ||
| 90 | } | ||
| 91 | |||
| 92 | int do_read_config(CommandRunner *runner, const char *filename, ConfigFlags flags) | ||
| 93 | { | ||
| 94 | const bool must_exist = flags & CFG_MUST_EXIST; | ||
| 95 | const bool builtin = flags & CFG_BUILTIN; | ||
| 96 | |||
| 97 | if (builtin) { | ||
| 98 | const BuiltinConfig *cfg = get_builtin_config(filename); | ||
| 99 | int err = 0; | ||
| 100 | if (cfg) { | ||
| 101 | current_config.file = filename; | ||
| 102 | current_config.line = 1; | ||
| 103 | exec_config(runner, cfg->text); | ||
| 104 | } else if (must_exist) { | ||
| 105 | error_msg ( | ||
| 106 | "Error reading '%s': no built-in config exists for that path", | ||
| 107 | filename | ||
| 108 | ); | ||
| 109 | err = 1; | ||
| 110 | } | ||
| 111 | return err; | ||
| 112 | } | ||
| 113 | |||
| 114 | char *buf; | ||
| 115 | ssize_t size = read_file(filename, &buf); | ||
| 116 | if (size < 0) { | ||
| 117 | int err = errno; | ||
| 118 | if (err != ENOENT || must_exist) { | ||
| 119 | error_msg("Error reading %s: %s", filename, strerror(err)); | ||
| 120 | } | ||
| 121 | return err; | ||
| 122 | } | ||
| 123 | |||
| 124 | current_config.file = filename; | ||
| 125 | current_config.line = 1; | ||
| 126 | exec_config(runner, string_view(buf, size)); | ||
| 127 | free(buf); | ||
| 128 | return 0; | ||
| 129 | } | ||
| 130 | |||
| 131 | int read_config(CommandRunner *runner, const char *filename, ConfigFlags flags) | ||
| 132 | { | ||
| 133 | // Recursive | ||
| 134 | const ConfigState saved = current_config; | ||
| 135 | int ret = do_read_config(runner, filename, flags); | ||
| 136 | current_config = saved; | ||
| 137 | return ret; | ||
| 138 | } | ||
| 139 | |||
| 140 | void exec_builtin_color_reset(EditorState *e) | ||
| 141 | { | ||
| 142 | clear_hl_colors(&e->colors); | ||
| 143 | const StringView reset = string_view(builtin_color_reset, sizeof(builtin_color_reset) - 1); | ||
| 144 | const ConfigState saved = current_config; | ||
| 145 | current_config.file = "color/reset"; | ||
| 146 | current_config.line = 1; | ||
| 147 | exec_normal_config(e, reset); | ||
| 148 | current_config = saved; | ||
| 149 | } | ||
| 150 | |||
| 151 | void exec_builtin_rc(EditorState *e) | ||
| 152 | { | ||
| 153 | exec_builtin_color_reset(e); | ||
| 154 | const StringView rc = string_view(builtin_rc, sizeof(builtin_rc) - 1); | ||
| 155 | const ConfigState saved = current_config; | ||
| 156 | current_config.file = "rc"; | ||
| 157 | current_config.line = 1; | ||
| 158 | exec_normal_config(e, rc); | ||
| 159 | current_config = saved; | ||
| 160 | } | ||
| 161 | |||
| 162 | void collect_builtin_configs(PointerArray *a, const char *prefix) | ||
| 163 | { | ||
| 164 | for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) { | ||
| 165 | const char *name = builtin_configs[i].name; | ||
| 166 | if (str_has_prefix(name, prefix)) { | ||
| 167 | ptr_array_append(a, xstrdup(name)); | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | void collect_builtin_includes(PointerArray *a, const char *prefix) | ||
| 173 | { | ||
| 174 | for (size_t i = 0; i < ARRAYLEN(builtin_configs); i++) { | ||
| 175 | const char *name = builtin_configs[i].name; | ||
| 176 | if (str_has_prefix(name, prefix) && !str_has_prefix(name, "syntax/")) { | ||
| 177 | ptr_array_append(a, xstrdup(name)); | ||
| 178 | } | ||
| 179 | } | ||
| 180 | } | ||
| 181 | |||
| 182 | UNITTEST { | ||
| 183 | BUG_ON(!get_builtin_config("rc")); | ||
| 184 | BUG_ON(!get_builtin_config("color/reset")); | ||
| 185 | } | ||
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 @@ | |||
| 1 | #ifndef CONFIG_H | ||
| 2 | #define CONFIG_H | ||
| 3 | |||
| 4 | #include <stddef.h> | ||
| 5 | #include "command/run.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/ptr-array.h" | ||
| 8 | #include "util/string-view.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | |||
| 11 | typedef enum { | ||
| 12 | CFG_NOFLAGS = 0, | ||
| 13 | CFG_MUST_EXIST = 1 << 0, | ||
| 14 | CFG_BUILTIN = 1 << 1 | ||
| 15 | } ConfigFlags; | ||
| 16 | |||
| 17 | typedef struct { | ||
| 18 | const char *const name; | ||
| 19 | const StringView text; | ||
| 20 | } BuiltinConfig; | ||
| 21 | |||
| 22 | typedef struct { | ||
| 23 | const char *file; | ||
| 24 | unsigned int line; | ||
| 25 | } ConfigState; | ||
| 26 | |||
| 27 | extern ConfigState current_config; | ||
| 28 | |||
| 29 | struct EditorState; | ||
| 30 | |||
| 31 | String dump_builtin_configs(void); | ||
| 32 | const BuiltinConfig *get_builtin_config(const char *name) PURE; | ||
| 33 | const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs); | ||
| 34 | void exec_config(CommandRunner *runner, StringView config); | ||
| 35 | int do_read_config(CommandRunner *runner, const char *filename, ConfigFlags flags) WARN_UNUSED_RESULT; | ||
| 36 | int read_config(CommandRunner *runner, const char *filename, ConfigFlags f); | ||
| 37 | void exec_builtin_color_reset(struct EditorState *e); | ||
| 38 | void exec_builtin_rc(struct EditorState *e); | ||
| 39 | void collect_builtin_configs(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 40 | void collect_builtin_includes(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 41 | |||
| 42 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <inttypes.h> | ||
| 3 | #include <stdlib.h> | ||
| 4 | #include <string.h> | ||
| 5 | #include "convert.h" | ||
| 6 | #include "util/debug.h" | ||
| 7 | #include "util/intern.h" | ||
| 8 | #include "util/log.h" | ||
| 9 | #include "util/str-util.h" | ||
| 10 | #include "util/utf8.h" | ||
| 11 | #include "util/xmalloc.h" | ||
| 12 | #include "util/xreadwrite.h" | ||
| 13 | |||
| 14 | struct FileEncoder { | ||
| 15 | struct cconv *cconv; | ||
| 16 | unsigned char *nbuf; | ||
| 17 | size_t nsize; | ||
| 18 | bool crlf; | ||
| 19 | int fd; | ||
| 20 | }; | ||
| 21 | |||
| 22 | struct FileDecoder { | ||
| 23 | const char *encoding; | ||
| 24 | const unsigned char *ibuf; | ||
| 25 | ssize_t ipos, isize; | ||
| 26 | struct cconv *cconv; | ||
| 27 | bool (*read_line)(struct FileDecoder *dec, const char **linep, size_t *lenp); | ||
| 28 | }; | ||
| 29 | |||
| 30 | const char *file_decoder_get_encoding(const FileDecoder *dec) | ||
| 31 | { | ||
| 32 | return dec->encoding; | ||
| 33 | } | ||
| 34 | |||
| 35 | static bool read_utf8_line(FileDecoder *dec, const char **linep, size_t *lenp) | ||
| 36 | { | ||
| 37 | const char *line = dec->ibuf + dec->ipos; | ||
| 38 | const char *nl = memchr(line, '\n', dec->isize - dec->ipos); | ||
| 39 | size_t len; | ||
| 40 | |||
| 41 | if (nl) { | ||
| 42 | len = nl - line; | ||
| 43 | dec->ipos += len + 1; | ||
| 44 | } else { | ||
| 45 | len = dec->isize - dec->ipos; | ||
| 46 | if (len == 0) { | ||
| 47 | return false; | ||
| 48 | } | ||
| 49 | dec->ipos += len; | ||
| 50 | } | ||
| 51 | |||
| 52 | *linep = line; | ||
| 53 | *lenp = len; | ||
| 54 | return true; | ||
| 55 | } | ||
| 56 | |||
| 57 | static size_t unix_to_dos ( | ||
| 58 | FileEncoder *enc, | ||
| 59 | const unsigned char *buf, | ||
| 60 | size_t size | ||
| 61 | ) { | ||
| 62 | if (enc->nsize < size * 2) { | ||
| 63 | enc->nsize = size * 2; | ||
| 64 | xrenew(enc->nbuf, enc->nsize); | ||
| 65 | } | ||
| 66 | size_t d = 0; | ||
| 67 | for (size_t s = 0; s < size; s++) { | ||
| 68 | unsigned char ch = buf[s]; | ||
| 69 | if (ch == '\n') { | ||
| 70 | enc->nbuf[d++] = '\r'; | ||
| 71 | } | ||
| 72 | enc->nbuf[d++] = ch; | ||
| 73 | } | ||
| 74 | return d; | ||
| 75 | } | ||
| 76 | |||
| 77 | #ifdef ICONV_DISABLE // iconv not available; use basic, UTF-8 implementation: | ||
| 78 | |||
| 79 | bool conversion_supported_by_iconv ( | ||
| 80 | const char* UNUSED_ARG(from), | ||
| 81 | const char* UNUSED_ARG(to) | ||
| 82 | ) { | ||
| 83 | errno = EINVAL; | ||
| 84 | return false; | ||
| 85 | } | ||
| 86 | |||
| 87 | FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd) | ||
| 88 | { | ||
| 89 | if (unlikely(encoding->type != UTF8)) { | ||
| 90 | errno = EINVAL; | ||
| 91 | return NULL; | ||
| 92 | } | ||
| 93 | FileEncoder *enc = xnew0(FileEncoder, 1); | ||
| 94 | enc->crlf = crlf; | ||
| 95 | enc->fd = fd; | ||
| 96 | return enc; | ||
| 97 | } | ||
| 98 | |||
| 99 | void free_file_encoder(FileEncoder *enc) | ||
| 100 | { | ||
| 101 | free(enc->nbuf); | ||
| 102 | free(enc); | ||
| 103 | } | ||
| 104 | |||
| 105 | ssize_t file_encoder_write(FileEncoder *enc, const unsigned char *buf, size_t n) | ||
| 106 | { | ||
| 107 | if (enc->crlf) { | ||
| 108 | n = unix_to_dos(enc, buf, n); | ||
| 109 | buf = enc->nbuf; | ||
| 110 | } | ||
| 111 | return xwrite_all(enc->fd, buf, n); | ||
| 112 | } | ||
| 113 | |||
| 114 | size_t file_encoder_get_nr_errors(const FileEncoder* UNUSED_ARG(enc)) | ||
| 115 | { | ||
| 116 | return 0; | ||
| 117 | } | ||
| 118 | |||
| 119 | FileDecoder *new_file_decoder(const char *encoding, const unsigned char *buf, size_t n) | ||
| 120 | { | ||
| 121 | if (unlikely(encoding && !streq(encoding, "UTF-8"))) { | ||
| 122 | errno = EINVAL; | ||
| 123 | return NULL; | ||
| 124 | } | ||
| 125 | FileDecoder *dec = xnew0(FileDecoder, 1); | ||
| 126 | dec->ibuf = buf; | ||
| 127 | dec->isize = n; | ||
| 128 | return dec; | ||
| 129 | } | ||
| 130 | |||
| 131 | void free_file_decoder(FileDecoder *dec) | ||
| 132 | { | ||
| 133 | free(dec); | ||
| 134 | } | ||
| 135 | |||
| 136 | bool file_decoder_read_line(FileDecoder *dec, const char **linep, size_t *lenp) | ||
| 137 | { | ||
| 138 | return read_utf8_line(dec, linep, lenp); | ||
| 139 | } | ||
| 140 | |||
| 141 | #else // ICONV_DISABLE is undefined; use full iconv implementation: | ||
| 142 | |||
| 143 | #include <iconv.h> | ||
| 144 | |||
| 145 | static const unsigned char replacement[2] = "\xc2\xbf"; // U+00BF | ||
| 146 | |||
| 147 | struct cconv { | ||
| 148 | iconv_t cd; | ||
| 149 | char *obuf; | ||
| 150 | size_t osize; | ||
| 151 | size_t opos; | ||
| 152 | size_t consumed; | ||
| 153 | size_t errors; | ||
| 154 | |||
| 155 | // Temporary input buffer | ||
| 156 | char tbuf[16]; | ||
| 157 | size_t tcount; | ||
| 158 | |||
| 159 | // Replacement character 0xBF (inverted question mark) | ||
| 160 | char rbuf[4]; | ||
| 161 | size_t rcount; | ||
| 162 | |||
| 163 | // Input character size in bytes, or zero for UTF-8 | ||
| 164 | size_t char_size; | ||
| 165 | }; | ||
| 166 | |||
| 167 | static struct cconv *create(iconv_t cd) | ||
| 168 | { | ||
| 169 | struct cconv *c = xnew0(struct cconv, 1); | ||
| 170 | c->cd = cd; | ||
| 171 | c->osize = 8192; | ||
| 172 | c->obuf = xmalloc(c->osize); | ||
| 173 | return c; | ||
| 174 | } | ||
| 175 | |||
| 176 | static size_t encoding_char_size(const char *encoding) | ||
| 177 | { | ||
| 178 | if (str_has_prefix(encoding, "UTF-16")) { | ||
| 179 | return 2; | ||
| 180 | } | ||
| 181 | if (str_has_prefix(encoding, "UTF-32")) { | ||
| 182 | return 4; | ||
| 183 | } | ||
| 184 | return 1; | ||
| 185 | } | ||
| 186 | |||
| 187 | static size_t iconv_wrapper ( | ||
| 188 | iconv_t cd, | ||
| 189 | const char **restrict inbuf, | ||
| 190 | size_t *restrict inbytesleft, | ||
| 191 | char **restrict outbuf, | ||
| 192 | size_t *restrict outbytesleft | ||
| 193 | ) { | ||
| 194 | // POSIX defines the second parameter of iconv(3) as "char **restrict" | ||
| 195 | // but NetBSD declares it as "const char **restrict" | ||
| 196 | #ifdef __NetBSD__ | ||
| 197 | const char **restrict in = inbuf; | ||
| 198 | #else | ||
| 199 | char **restrict in = (char **restrict)inbuf; | ||
| 200 | #endif | ||
| 201 | |||
| 202 | return iconv(cd, in, inbytesleft, outbuf, outbytesleft); | ||
| 203 | } | ||
| 204 | |||
| 205 | static void encode_replacement(struct cconv *c) | ||
| 206 | { | ||
| 207 | const char *ib = replacement; | ||
| 208 | char *ob = c->rbuf; | ||
| 209 | size_t ic = sizeof(replacement); | ||
| 210 | size_t oc = sizeof(c->rbuf); | ||
| 211 | size_t rc = iconv_wrapper(c->cd, &ib, &ic, &ob, &oc); | ||
| 212 | |||
| 213 | if (rc == (size_t)-1) { | ||
| 214 | c->rbuf[0] = '\xbf'; | ||
| 215 | c->rcount = 1; | ||
| 216 | } else { | ||
| 217 | c->rcount = ob - c->rbuf; | ||
| 218 | } | ||
| 219 | } | ||
| 220 | |||
| 221 | static void resize_obuf(struct cconv *c) | ||
| 222 | { | ||
| 223 | c->osize *= 2; | ||
| 224 | xrenew(c->obuf, c->osize); | ||
| 225 | } | ||
| 226 | |||
| 227 | static void add_replacement(struct cconv *c) | ||
| 228 | { | ||
| 229 | if (c->osize - c->opos < 4) { | ||
| 230 | resize_obuf(c); | ||
| 231 | } | ||
| 232 | |||
| 233 | memcpy(c->obuf + c->opos, c->rbuf, c->rcount); | ||
| 234 | c->opos += c->rcount; | ||
| 235 | } | ||
| 236 | |||
| 237 | static size_t handle_invalid(struct cconv *c, const char *buf, size_t count) | ||
| 238 | { | ||
| 239 | LOG_DEBUG("%zu %zu", c->char_size, count); | ||
| 240 | add_replacement(c); | ||
| 241 | if (c->char_size == 0) { | ||
| 242 | // Converting from UTF-8 | ||
| 243 | size_t idx = 0; | ||
| 244 | CodePoint u = u_get_char(buf, count, &idx); | ||
| 245 | LOG_DEBUG("U+%04" PRIX32, u); | ||
| 246 | return idx; | ||
| 247 | } | ||
| 248 | if (c->char_size > count) { | ||
| 249 | // wtf | ||
| 250 | return 1; | ||
| 251 | } | ||
| 252 | return c->char_size; | ||
| 253 | } | ||
| 254 | |||
| 255 | static int xiconv(struct cconv *c, const char **ib, size_t *ic) | ||
| 256 | { | ||
| 257 | while (1) { | ||
| 258 | char *ob = c->obuf + c->opos; | ||
| 259 | size_t oc = c->osize - c->opos; | ||
| 260 | size_t rc = iconv_wrapper(c->cd, ib, ic, &ob, &oc); | ||
| 261 | c->opos = ob - c->obuf; | ||
| 262 | if (rc == (size_t)-1) { | ||
| 263 | switch (errno) { | ||
| 264 | case EILSEQ: | ||
| 265 | c->errors++; | ||
| 266 | // Reset | ||
| 267 | iconv(c->cd, NULL, NULL, NULL, NULL); | ||
| 268 | return errno; | ||
| 269 | case EINVAL: | ||
| 270 | return errno; | ||
| 271 | case E2BIG: | ||
| 272 | resize_obuf(c); | ||
| 273 | continue; | ||
| 274 | default: | ||
| 275 | BUG("iconv: %s", strerror(errno)); | ||
| 276 | } | ||
| 277 | } else { | ||
| 278 | c->errors += rc; | ||
| 279 | } | ||
| 280 | return 0; | ||
| 281 | } | ||
| 282 | } | ||
| 283 | |||
| 284 | static size_t convert_incomplete(struct cconv *c, const char *input, size_t len) | ||
| 285 | { | ||
| 286 | size_t ipos = 0; | ||
| 287 | while (c->tcount < sizeof(c->tbuf) && ipos < len) { | ||
| 288 | c->tbuf[c->tcount++] = input[ipos++]; | ||
| 289 | const char *ib = c->tbuf; | ||
| 290 | size_t ic = c->tcount; | ||
| 291 | int rc = xiconv(c, &ib, &ic); | ||
| 292 | if (ic > 0) { | ||
| 293 | memmove(c->tbuf, ib, ic); | ||
| 294 | } | ||
| 295 | c->tcount = ic; | ||
| 296 | if (rc == EINVAL) { | ||
| 297 | // Incomplete character at end of input buffer; try again | ||
| 298 | // with more input data | ||
| 299 | continue; | ||
| 300 | } | ||
| 301 | if (rc == EILSEQ) { | ||
| 302 | // Invalid multibyte sequence | ||
| 303 | size_t skip = handle_invalid(c, c->tbuf, c->tcount); | ||
| 304 | c->tcount -= skip; | ||
| 305 | if (c->tcount > 0) { | ||
| 306 | LOG_DEBUG("tcount=%zu, skip=%zu", c->tcount, skip); | ||
| 307 | memmove(c->tbuf, c->tbuf + skip, c->tcount); | ||
| 308 | continue; | ||
| 309 | } | ||
| 310 | return ipos; | ||
| 311 | } | ||
| 312 | break; | ||
| 313 | } | ||
| 314 | |||
| 315 | LOG_DEBUG("%zu %zu", ipos, c->tcount); | ||
| 316 | return ipos; | ||
| 317 | } | ||
| 318 | |||
| 319 | static void cconv_process(struct cconv *c, const char *input, size_t len) | ||
| 320 | { | ||
| 321 | if (c->consumed > 0) { | ||
| 322 | size_t fill = c->opos - c->consumed; | ||
| 323 | memmove(c->obuf, c->obuf + c->consumed, fill); | ||
| 324 | c->opos = fill; | ||
| 325 | c->consumed = 0; | ||
| 326 | } | ||
| 327 | |||
| 328 | if (c->tcount > 0) { | ||
| 329 | size_t ipos = convert_incomplete(c, input, len); | ||
| 330 | input += ipos; | ||
| 331 | len -= ipos; | ||
| 332 | } | ||
| 333 | |||
| 334 | const char *ib = input; | ||
| 335 | for (size_t ic = len; ic > 0; ) { | ||
| 336 | int r = xiconv(c, &ib, &ic); | ||
| 337 | if (r == EINVAL) { | ||
| 338 | // Incomplete character at end of input buffer | ||
| 339 | if (ic < sizeof(c->tbuf)) { | ||
| 340 | memcpy(c->tbuf, ib, ic); | ||
| 341 | c->tcount = ic; | ||
| 342 | } else { | ||
| 343 | // FIXME | ||
| 344 | } | ||
| 345 | ic = 0; | ||
| 346 | continue; | ||
| 347 | } | ||
| 348 | if (r == EILSEQ) { | ||
| 349 | // Invalid multibyte sequence | ||
| 350 | size_t skip = handle_invalid(c, ib, ic); | ||
| 351 | ic -= skip; | ||
| 352 | ib += skip; | ||
| 353 | continue; | ||
| 354 | } | ||
| 355 | } | ||
| 356 | } | ||
| 357 | |||
| 358 | static struct cconv *cconv_to_utf8(const char *encoding) | ||
| 359 | { | ||
| 360 | iconv_t cd = iconv_open("UTF-8", encoding); | ||
| 361 | if (cd == (iconv_t)-1) { | ||
| 362 | return NULL; | ||
| 363 | } | ||
| 364 | struct cconv *c = create(cd); | ||
| 365 | memcpy(c->rbuf, replacement, sizeof(replacement)); | ||
| 366 | c->rcount = sizeof(replacement); | ||
| 367 | c->char_size = encoding_char_size(encoding); | ||
| 368 | return c; | ||
| 369 | } | ||
| 370 | |||
| 371 | static struct cconv *cconv_from_utf8(const char *encoding) | ||
| 372 | { | ||
| 373 | iconv_t cd = iconv_open(encoding, "UTF-8"); | ||
| 374 | if (cd == (iconv_t)-1) { | ||
| 375 | return NULL; | ||
| 376 | } | ||
| 377 | struct cconv *c = create(cd); | ||
| 378 | encode_replacement(c); | ||
| 379 | return c; | ||
| 380 | } | ||
| 381 | |||
| 382 | static void cconv_flush(struct cconv *c) | ||
| 383 | { | ||
| 384 | if (c->tcount > 0) { | ||
| 385 | // Replace incomplete character at end of input buffer | ||
| 386 | LOG_DEBUG("incomplete character at EOF"); | ||
| 387 | add_replacement(c); | ||
| 388 | c->tcount = 0; | ||
| 389 | } | ||
| 390 | } | ||
| 391 | |||
| 392 | static char *cconv_consume_line(struct cconv *c, size_t *len) | ||
| 393 | { | ||
| 394 | char *line = c->obuf + c->consumed; | ||
| 395 | char *nl = memchr(line, '\n', c->opos - c->consumed); | ||
| 396 | if (!nl) { | ||
| 397 | *len = 0; | ||
| 398 | return NULL; | ||
| 399 | } | ||
| 400 | |||
| 401 | size_t n = nl - line + 1; | ||
| 402 | c->consumed += n; | ||
| 403 | *len = n; | ||
| 404 | return line; | ||
| 405 | } | ||
| 406 | |||
| 407 | static char *cconv_consume_all(struct cconv *c, size_t *len) | ||
| 408 | { | ||
| 409 | char *buf = c->obuf + c->consumed; | ||
| 410 | *len = c->opos - c->consumed; | ||
| 411 | c->consumed = c->opos; | ||
| 412 | return buf; | ||
| 413 | } | ||
| 414 | |||
| 415 | static void cconv_free(struct cconv *c) | ||
| 416 | { | ||
| 417 | iconv_close(c->cd); | ||
| 418 | free(c->obuf); | ||
| 419 | free(c); | ||
| 420 | } | ||
| 421 | |||
| 422 | bool conversion_supported_by_iconv(const char *from, const char *to) | ||
| 423 | { | ||
| 424 | if (unlikely(from[0] == '\0' || to[0] == '\0')) { | ||
| 425 | errno = EINVAL; | ||
| 426 | return false; | ||
| 427 | } | ||
| 428 | |||
| 429 | iconv_t cd = iconv_open(to, from); | ||
| 430 | if (cd == (iconv_t)-1) { | ||
| 431 | return false; | ||
| 432 | } | ||
| 433 | |||
| 434 | iconv_close(cd); | ||
| 435 | return true; | ||
| 436 | } | ||
| 437 | |||
| 438 | FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd) | ||
| 439 | { | ||
| 440 | FileEncoder *enc = xnew0(FileEncoder, 1); | ||
| 441 | enc->crlf = crlf; | ||
| 442 | enc->fd = fd; | ||
| 443 | |||
| 444 | if (encoding->type != UTF8) { | ||
| 445 | enc->cconv = cconv_from_utf8(encoding->name); | ||
| 446 | if (!enc->cconv) { | ||
| 447 | free(enc); | ||
| 448 | return NULL; | ||
| 449 | } | ||
| 450 | } | ||
| 451 | |||
| 452 | return enc; | ||
| 453 | } | ||
| 454 | |||
| 455 | void free_file_encoder(FileEncoder *enc) | ||
| 456 | { | ||
| 457 | if (enc->cconv) { | ||
| 458 | cconv_free(enc->cconv); | ||
| 459 | } | ||
| 460 | free(enc->nbuf); | ||
| 461 | free(enc); | ||
| 462 | } | ||
| 463 | |||
| 464 | // NOTE: buf must contain whole characters! | ||
| 465 | ssize_t file_encoder_write ( | ||
| 466 | FileEncoder *enc, | ||
| 467 | const unsigned char *buf, | ||
| 468 | size_t size | ||
| 469 | ) { | ||
| 470 | if (enc->crlf) { | ||
| 471 | size = unix_to_dos(enc, buf, size); | ||
| 472 | buf = enc->nbuf; | ||
| 473 | } | ||
| 474 | if (enc->cconv) { | ||
| 475 | cconv_process(enc->cconv, buf, size); | ||
| 476 | cconv_flush(enc->cconv); | ||
| 477 | buf = cconv_consume_all(enc->cconv, &size); | ||
| 478 | } | ||
| 479 | return xwrite_all(enc->fd, buf, size); | ||
| 480 | } | ||
| 481 | |||
| 482 | size_t file_encoder_get_nr_errors(const FileEncoder *enc) | ||
| 483 | { | ||
| 484 | return enc->cconv ? enc->cconv->errors : 0; | ||
| 485 | } | ||
| 486 | |||
| 487 | static bool fill(FileDecoder *dec) | ||
| 488 | { | ||
| 489 | if (dec->ipos == dec->isize) { | ||
| 490 | return false; | ||
| 491 | } | ||
| 492 | |||
| 493 | // Smaller than cconv.obuf to make realloc less likely | ||
| 494 | size_t max = 7 * 1024; | ||
| 495 | |||
| 496 | size_t icount = MIN(dec->isize - dec->ipos, max); | ||
| 497 | cconv_process(dec->cconv, dec->ibuf + dec->ipos, icount); | ||
| 498 | dec->ipos += icount; | ||
| 499 | if (dec->ipos == dec->isize) { | ||
| 500 | // Must be flushed after all input has been fed | ||
| 501 | cconv_flush(dec->cconv); | ||
| 502 | } | ||
| 503 | return true; | ||
| 504 | } | ||
| 505 | |||
| 506 | static bool decode_and_read_line(FileDecoder *dec, const char **linep, size_t *lenp) | ||
| 507 | { | ||
| 508 | char *line; | ||
| 509 | size_t len; | ||
| 510 | while (1) { | ||
| 511 | line = cconv_consume_line(dec->cconv, &len); | ||
| 512 | if (line || !fill(dec)) { | ||
| 513 | break; | ||
| 514 | } | ||
| 515 | } | ||
| 516 | |||
| 517 | if (line) { | ||
| 518 | // Newline not wanted | ||
| 519 | len--; | ||
| 520 | } else { | ||
| 521 | line = cconv_consume_all(dec->cconv, &len); | ||
| 522 | if (len == 0) { | ||
| 523 | return false; | ||
| 524 | } | ||
| 525 | } | ||
| 526 | |||
| 527 | *linep = line; | ||
| 528 | *lenp = len; | ||
| 529 | return true; | ||
| 530 | } | ||
| 531 | |||
| 532 | static bool set_encoding(FileDecoder *dec, const char *encoding) | ||
| 533 | { | ||
| 534 | if (strcmp(encoding, "UTF-8") == 0) { | ||
| 535 | dec->read_line = read_utf8_line; | ||
| 536 | } else { | ||
| 537 | dec->cconv = cconv_to_utf8(encoding); | ||
| 538 | if (!dec->cconv) { | ||
| 539 | return false; | ||
| 540 | } | ||
| 541 | dec->read_line = decode_and_read_line; | ||
| 542 | } | ||
| 543 | dec->encoding = str_intern(encoding); | ||
| 544 | return true; | ||
| 545 | } | ||
| 546 | |||
| 547 | FileDecoder *new_file_decoder ( | ||
| 548 | const char *encoding, | ||
| 549 | const unsigned char *buf, | ||
| 550 | size_t size | ||
| 551 | ) { | ||
| 552 | FileDecoder *dec = xnew0(FileDecoder, 1); | ||
| 553 | dec->ibuf = buf; | ||
| 554 | dec->isize = size; | ||
| 555 | |||
| 556 | if (!encoding) { | ||
| 557 | encoding = "UTF-8"; | ||
| 558 | } | ||
| 559 | |||
| 560 | if (!set_encoding(dec, encoding)) { | ||
| 561 | free_file_decoder(dec); | ||
| 562 | return NULL; | ||
| 563 | } | ||
| 564 | |||
| 565 | return dec; | ||
| 566 | } | ||
| 567 | |||
| 568 | void free_file_decoder(FileDecoder *dec) | ||
| 569 | { | ||
| 570 | if (dec->cconv) { | ||
| 571 | cconv_free(dec->cconv); | ||
| 572 | } | ||
| 573 | free(dec); | ||
| 574 | } | ||
| 575 | |||
| 576 | bool file_decoder_read_line(FileDecoder *dec, const char **linep, size_t *lenp) | ||
| 577 | { | ||
| 578 | return dec->read_line(dec, linep, lenp); | ||
| 579 | } | ||
| 580 | |||
| 581 | #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 @@ | |||
| 1 | #ifndef ENCODING_CONVERT_H | ||
| 2 | #define ENCODING_CONVERT_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <sys/types.h> | ||
| 6 | #include "encoding.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | |||
| 9 | typedef struct FileDecoder FileDecoder; | ||
| 10 | typedef struct FileEncoder FileEncoder; | ||
| 11 | |||
| 12 | bool conversion_supported_by_iconv(const char *from, const char *to) NONNULL_ARGS; | ||
| 13 | |||
| 14 | FileDecoder *new_file_decoder(const char *encoding, const unsigned char *buf, size_t size); | ||
| 15 | void free_file_decoder(FileDecoder *dec); | ||
| 16 | bool file_decoder_read_line(FileDecoder *dec, const char **line, size_t *len) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 17 | const char *file_decoder_get_encoding(const FileDecoder *dec) NONNULL_ARGS; | ||
| 18 | |||
| 19 | FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd) NONNULL_ARGS; | ||
| 20 | void free_file_encoder(FileEncoder *enc) NONNULL_ARGS; | ||
| 21 | ssize_t file_encoder_write(FileEncoder *enc, const unsigned char *buf, size_t size) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 22 | size_t file_encoder_get_nr_errors(const FileEncoder *enc) NONNULL_ARGS; | ||
| 23 | |||
| 24 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include "copy.h" | ||
| 3 | #include "block-iter.h" | ||
| 4 | #include "change.h" | ||
| 5 | #include "misc.h" | ||
| 6 | #include "move.h" | ||
| 7 | #include "selection.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | |||
| 10 | void record_copy(Clipboard *clip, char *buf, size_t len, bool is_lines) | ||
| 11 | { | ||
| 12 | BUG_ON(len && !buf); | ||
| 13 | free(clip->buf); | ||
| 14 | clip->buf = buf; | ||
| 15 | clip->len = len; | ||
| 16 | clip->is_lines = is_lines; | ||
| 17 | } | ||
| 18 | |||
| 19 | void copy(Clipboard *clip, View *view, size_t len, bool is_lines) | ||
| 20 | { | ||
| 21 | if (len) { | ||
| 22 | char *buf = block_iter_get_bytes(&view->cursor, len); | ||
| 23 | record_copy(clip, buf, len, is_lines); | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | void cut(Clipboard *clip, View *view, size_t len, bool is_lines) | ||
| 28 | { | ||
| 29 | if (len) { | ||
| 30 | copy(clip, view, len, is_lines); | ||
| 31 | buffer_delete_bytes(view, len); | ||
| 32 | } | ||
| 33 | } | ||
| 34 | |||
| 35 | void paste(Clipboard *clip, View *view, PasteLinesType type, bool move_after) | ||
| 36 | { | ||
| 37 | if (clip->len == 0) { | ||
| 38 | return; | ||
| 39 | } | ||
| 40 | |||
| 41 | BUG_ON(!clip->buf); | ||
| 42 | if (!clip->is_lines || type == PASTE_LINES_INLINE) { | ||
| 43 | insert_text(view, clip->buf, clip->len, move_after); | ||
| 44 | return; | ||
| 45 | } | ||
| 46 | |||
| 47 | size_t del_count = 0; | ||
| 48 | if (view->selection) { | ||
| 49 | del_count = prepare_selection(view); | ||
| 50 | unselect(view); | ||
| 51 | } | ||
| 52 | |||
| 53 | const long x = view_get_preferred_x(view); | ||
| 54 | if (!del_count) { | ||
| 55 | if (type == PASTE_LINES_BELOW_CURSOR) { | ||
| 56 | block_iter_eat_line(&view->cursor); | ||
| 57 | } else { | ||
| 58 | BUG_ON(type != PASTE_LINES_ABOVE_CURSOR); | ||
| 59 | block_iter_bol(&view->cursor); | ||
| 60 | } | ||
| 61 | } | ||
| 62 | |||
| 63 | buffer_replace_bytes(view, del_count, clip->buf, clip->len); | ||
| 64 | |||
| 65 | if (move_after) { | ||
| 66 | block_iter_skip_bytes(&view->cursor, clip->len); | ||
| 67 | } else { | ||
| 68 | // Try to keep cursor column | ||
| 69 | move_to_preferred_x(view, x); | ||
| 70 | } | ||
| 71 | |||
| 72 | // New preferred_x | ||
| 73 | view_reset_preferred_x(view); | ||
| 74 | } | ||
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 @@ | |||
| 1 | #ifndef COPY_H | ||
| 2 | #define COPY_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | typedef struct { | ||
| 10 | char *buf; | ||
| 11 | size_t len; | ||
| 12 | bool is_lines; | ||
| 13 | } Clipboard; | ||
| 14 | |||
| 15 | typedef enum { | ||
| 16 | PASTE_LINES_BELOW_CURSOR, | ||
| 17 | PASTE_LINES_ABOVE_CURSOR, | ||
| 18 | PASTE_LINES_INLINE, | ||
| 19 | } PasteLinesType; | ||
| 20 | |||
| 21 | void record_copy(Clipboard *clip, char *buf, size_t len, bool is_lines); | ||
| 22 | void copy(Clipboard *clip, View *view, size_t len, bool is_lines); | ||
| 23 | void cut(Clipboard *clip, View *view, size_t len, bool is_lines); | ||
| 24 | void paste(Clipboard *clip, View *view, PasteLinesType type, bool move_after); | ||
| 25 | |||
| 26 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "ctags.h" | ||
| 4 | #include "util/ascii.h" | ||
| 5 | #include "util/debug.h" | ||
| 6 | #include "util/str-util.h" | ||
| 7 | #include "util/strtonum.h" | ||
| 8 | #include "util/xmalloc.h" | ||
| 9 | |||
| 10 | static size_t parse_ex_pattern(const char *buf, size_t size, char **escaped) | ||
| 11 | { | ||
| 12 | BUG_ON(size == 0); | ||
| 13 | BUG_ON(buf[0] != '/' && buf[0] != '?'); | ||
| 14 | |||
| 15 | // The search pattern is not a real regular expression; special characters | ||
| 16 | // need to be escaped | ||
| 17 | char *pattern = xmalloc(size * 2); | ||
| 18 | char open_delim = buf[0]; | ||
| 19 | for (size_t i = 1, j = 0; i < size; i++) { | ||
| 20 | if (unlikely(buf[i] == '\0')) { | ||
| 21 | break; | ||
| 22 | } | ||
| 23 | if (buf[i] == '\\' && i + 1 < size) { | ||
| 24 | i++; | ||
| 25 | if (buf[i] == '\\') { | ||
| 26 | pattern[j++] = '\\'; | ||
| 27 | } | ||
| 28 | pattern[j++] = buf[i]; | ||
| 29 | continue; | ||
| 30 | } | ||
| 31 | if (buf[i] == open_delim) { | ||
| 32 | pattern[j] = '\0'; | ||
| 33 | *escaped = pattern; | ||
| 34 | return i + 1; | ||
| 35 | } | ||
| 36 | char c = buf[i]; | ||
| 37 | if (c == '*' || c == '[' || c == ']') { | ||
| 38 | pattern[j++] = '\\'; | ||
| 39 | } | ||
| 40 | pattern[j++] = buf[i]; | ||
| 41 | } | ||
| 42 | |||
| 43 | free(pattern); | ||
| 44 | return 0; | ||
| 45 | } | ||
| 46 | |||
| 47 | static size_t parse_ex_cmd(Tag *tag, const char *buf, size_t size) | ||
| 48 | { | ||
| 49 | if (unlikely(size == 0)) { | ||
| 50 | return 0; | ||
| 51 | } | ||
| 52 | |||
| 53 | size_t n; | ||
| 54 | if (buf[0] == '/' || buf[0] == '?') { | ||
| 55 | n = parse_ex_pattern(buf, size, &tag->pattern); | ||
| 56 | } else { | ||
| 57 | n = buf_parse_ulong(buf, size, &tag->lineno); | ||
| 58 | } | ||
| 59 | |||
| 60 | if (n == 0) { | ||
| 61 | return 0; | ||
| 62 | } | ||
| 63 | |||
| 64 | if (n + 1 < size && buf[n] == ';' && buf[n + 1] == '"') { | ||
| 65 | n += 2; | ||
| 66 | } | ||
| 67 | |||
| 68 | return n; | ||
| 69 | } | ||
| 70 | |||
| 71 | bool parse_ctags_line(Tag *tag, const char *line, size_t line_len) | ||
| 72 | { | ||
| 73 | size_t pos = 0; | ||
| 74 | *tag = (Tag){.name = get_delim(line, &pos, line_len, '\t')}; | ||
| 75 | if (tag->name.length == 0 || pos >= line_len) { | ||
| 76 | return false; | ||
| 77 | } | ||
| 78 | |||
| 79 | tag->filename = get_delim(line, &pos, line_len, '\t'); | ||
| 80 | if (tag->filename.length == 0 || pos >= line_len) { | ||
| 81 | return false; | ||
| 82 | } | ||
| 83 | |||
| 84 | size_t len = parse_ex_cmd(tag, line + pos, line_len - pos); | ||
| 85 | if (len == 0) { | ||
| 86 | BUG_ON(tag->pattern); | ||
| 87 | return false; | ||
| 88 | } | ||
| 89 | |||
| 90 | pos += len; | ||
| 91 | if (pos >= line_len) { | ||
| 92 | return true; | ||
| 93 | } | ||
| 94 | |||
| 95 | /* | ||
| 96 | * Extension fields (key:[value]): | ||
| 97 | * | ||
| 98 | * file: visibility limited to this file | ||
| 99 | * struct:NAME tag is member of struct NAME | ||
| 100 | * union:NAME tag is member of union NAME | ||
| 101 | * typeref:struct:NAME::MEMBER_TYPE MEMBER_TYPE is type of the tag | ||
| 102 | */ | ||
| 103 | if (line[pos++] != '\t') { | ||
| 104 | // free `pattern` allocated by parse_ex_cmd() | ||
| 105 | free_tag(tag); | ||
| 106 | tag->pattern = NULL; | ||
| 107 | return false; | ||
| 108 | } | ||
| 109 | |||
| 110 | while (pos < line_len) { | ||
| 111 | StringView field = get_delim(line, &pos, line_len, '\t'); | ||
| 112 | if (field.length == 1 && ascii_isalpha(field.data[0])) { | ||
| 113 | tag->kind = field.data[0]; | ||
| 114 | } else if (strview_equal_cstring(&field, "file:")) { | ||
| 115 | tag->local = true; | ||
| 116 | } | ||
| 117 | // TODO: struct/union/typeref | ||
| 118 | } | ||
| 119 | |||
| 120 | return true; | ||
| 121 | } | ||
| 122 | |||
| 123 | bool next_tag ( | ||
| 124 | const char *buf, | ||
| 125 | size_t buf_len, | ||
| 126 | size_t *posp, | ||
| 127 | const StringView *prefix, | ||
| 128 | bool exact, | ||
| 129 | Tag *tag | ||
| 130 | ) { | ||
| 131 | const char *p = prefix->data; | ||
| 132 | size_t plen = prefix->length; | ||
| 133 | for (size_t pos = *posp; pos < buf_len; ) { | ||
| 134 | StringView line = buf_slice_next_line(buf, &pos, buf_len); | ||
| 135 | if (line.length == 0 || line.data[0] == '!') { | ||
| 136 | continue; | ||
| 137 | } | ||
| 138 | if (!strview_has_strn_prefix(&line, p, plen)) { | ||
| 139 | continue; | ||
| 140 | } | ||
| 141 | if (exact && line.data[plen] != '\t') { | ||
| 142 | continue; | ||
| 143 | } | ||
| 144 | if (!parse_ctags_line(tag, line.data, line.length)) { | ||
| 145 | continue; | ||
| 146 | } | ||
| 147 | *posp = pos; | ||
| 148 | return true; | ||
| 149 | } | ||
| 150 | return false; | ||
| 151 | } | ||
| 152 | |||
| 153 | // NOTE: tag itself is not freed | ||
| 154 | void free_tag(Tag *tag) | ||
| 155 | { | ||
| 156 | free(tag->pattern); | ||
| 157 | } | ||
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 @@ | |||
| 1 | #ifndef CTAGS_H | ||
| 2 | #define CTAGS_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/string-view.h" | ||
| 8 | |||
| 9 | typedef struct { | ||
| 10 | StringView name; // Name of tag (points into TagFile::buf) | ||
| 11 | StringView filename; // File containing tag (points into TagFile::buf) | ||
| 12 | char *pattern; // Regex pattern used to locate tag (escaped ex command) | ||
| 13 | unsigned long lineno; // Line number in file (mutually exclusive with pattern) | ||
| 14 | char kind; // ASCII letter representing type of tag (e.g. f=function) | ||
| 15 | bool local; // Indicates if tag is local to file (e.g. "static" in C) | ||
| 16 | } Tag; | ||
| 17 | |||
| 18 | NONNULL_ARGS WARN_UNUSED_RESULT | ||
| 19 | bool next_tag ( | ||
| 20 | const char *buf, | ||
| 21 | size_t buf_len, | ||
| 22 | size_t *posp, | ||
| 23 | const StringView *prefix, | ||
| 24 | bool exact, | ||
| 25 | Tag *t | ||
| 26 | ); | ||
| 27 | |||
| 28 | bool parse_ctags_line(Tag *t, const char *line, size_t line_len) NONNULL_ARG(1); | ||
| 29 | void free_tag(Tag *t) NONNULL_ARGS; | ||
| 30 | |||
| 31 | #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 @@ | |||
| 1 | #include <string.h> | ||
| 2 | #include "edit.h" | ||
| 3 | #include "block.h" | ||
| 4 | #include "buffer.h" | ||
| 5 | #include "syntax/highlight.h" | ||
| 6 | #include "util/debug.h" | ||
| 7 | #include "util/list.h" | ||
| 8 | #include "util/xmalloc.h" | ||
| 9 | |||
| 10 | enum { | ||
| 11 | BLOCK_EDIT_SIZE = 512 | ||
| 12 | }; | ||
| 13 | |||
| 14 | static void sanity_check_blocks(const View *view, bool check_newlines) | ||
| 15 | { | ||
| 16 | #if DEBUG >= 1 | ||
| 17 | const Buffer *buffer = view->buffer; | ||
| 18 | BUG_ON(list_empty(&buffer->blocks)); | ||
| 19 | BUG_ON(view->cursor.offset > view->cursor.blk->size); | ||
| 20 | |||
| 21 | const Block *blk = BLOCK(buffer->blocks.next); | ||
| 22 | if (blk->size == 0) { | ||
| 23 | // The only time a zero-sized block is valid is when it's the | ||
| 24 | // first and only block | ||
| 25 | BUG_ON(buffer->blocks.next->next != &buffer->blocks); | ||
| 26 | BUG_ON(view->cursor.blk != blk); | ||
| 27 | return; | ||
| 28 | } | ||
| 29 | |||
| 30 | bool cursor_seen = false; | ||
| 31 | block_for_each(blk, &buffer->blocks) { | ||
| 32 | const size_t size = blk->size; | ||
| 33 | BUG_ON(size == 0); | ||
| 34 | BUG_ON(size > blk->alloc); | ||
| 35 | if (blk == view->cursor.blk) { | ||
| 36 | cursor_seen = true; | ||
| 37 | } | ||
| 38 | if (check_newlines) { | ||
| 39 | BUG_ON(blk->data[size - 1] != '\n'); | ||
| 40 | } | ||
| 41 | if (DEBUG > 2) { | ||
| 42 | BUG_ON(count_nl(blk->data, size) != blk->nl); | ||
| 43 | } | ||
| 44 | } | ||
| 45 | BUG_ON(!cursor_seen); | ||
| 46 | #else | ||
| 47 | // Silence "unused parameter" warnings | ||
| 48 | (void)view; | ||
| 49 | (void)check_newlines; | ||
| 50 | #endif | ||
| 51 | } | ||
| 52 | |||
| 53 | static size_t copy_count_nl(char *dst, const char *src, size_t len) | ||
| 54 | { | ||
| 55 | size_t nl = 0; | ||
| 56 | for (size_t i = 0; i < len; i++) { | ||
| 57 | dst[i] = src[i]; | ||
| 58 | if (src[i] == '\n') { | ||
| 59 | nl++; | ||
| 60 | } | ||
| 61 | } | ||
| 62 | return nl; | ||
| 63 | } | ||
| 64 | |||
| 65 | static size_t insert_to_current(BlockIter *cursor, const char *buf, size_t len) | ||
| 66 | { | ||
| 67 | Block *blk = cursor->blk; | ||
| 68 | size_t offset = cursor->offset; | ||
| 69 | size_t size = blk->size + len; | ||
| 70 | |||
| 71 | if (size > blk->alloc) { | ||
| 72 | blk->alloc = round_size_to_next_multiple(size, BLOCK_ALLOC_MULTIPLE); | ||
| 73 | xrenew(blk->data, blk->alloc); | ||
| 74 | } | ||
| 75 | memmove(blk->data + offset + len, blk->data + offset, blk->size - offset); | ||
| 76 | size_t nl = copy_count_nl(blk->data + offset, buf, len); | ||
| 77 | blk->nl += nl; | ||
| 78 | blk->size = size; | ||
| 79 | return nl; | ||
| 80 | } | ||
| 81 | |||
| 82 | /* | ||
| 83 | * Combine current block and new data into smaller blocks: | ||
| 84 | * - Block _must_ contain whole lines | ||
| 85 | * - Block _must_ contain at least one line | ||
| 86 | * - Preferred maximum size of block is BLOCK_EDIT_SIZE | ||
| 87 | * - Size of any block can be larger than BLOCK_EDIT_SIZE | ||
| 88 | * only if there's a very long line | ||
| 89 | */ | ||
| 90 | static size_t split_and_insert(BlockIter *cursor, const char *buf, size_t len) | ||
| 91 | { | ||
| 92 | Block *blk = cursor->blk; | ||
| 93 | ListHead *prev_node = blk->node.prev; | ||
| 94 | const char *buf1 = blk->data; | ||
| 95 | const char *buf2 = buf; | ||
| 96 | const char *buf3 = blk->data + cursor->offset; | ||
| 97 | size_t size1 = cursor->offset; | ||
| 98 | size_t size2 = len; | ||
| 99 | size_t size3 = blk->size - size1; | ||
| 100 | size_t total = size1 + size2 + size3; | ||
| 101 | size_t start = 0; // Beginning of new block | ||
| 102 | size_t size = 0; // Size of new block | ||
| 103 | size_t pos = 0; // Current position | ||
| 104 | size_t nl_added = 0; | ||
| 105 | |||
| 106 | while (start < total) { | ||
| 107 | // Size of new block if next line would be added | ||
| 108 | size_t new_size = 0; | ||
| 109 | size_t copied = 0; | ||
| 110 | |||
| 111 | if (pos < size1) { | ||
| 112 | const char *nl = memchr(buf1 + pos, '\n', size1 - pos); | ||
| 113 | if (nl) { | ||
| 114 | new_size = nl - buf1 + 1 - start; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | if (!new_size && pos < size1 + size2) { | ||
| 119 | size_t offset = 0; | ||
| 120 | if (pos > size1) { | ||
| 121 | offset = pos - size1; | ||
| 122 | } | ||
| 123 | |||
| 124 | const char *nl = memchr(buf2 + offset, '\n', size2 - offset); | ||
| 125 | if (nl) { | ||
| 126 | new_size = size1 + nl - buf2 + 1 - start; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | if (!new_size && pos < total) { | ||
| 131 | size_t offset = 0; | ||
| 132 | if (pos > size1 + size2) { | ||
| 133 | offset = pos - size1 - size2; | ||
| 134 | } | ||
| 135 | |||
| 136 | const char *nl = memchr(buf3 + offset, '\n', size3 - offset); | ||
| 137 | if (nl) { | ||
| 138 | new_size = size1 + size2 + nl - buf3 + 1 - start; | ||
| 139 | } else { | ||
| 140 | new_size = total - start; | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | if (new_size <= BLOCK_EDIT_SIZE) { | ||
| 145 | // Fits | ||
| 146 | size = new_size; | ||
| 147 | pos = start + new_size; | ||
| 148 | if (pos < total) { | ||
| 149 | continue; | ||
| 150 | } | ||
| 151 | } else { | ||
| 152 | // Does not fit | ||
| 153 | if (!size) { | ||
| 154 | // One block containing one very long line | ||
| 155 | size = new_size; | ||
| 156 | pos = start + new_size; | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | BUG_ON(!size); | ||
| 161 | Block *new = block_new(size); | ||
| 162 | if (start < size1) { | ||
| 163 | size_t avail = size1 - start; | ||
| 164 | size_t count = MIN(size, avail); | ||
| 165 | new->nl += copy_count_nl(new->data, buf1 + start, count); | ||
| 166 | copied += count; | ||
| 167 | start += count; | ||
| 168 | } | ||
| 169 | if (start >= size1 && start < size1 + size2) { | ||
| 170 | size_t offset = start - size1; | ||
| 171 | size_t avail = size2 - offset; | ||
| 172 | size_t count = MIN(size - copied, avail); | ||
| 173 | new->nl += copy_count_nl(new->data + copied, buf2 + offset, count); | ||
| 174 | copied += count; | ||
| 175 | start += count; | ||
| 176 | } | ||
| 177 | if (start >= size1 + size2) { | ||
| 178 | size_t offset = start - size1 - size2; | ||
| 179 | size_t avail = size3 - offset; | ||
| 180 | size_t count = size - copied; | ||
| 181 | BUG_ON(count > avail); | ||
| 182 | new->nl += copy_count_nl(new->data + copied, buf3 + offset, count); | ||
| 183 | copied += count; | ||
| 184 | start += count; | ||
| 185 | } | ||
| 186 | |||
| 187 | new->size = size; | ||
| 188 | BUG_ON(copied != size); | ||
| 189 | list_add_before(&new->node, &blk->node); | ||
| 190 | |||
| 191 | nl_added += new->nl; | ||
| 192 | size = 0; | ||
| 193 | } | ||
| 194 | |||
| 195 | cursor->blk = BLOCK(prev_node->next); | ||
| 196 | while (cursor->offset > cursor->blk->size) { | ||
| 197 | cursor->offset -= cursor->blk->size; | ||
| 198 | cursor->blk = BLOCK(cursor->blk->node.next); | ||
| 199 | } | ||
| 200 | |||
| 201 | nl_added -= blk->nl; | ||
| 202 | block_free(blk); | ||
| 203 | return nl_added; | ||
| 204 | } | ||
| 205 | |||
| 206 | static size_t insert_bytes(BlockIter *cursor, const char *buf, size_t len) | ||
| 207 | { | ||
| 208 | // Blocks must contain whole lines. | ||
| 209 | // Last char of buf might not be newline. | ||
| 210 | block_iter_normalize(cursor); | ||
| 211 | |||
| 212 | Block *blk = cursor->blk; | ||
| 213 | size_t new_size = blk->size + len; | ||
| 214 | if (new_size <= blk->alloc || new_size <= BLOCK_EDIT_SIZE) { | ||
| 215 | return insert_to_current(cursor, buf, len); | ||
| 216 | } | ||
| 217 | |||
| 218 | if (blk->nl <= 1 && !memchr(buf, '\n', len)) { | ||
| 219 | // Can't split this possibly very long line. | ||
| 220 | // insert_to_current() is much faster than split_and_insert(). | ||
| 221 | return insert_to_current(cursor, buf, len); | ||
| 222 | } | ||
| 223 | return split_and_insert(cursor, buf, len); | ||
| 224 | } | ||
| 225 | |||
| 226 | void do_insert(View *view, const char *buf, size_t len) | ||
| 227 | { | ||
| 228 | Buffer *buffer = view->buffer; | ||
| 229 | size_t nl = insert_bytes(&view->cursor, buf, len); | ||
| 230 | buffer->nl += nl; | ||
| 231 | sanity_check_blocks(view, true); | ||
| 232 | |||
| 233 | view_update_cursor_y(view); | ||
| 234 | buffer_mark_lines_changed(buffer, view->cy, nl ? LONG_MAX : view->cy); | ||
| 235 | if (buffer->syn) { | ||
| 236 | hl_insert(buffer, view->cy, nl); | ||
| 237 | } | ||
| 238 | } | ||
| 239 | |||
| 240 | static bool only_block(const Buffer *buffer, const Block *blk) | ||
| 241 | { | ||
| 242 | return blk->node.prev == &buffer->blocks && blk->node.next == &buffer->blocks; | ||
| 243 | } | ||
| 244 | |||
| 245 | char *do_delete(View *view, size_t len, bool sanity_check_newlines) | ||
| 246 | { | ||
| 247 | ListHead *saved_prev_node = NULL; | ||
| 248 | Block *blk = view->cursor.blk; | ||
| 249 | size_t offset = view->cursor.offset; | ||
| 250 | size_t pos = 0; | ||
| 251 | size_t deleted_nl = 0; | ||
| 252 | |||
| 253 | if (!len) { | ||
| 254 | return NULL; | ||
| 255 | } | ||
| 256 | |||
| 257 | if (!offset) { | ||
| 258 | // The block where cursor is can become empty and thereby may be deleted | ||
| 259 | saved_prev_node = blk->node.prev; | ||
| 260 | } | ||
| 261 | |||
| 262 | Buffer *buffer = view->buffer; | ||
| 263 | char *deleted = xmalloc(len); | ||
| 264 | while (pos < len) { | ||
| 265 | ListHead *next = blk->node.next; | ||
| 266 | size_t avail = blk->size - offset; | ||
| 267 | size_t count = MIN(len - pos, avail); | ||
| 268 | size_t nl = copy_count_nl(deleted + pos, blk->data + offset, count); | ||
| 269 | if (count < avail) { | ||
| 270 | memmove ( | ||
| 271 | blk->data + offset, | ||
| 272 | blk->data + offset + count, | ||
| 273 | avail - count | ||
| 274 | ); | ||
| 275 | } | ||
| 276 | |||
| 277 | deleted_nl += nl; | ||
| 278 | buffer->nl -= nl; | ||
| 279 | blk->nl -= nl; | ||
| 280 | blk->size -= count; | ||
| 281 | if (!blk->size && !only_block(buffer, blk)) { | ||
| 282 | block_free(blk); | ||
| 283 | } | ||
| 284 | |||
| 285 | offset = 0; | ||
| 286 | pos += count; | ||
| 287 | blk = BLOCK(next); | ||
| 288 | |||
| 289 | BUG_ON(pos < len && next == &buffer->blocks); | ||
| 290 | } | ||
| 291 | |||
| 292 | if (saved_prev_node) { | ||
| 293 | // Cursor was at beginning of a block that was possibly deleted | ||
| 294 | if (saved_prev_node->next == &buffer->blocks) { | ||
| 295 | view->cursor.blk = BLOCK(saved_prev_node); | ||
| 296 | view->cursor.offset = view->cursor.blk->size; | ||
| 297 | } else { | ||
| 298 | view->cursor.blk = BLOCK(saved_prev_node->next); | ||
| 299 | } | ||
| 300 | } | ||
| 301 | |||
| 302 | blk = view->cursor.blk; | ||
| 303 | if ( | ||
| 304 | blk->size | ||
| 305 | && blk->data[blk->size - 1] != '\n' | ||
| 306 | && blk->node.next != &buffer->blocks | ||
| 307 | ) { | ||
| 308 | Block *next = BLOCK(blk->node.next); | ||
| 309 | size_t size = blk->size + next->size; | ||
| 310 | |||
| 311 | if (size > blk->alloc) { | ||
| 312 | blk->alloc = round_size_to_next_multiple(size, BLOCK_ALLOC_MULTIPLE); | ||
| 313 | xrenew(blk->data, blk->alloc); | ||
| 314 | } | ||
| 315 | memcpy(blk->data + blk->size, next->data, next->size); | ||
| 316 | blk->size = size; | ||
| 317 | blk->nl += next->nl; | ||
| 318 | block_free(next); | ||
| 319 | } | ||
| 320 | |||
| 321 | sanity_check_blocks(view, sanity_check_newlines); | ||
| 322 | |||
| 323 | view_update_cursor_y(view); | ||
| 324 | buffer_mark_lines_changed(buffer, view->cy, deleted_nl ? LONG_MAX : view->cy); | ||
| 325 | if (buffer->syn) { | ||
| 326 | hl_delete(buffer, view->cy, deleted_nl); | ||
| 327 | } | ||
| 328 | return deleted; | ||
| 329 | } | ||
| 330 | |||
| 331 | char *do_replace(View *view, size_t del, const char *buf, size_t ins) | ||
| 332 | { | ||
| 333 | block_iter_normalize(&view->cursor); | ||
| 334 | Block *blk = view->cursor.blk; | ||
| 335 | size_t offset = view->cursor.offset; | ||
| 336 | |||
| 337 | size_t avail = blk->size - offset; | ||
| 338 | if (del >= avail) { | ||
| 339 | goto slow; | ||
| 340 | } | ||
| 341 | |||
| 342 | size_t new_size = blk->size + ins - del; | ||
| 343 | if (new_size > BLOCK_EDIT_SIZE) { | ||
| 344 | // Should split | ||
| 345 | if (blk->nl > 1 || memchr(buf, '\n', ins)) { | ||
| 346 | // Most likely can be split | ||
| 347 | goto slow; | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | if (new_size > blk->alloc) { | ||
| 352 | blk->alloc = round_size_to_next_multiple(new_size, BLOCK_ALLOC_MULTIPLE); | ||
| 353 | xrenew(blk->data, blk->alloc); | ||
| 354 | } | ||
| 355 | |||
| 356 | // Modification is limited to one block | ||
| 357 | Buffer *buffer = view->buffer; | ||
| 358 | char *ptr = blk->data + offset; | ||
| 359 | char *deleted = xmalloc(del); | ||
| 360 | size_t del_nl = copy_count_nl(deleted, ptr, del); | ||
| 361 | blk->nl -= del_nl; | ||
| 362 | buffer->nl -= del_nl; | ||
| 363 | |||
| 364 | if (del != ins) { | ||
| 365 | memmove(ptr + ins, ptr + del, avail - del); | ||
| 366 | } | ||
| 367 | |||
| 368 | size_t ins_nl = copy_count_nl(ptr, buf, ins); | ||
| 369 | blk->nl += ins_nl; | ||
| 370 | buffer->nl += ins_nl; | ||
| 371 | blk->size = new_size; | ||
| 372 | sanity_check_blocks(view, true); | ||
| 373 | view_update_cursor_y(view); | ||
| 374 | |||
| 375 | // If the number of inserted and removed bytes are the same, some | ||
| 376 | // line(s) changed but the lines after them didn't move up or down | ||
| 377 | long max = (del_nl == ins_nl) ? view->cy + del_nl : LONG_MAX; | ||
| 378 | buffer_mark_lines_changed(buffer, view->cy, max); | ||
| 379 | |||
| 380 | if (buffer->syn) { | ||
| 381 | hl_delete(buffer, view->cy, del_nl); | ||
| 382 | hl_insert(buffer, view->cy, ins_nl); | ||
| 383 | } | ||
| 384 | |||
| 385 | return deleted; | ||
| 386 | |||
| 387 | slow: | ||
| 388 | // The "sanity_check_newlines" argument of do_delete() is false here | ||
| 389 | // because it may be removing a terminating newline that do_insert() | ||
| 390 | // is going to insert again at a different position: | ||
| 391 | deleted = do_delete(view, del, false); | ||
| 392 | do_insert(view, buf, ins); | ||
| 393 | return deleted; | ||
| 394 | } | ||
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 @@ | |||
| 1 | #ifndef EDIT_H | ||
| 2 | #define EDIT_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | void do_insert(View *view, const char *buf, size_t len) NONNULL_ARG(1); | ||
| 10 | char *do_delete(View *view, size_t len, bool sanity_check_newlines) NONNULL_ARGS; | ||
| 11 | char *do_replace(View *view, size_t del, const char *buf, size_t ins) NONNULL_ARGS_AND_RETURN; | ||
| 12 | |||
| 13 | #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 @@ | |||
| 1 | #include "compat.h" | ||
| 2 | #include <errno.h> | ||
| 3 | #include <langinfo.h> | ||
| 4 | #include <locale.h> | ||
| 5 | #include <stdint.h> | ||
| 6 | #include <stdio.h> | ||
| 7 | #include <stdlib.h> | ||
| 8 | #include <string.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | #include "editor.h" | ||
| 11 | #include "bind.h" | ||
| 12 | #include "bookmark.h" | ||
| 13 | #include "command/macro.h" | ||
| 14 | #include "commands.h" | ||
| 15 | #include "compiler.h" | ||
| 16 | #include "encoding.h" | ||
| 17 | #include "error.h" | ||
| 18 | #include "file-option.h" | ||
| 19 | #include "filetype.h" | ||
| 20 | #include "lock.h" | ||
| 21 | #include "mode.h" | ||
| 22 | #include "regexp.h" | ||
| 23 | #include "screen.h" | ||
| 24 | #include "search.h" | ||
| 25 | #include "signals.h" | ||
| 26 | #include "syntax/syntax.h" | ||
| 27 | #include "tag.h" | ||
| 28 | #include "terminal/input.h" | ||
| 29 | #include "terminal/mode.h" | ||
| 30 | #include "terminal/output.h" | ||
| 31 | #include "terminal/style.h" | ||
| 32 | #include "util/ascii.h" | ||
| 33 | #include "util/debug.h" | ||
| 34 | #include "util/exitcode.h" | ||
| 35 | #include "util/intern.h" | ||
| 36 | #include "util/log.h" | ||
| 37 | #include "util/utf8.h" | ||
| 38 | #include "util/xmalloc.h" | ||
| 39 | #include "util/xstdio.h" | ||
| 40 | #include "window.h" | ||
| 41 | #include "../build/version.h" | ||
| 42 | |||
| 43 | static void set_and_check_locale(void) | ||
| 44 | { | ||
| 45 | const char *default_locale = setlocale(LC_CTYPE, ""); | ||
| 46 | if (likely(default_locale)) { | ||
| 47 | const char *codeset = nl_langinfo(CODESET); | ||
| 48 | LOG_INFO("locale: %s (codeset: %s)", default_locale, codeset); | ||
| 49 | if (likely(lookup_encoding(codeset) == UTF8)) { | ||
| 50 | return; | ||
| 51 | } | ||
| 52 | } else { | ||
| 53 | LOG_ERROR("failed to set default locale"); | ||
| 54 | } | ||
| 55 | |||
| 56 | static const char fallbacks[][12] = {"C.UTF-8", "en_US.UTF-8"}; | ||
| 57 | const char *fallback = NULL; | ||
| 58 | for (size_t i = 0; i < ARRAYLEN(fallbacks) && !fallback; i++) { | ||
| 59 | fallback = setlocale(LC_CTYPE, fallbacks[i]); | ||
| 60 | } | ||
| 61 | if (fallback) { | ||
| 62 | LOG_INFO("using fallback locale for LC_CTYPE: %s", fallback); | ||
| 63 | return; | ||
| 64 | } | ||
| 65 | |||
| 66 | LOG_ERROR("no UTF-8 fallback locales found"); | ||
| 67 | fputs("setlocale() failed\n", stderr); | ||
| 68 | exit(EX_CONFIG); | ||
| 69 | } | ||
| 70 | |||
| 71 | EditorState *init_editor_state(void) | ||
| 72 | { | ||
| 73 | EditorState *e = xnew(EditorState, 1); | ||
| 74 | *e = (EditorState) { | ||
| 75 | .status = EDITOR_INITIALIZING, | ||
| 76 | .input_mode = INPUT_NORMAL, | ||
| 77 | .version = VERSION, | ||
| 78 | .command_history = { | ||
| 79 | .max_entries = 512, | ||
| 80 | }, | ||
| 81 | .search_history = { | ||
| 82 | .max_entries = 128, | ||
| 83 | }, | ||
| 84 | .cursor_styles = { | ||
| 85 | [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT}, | ||
| 86 | [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 87 | [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 88 | [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 89 | }, | ||
| 90 | .modes = { | ||
| 91 | [INPUT_NORMAL] = {.cmds = &normal_commands}, | ||
| 92 | [INPUT_COMMAND] = {.cmds = &cmd_mode_commands}, | ||
| 93 | [INPUT_SEARCH] = {.cmds = &search_mode_commands}, | ||
| 94 | }, | ||
| 95 | .options = { | ||
| 96 | .auto_indent = true, | ||
| 97 | .detect_indent = 0, | ||
| 98 | .editorconfig = false, | ||
| 99 | .emulate_tab = false, | ||
| 100 | .expand_tab = false, | ||
| 101 | .file_history = true, | ||
| 102 | .indent_width = 8, | ||
| 103 | .overwrite = false, | ||
| 104 | .save_unmodified = SAVE_FULL, | ||
| 105 | .syntax = true, | ||
| 106 | .tab_width = 8, | ||
| 107 | .text_width = 72, | ||
| 108 | .ws_error = WSE_SPECIAL, | ||
| 109 | |||
| 110 | // Global-only options | ||
| 111 | .case_sensitive_search = CSS_TRUE, | ||
| 112 | .crlf_newlines = false, | ||
| 113 | .display_special = false, | ||
| 114 | .esc_timeout = 100, | ||
| 115 | .filesize_limit = 250, | ||
| 116 | .lock_files = true, | ||
| 117 | .optimize_true_color = true, | ||
| 118 | .scroll_margin = 0, | ||
| 119 | .select_cursor_char = true, | ||
| 120 | .set_window_title = false, | ||
| 121 | .show_line_numbers = false, | ||
| 122 | .statusline_left = str_intern(" %f%s%m%s%r%s%M"), | ||
| 123 | .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "), | ||
| 124 | .tab_bar = true, | ||
| 125 | .utf8_bom = false, | ||
| 126 | } | ||
| 127 | }; | ||
| 128 | |||
| 129 | sanity_check_global_options(&e->options); | ||
| 130 | |||
| 131 | for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { | ||
| 132 | const CommandSet *cmds = e->modes[i].cmds; | ||
| 133 | BUG_ON(!cmds); | ||
| 134 | BUG_ON(!cmds->lookup); | ||
| 135 | } | ||
| 136 | |||
| 137 | const char *home = getenv("HOME"); | ||
| 138 | const char *dte_home = getenv("DTE_HOME"); | ||
| 139 | e->home_dir = strview_intern(home ? home : ""); | ||
| 140 | if (dte_home) { | ||
| 141 | e->user_config_dir = xstrdup(dte_home); | ||
| 142 | } else { | ||
| 143 | e->user_config_dir = xasprintf("%s/.dte", e->home_dir.data); | ||
| 144 | } | ||
| 145 | |||
| 146 | LOG_INFO("dte version: " VERSION); | ||
| 147 | LOG_INFO("features:%s", feature_string); | ||
| 148 | |||
| 149 | pid_t pid = getpid(); | ||
| 150 | bool leader = pid == getsid(0); | ||
| 151 | e->session_leader = leader; | ||
| 152 | LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : ""); | ||
| 153 | |||
| 154 | pid_t pgid = getpgrp(); | ||
| 155 | if (pgid != pid) { | ||
| 156 | LOG_INFO("pgid: %jd", (intmax_t)pgid); | ||
| 157 | } | ||
| 158 | |||
| 159 | set_and_check_locale(); | ||
| 160 | init_file_locks_context(e->user_config_dir, pid); | ||
| 161 | |||
| 162 | // Allow child processes to detect that they're running under dte | ||
| 163 | if (unlikely(setenv("DTE_VERSION", VERSION, true) != 0)) { | ||
| 164 | fatal_error("setenv", errno); | ||
| 165 | } | ||
| 166 | |||
| 167 | RegexpWordBoundaryTokens *wb = &e->regexp_word_tokens; | ||
| 168 | if (regexp_init_word_boundary_tokens(wb)) { | ||
| 169 | LOG_INFO("regex word boundary tokens detected: %s %s", wb->start, wb->end); | ||
| 170 | } else { | ||
| 171 | LOG_WARNING("no regex word boundary tokens detected"); | ||
| 172 | } | ||
| 173 | |||
| 174 | term_input_init(&e->terminal.ibuf); | ||
| 175 | term_output_init(&e->terminal.obuf); | ||
| 176 | hashmap_init(&e->aliases, 32); | ||
| 177 | intmap_init(&e->modes[INPUT_NORMAL].key_bindings, 150); | ||
| 178 | intmap_init(&e->modes[INPUT_COMMAND].key_bindings, 40); | ||
| 179 | intmap_init(&e->modes[INPUT_SEARCH].key_bindings, 40); | ||
| 180 | return e; | ||
| 181 | } | ||
| 182 | |||
| 183 | void free_editor_state(EditorState *e) | ||
| 184 | { | ||
| 185 | free(e->clipboard.buf); | ||
| 186 | free_file_options(&e->file_options); | ||
| 187 | free_filetypes(&e->filetypes); | ||
| 188 | free_syntaxes(&e->syntaxes); | ||
| 189 | file_history_free(&e->file_history); | ||
| 190 | history_free(&e->command_history); | ||
| 191 | history_free(&e->search_history); | ||
| 192 | search_free_regexp(&e->search); | ||
| 193 | term_output_free(&e->terminal.obuf); | ||
| 194 | term_input_free(&e->terminal.ibuf); | ||
| 195 | cmdline_free(&e->cmdline); | ||
| 196 | clear_messages(&e->messages); | ||
| 197 | free_macro(&e->macro); | ||
| 198 | tag_file_free(&e->tagfile); | ||
| 199 | |||
| 200 | ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free)); | ||
| 201 | ptr_array_free_cb(&e->buffers, FREE_FUNC(free_buffer)); | ||
| 202 | hashmap_free(&e->compilers, FREE_FUNC(free_compiler)); | ||
| 203 | hashmap_free(&e->colors.other, free); | ||
| 204 | hashmap_free(&e->aliases, free); | ||
| 205 | |||
| 206 | for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { | ||
| 207 | free_bindings(&e->modes[i].key_bindings); | ||
| 208 | } | ||
| 209 | |||
| 210 | free_interned_strings(); | ||
| 211 | free_interned_regexps(); | ||
| 212 | |||
| 213 | // TODO: intern this (so that it's freed by free_intern_pool()) | ||
| 214 | free((void*)e->user_config_dir); | ||
| 215 | |||
| 216 | free(e); | ||
| 217 | } | ||
| 218 | |||
| 219 | static void sanity_check(const View *view) | ||
| 220 | { | ||
| 221 | #if DEBUG >= 1 | ||
| 222 | const Block *blk; | ||
| 223 | block_for_each(blk, &view->buffer->blocks) { | ||
| 224 | if (blk == view->cursor.blk) { | ||
| 225 | BUG_ON(view->cursor.offset > view->cursor.blk->size); | ||
| 226 | return; | ||
| 227 | } | ||
| 228 | } | ||
| 229 | BUG("cursor not seen"); | ||
| 230 | #else | ||
| 231 | (void)view; | ||
| 232 | #endif | ||
| 233 | } | ||
| 234 | |||
| 235 | void any_key(Terminal *term, unsigned int esc_timeout) | ||
| 236 | { | ||
| 237 | KeyCode key; | ||
| 238 | xfputs("Press any key to continue\r\n", stderr); | ||
| 239 | while ((key = term_read_key(term, esc_timeout)) == KEY_NONE) { | ||
| 240 | ; | ||
| 241 | } | ||
| 242 | bool bracketed_paste = key == KEY_BRACKETED_PASTE; | ||
| 243 | if (bracketed_paste || key == KEY_DETECTED_PASTE) { | ||
| 244 | term_discard_paste(&term->ibuf, bracketed_paste); | ||
| 245 | } | ||
| 246 | } | ||
| 247 | |||
| 248 | NOINLINE | ||
| 249 | void ui_resize(EditorState *e) | ||
| 250 | { | ||
| 251 | if (e->status == EDITOR_INITIALIZING) { | ||
| 252 | return; | ||
| 253 | } | ||
| 254 | resized = 0; | ||
| 255 | update_screen_size(&e->terminal, e->root_frame); | ||
| 256 | normal_update(e); | ||
| 257 | } | ||
| 258 | |||
| 259 | void ui_start(EditorState *e) | ||
| 260 | { | ||
| 261 | if (e->status == EDITOR_INITIALIZING) { | ||
| 262 | return; | ||
| 263 | } | ||
| 264 | |||
| 265 | // Note: the order of these calls is important - Kitty saves/restores | ||
| 266 | // some terminal state when switching buffers, so switching to the | ||
| 267 | // alternate screen buffer needs to happen before modes are enabled | ||
| 268 | term_use_alt_screen_buffer(&e->terminal); | ||
| 269 | term_enable_private_modes(&e->terminal); | ||
| 270 | |||
| 271 | ui_resize(e); | ||
| 272 | } | ||
| 273 | |||
| 274 | void ui_end(EditorState *e) | ||
| 275 | { | ||
| 276 | if (e->status == EDITOR_INITIALIZING) { | ||
| 277 | return; | ||
| 278 | } | ||
| 279 | Terminal *term = &e->terminal; | ||
| 280 | TermOutputBuffer *obuf = &term->obuf; | ||
| 281 | term_clear_screen(obuf); | ||
| 282 | term_move_cursor(obuf, 0, term->height - 1); | ||
| 283 | term_restore_cursor_style(term); | ||
| 284 | term_show_cursor(term); | ||
| 285 | term_restore_private_modes(term); | ||
| 286 | term_use_normal_screen_buffer(term); | ||
| 287 | term_end_sync_update(term); | ||
| 288 | term_output_flush(obuf); | ||
| 289 | term_cooked(); | ||
| 290 | } | ||
| 291 | |||
| 292 | int main_loop(EditorState *e) | ||
| 293 | { | ||
| 294 | while (e->status == EDITOR_RUNNING) { | ||
| 295 | if (unlikely(resized)) { | ||
| 296 | LOG_INFO("SIGWINCH received"); | ||
| 297 | ui_resize(e); | ||
| 298 | } | ||
| 299 | |||
| 300 | KeyCode key = term_read_key(&e->terminal, e->options.esc_timeout); | ||
| 301 | if (unlikely(key == KEY_NONE)) { | ||
| 302 | continue; | ||
| 303 | } | ||
| 304 | |||
| 305 | const ScreenState s = { | ||
| 306 | .is_modified = buffer_modified(e->buffer), | ||
| 307 | .id = e->buffer->id, | ||
| 308 | .cy = e->view->cy, | ||
| 309 | .vx = e->view->vx, | ||
| 310 | .vy = e->view->vy | ||
| 311 | }; | ||
| 312 | |||
| 313 | clear_error(); | ||
| 314 | handle_input(e, key); | ||
| 315 | sanity_check(e->view); | ||
| 316 | update_screen(e, &s); | ||
| 317 | } | ||
| 318 | |||
| 319 | BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX); | ||
| 320 | return e->status; | ||
| 321 | } | ||
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 @@ | |||
| 1 | #ifndef EDITOR_H | ||
| 2 | #define EDITOR_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "buffer.h" | ||
| 7 | #include "cmdline.h" | ||
| 8 | #include "command/macro.h" | ||
| 9 | #include "command/run.h" | ||
| 10 | #include "commands.h" | ||
| 11 | #include "copy.h" | ||
| 12 | #include "file-history.h" | ||
| 13 | #include "frame.h" | ||
| 14 | #include "history.h" | ||
| 15 | #include "msg.h" | ||
| 16 | #include "options.h" | ||
| 17 | #include "regexp.h" | ||
| 18 | #include "search.h" | ||
| 19 | #include "syntax/color.h" | ||
| 20 | #include "tag.h" | ||
| 21 | #include "terminal/cursor.h" | ||
| 22 | #include "terminal/terminal.h" | ||
| 23 | #include "util/debug.h" | ||
| 24 | #include "util/hashmap.h" | ||
| 25 | #include "util/intmap.h" | ||
| 26 | #include "util/macros.h" | ||
| 27 | #include "util/ptr-array.h" | ||
| 28 | #include "util/string-view.h" | ||
| 29 | #include "view.h" | ||
| 30 | |||
| 31 | typedef enum { | ||
| 32 | EDITOR_INITIALIZING = -2, | ||
| 33 | EDITOR_RUNNING = -1, | ||
| 34 | // Values 0-125 are exit codes | ||
| 35 | EDITOR_EXIT_OK = 0, | ||
| 36 | EDITOR_EXIT_MAX = 125, | ||
| 37 | } EditorStatus; | ||
| 38 | |||
| 39 | typedef enum { | ||
| 40 | INPUT_NORMAL, | ||
| 41 | INPUT_COMMAND, | ||
| 42 | INPUT_SEARCH, | ||
| 43 | } InputMode; | ||
| 44 | |||
| 45 | typedef struct { | ||
| 46 | const CommandSet *cmds; | ||
| 47 | IntMap key_bindings; | ||
| 48 | } ModeHandler; | ||
| 49 | |||
| 50 | typedef struct EditorState { | ||
| 51 | EditorStatus status; | ||
| 52 | InputMode input_mode; | ||
| 53 | CommandLine cmdline; | ||
| 54 | SearchState search; | ||
| 55 | GlobalOptions options; | ||
| 56 | Terminal terminal; | ||
| 57 | StringView home_dir; | ||
| 58 | const char *user_config_dir; | ||
| 59 | bool child_controls_terminal; | ||
| 60 | bool everything_changed; | ||
| 61 | bool cursor_style_changed; | ||
| 62 | bool session_leader; | ||
| 63 | size_t cmdline_x; | ||
| 64 | ModeHandler modes[3]; | ||
| 65 | Clipboard clipboard; | ||
| 66 | TagFile tagfile; | ||
| 67 | HashMap aliases; | ||
| 68 | HashMap compilers; | ||
| 69 | HashMap syntaxes; | ||
| 70 | ColorScheme colors; | ||
| 71 | CommandMacroState macro; | ||
| 72 | TermCursorStyle cursor_styles[NR_CURSOR_MODES]; | ||
| 73 | Frame *root_frame; | ||
| 74 | struct Window *window; | ||
| 75 | View *view; | ||
| 76 | Buffer *buffer; | ||
| 77 | PointerArray buffers; | ||
| 78 | PointerArray filetypes; | ||
| 79 | PointerArray file_options; | ||
| 80 | PointerArray bookmarks; | ||
| 81 | MessageArray messages; | ||
| 82 | FileHistory file_history; | ||
| 83 | History search_history; | ||
| 84 | History command_history; | ||
| 85 | RegexpWordBoundaryTokens regexp_word_tokens; | ||
| 86 | const char *version; | ||
| 87 | } EditorState; | ||
| 88 | |||
| 89 | static inline void mark_everything_changed(EditorState *e) | ||
| 90 | { | ||
| 91 | e->everything_changed = true; | ||
| 92 | } | ||
| 93 | |||
| 94 | static inline void set_input_mode(EditorState *e, InputMode mode) | ||
| 95 | { | ||
| 96 | e->cursor_style_changed = true; | ||
| 97 | e->input_mode = mode; | ||
| 98 | } | ||
| 99 | |||
| 100 | static inline CommandRunner cmdrunner_for_mode(EditorState *e, InputMode mode, bool allow_recording) | ||
| 101 | { | ||
| 102 | BUG_ON(mode >= ARRAYLEN(e->modes)); | ||
| 103 | CommandRunner runner = { | ||
| 104 | .cmds = e->modes[mode].cmds, | ||
| 105 | .lookup_alias = (mode == INPUT_NORMAL) ? find_normal_alias : NULL, | ||
| 106 | .home_dir = &e->home_dir, | ||
| 107 | .allow_recording = allow_recording, | ||
| 108 | .userdata = e, | ||
| 109 | }; | ||
| 110 | return runner; | ||
| 111 | } | ||
| 112 | |||
| 113 | EditorState *init_editor_state(void) RETURNS_NONNULL; | ||
| 114 | void free_editor_state(EditorState *e) NONNULL_ARGS; | ||
| 115 | void any_key(Terminal *term, unsigned int esc_timeout) NONNULL_ARGS; | ||
| 116 | int main_loop(EditorState *e) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 117 | void ui_start(EditorState *e) NONNULL_ARGS; | ||
| 118 | void ui_end(EditorState *e) NONNULL_ARGS; | ||
| 119 | void ui_resize(EditorState *e) NONNULL_ARGS; | ||
| 120 | |||
| 121 | #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 @@ | |||
| 1 | #include "encoding.h" | ||
| 2 | #include "util/ascii.h" | ||
| 3 | #include "util/bsearch.h" | ||
| 4 | #include "util/debug.h" | ||
| 5 | #include "util/intern.h" | ||
| 6 | #include "util/str-util.h" | ||
| 7 | |||
| 8 | typedef struct { | ||
| 9 | const char alias[8]; | ||
| 10 | EncodingType encoding; | ||
| 11 | } EncodingAlias; | ||
| 12 | |||
| 13 | static const char encoding_names[][16] = { | ||
| 14 | [UTF8] = "UTF-8", | ||
| 15 | [UTF16BE] = "UTF-16BE", | ||
| 16 | [UTF16LE] = "UTF-16LE", | ||
| 17 | [UTF32BE] = "UTF-32BE", | ||
| 18 | [UTF32LE] = "UTF-32LE", | ||
| 19 | }; | ||
| 20 | |||
| 21 | static const EncodingAlias encoding_aliases[] = { | ||
| 22 | {"UCS-2", UTF16BE}, | ||
| 23 | {"UCS-2BE", UTF16BE}, | ||
| 24 | {"UCS-2LE", UTF16LE}, | ||
| 25 | {"UCS-4", UTF32BE}, | ||
| 26 | {"UCS-4BE", UTF32BE}, | ||
| 27 | {"UCS-4LE", UTF32LE}, | ||
| 28 | {"UCS2", UTF16BE}, | ||
| 29 | {"UCS4", UTF32BE}, | ||
| 30 | {"UTF-16", UTF16BE}, | ||
| 31 | {"UTF-32", UTF32BE}, | ||
| 32 | {"UTF16", UTF16BE}, | ||
| 33 | {"UTF16BE", UTF16BE}, | ||
| 34 | {"UTF16LE", UTF16LE}, | ||
| 35 | {"UTF32", UTF32BE}, | ||
| 36 | {"UTF32BE", UTF32BE}, | ||
| 37 | {"UTF32LE", UTF32LE}, | ||
| 38 | {"UTF8", UTF8}, | ||
| 39 | }; | ||
| 40 | |||
| 41 | static const ByteOrderMark boms[NR_ENCODING_TYPES] = { | ||
| 42 | [UTF8] = {{0xef, 0xbb, 0xbf}, 3}, | ||
| 43 | [UTF16BE] = {{0xfe, 0xff}, 2}, | ||
| 44 | [UTF16LE] = {{0xff, 0xfe}, 2}, | ||
| 45 | [UTF32BE] = {{0x00, 0x00, 0xfe, 0xff}, 4}, | ||
| 46 | [UTF32LE] = {{0xff, 0xfe, 0x00, 0x00}, 4}, | ||
| 47 | }; | ||
| 48 | |||
| 49 | UNITTEST { | ||
| 50 | CHECK_BSEARCH_ARRAY(encoding_aliases, alias, ascii_strcmp_icase); | ||
| 51 | } | ||
| 52 | |||
| 53 | static int enc_alias_cmp(const void *key, const void *elem) | ||
| 54 | { | ||
| 55 | const EncodingAlias *a = key; | ||
| 56 | const char *name = elem; | ||
| 57 | return ascii_strcmp_icase(a->alias, name); | ||
| 58 | } | ||
| 59 | |||
| 60 | EncodingType lookup_encoding(const char *name) | ||
| 61 | { | ||
| 62 | static_assert(ARRAYLEN(encoding_names) == NR_ENCODING_TYPES - 1); | ||
| 63 | for (size_t i = 0; i < ARRAYLEN(encoding_names); i++) { | ||
| 64 | if (ascii_streq_icase(name, encoding_names[i])) { | ||
| 65 | return (EncodingType) i; | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | const EncodingAlias *a = BSEARCH(name, encoding_aliases, enc_alias_cmp); | ||
| 70 | return a ? a->encoding : UNKNOWN_ENCODING; | ||
| 71 | } | ||
| 72 | |||
| 73 | static const char *encoding_type_to_string(EncodingType type) | ||
| 74 | { | ||
| 75 | if (type < NR_ENCODING_TYPES && type != UNKNOWN_ENCODING) { | ||
| 76 | return str_intern(encoding_names[type]); | ||
| 77 | } | ||
| 78 | return NULL; | ||
| 79 | } | ||
| 80 | |||
| 81 | Encoding encoding_from_name(const char *name) | ||
| 82 | { | ||
| 83 | const EncodingType type = lookup_encoding(name); | ||
| 84 | const char *normalized_name; | ||
| 85 | if (type == UNKNOWN_ENCODING) { | ||
| 86 | char upper[256]; | ||
| 87 | size_t n; | ||
| 88 | for (n = 0; n < sizeof(upper) && name[n]; n++) { | ||
| 89 | upper[n] = ascii_toupper(name[n]); | ||
| 90 | } | ||
| 91 | normalized_name = mem_intern(upper, n); | ||
| 92 | } else { | ||
| 93 | normalized_name = encoding_type_to_string(type); | ||
| 94 | } | ||
| 95 | return (Encoding) { | ||
| 96 | .type = type, | ||
| 97 | .name = normalized_name | ||
| 98 | }; | ||
| 99 | } | ||
| 100 | |||
| 101 | Encoding encoding_from_type(EncodingType type) | ||
| 102 | { | ||
| 103 | return (Encoding) { | ||
| 104 | .type = type, | ||
| 105 | .name = encoding_type_to_string(type) | ||
| 106 | }; | ||
| 107 | } | ||
| 108 | |||
| 109 | EncodingType detect_encoding_from_bom(const unsigned char *buf, size_t size) | ||
| 110 | { | ||
| 111 | // Skip exhaustive checks if there's clearly no BOM | ||
| 112 | if (size < 2 || ((unsigned int)buf[0]) - 1 < 0xEE) { | ||
| 113 | return UNKNOWN_ENCODING; | ||
| 114 | } | ||
| 115 | |||
| 116 | // Iterate array backwards to ensure UTF32LE is checked before UTF16LE | ||
| 117 | for (int i = NR_ENCODING_TYPES - 1; i >= 0; i--) { | ||
| 118 | const unsigned int bom_len = boms[i].len; | ||
| 119 | if (bom_len > 0 && size >= bom_len && mem_equal(buf, boms[i].bytes, bom_len)) { | ||
| 120 | return (EncodingType) i; | ||
| 121 | } | ||
| 122 | } | ||
| 123 | return UNKNOWN_ENCODING; | ||
| 124 | } | ||
| 125 | |||
| 126 | const ByteOrderMark *get_bom_for_encoding(EncodingType encoding) | ||
| 127 | { | ||
| 128 | static_assert(ARRAYLEN(boms) == NR_ENCODING_TYPES); | ||
| 129 | BUG_ON(encoding >= ARRAYLEN(boms)); | ||
| 130 | const ByteOrderMark *bom = &boms[encoding]; | ||
| 131 | return bom->len ? bom : NULL; | ||
| 132 | } | ||
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 @@ | |||
| 1 | #ifndef ENCODING_ENCODING_H | ||
| 2 | #define ENCODING_ENCODING_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | |||
| 8 | typedef enum { | ||
| 9 | UTF8, | ||
| 10 | UTF16BE, | ||
| 11 | UTF16LE, | ||
| 12 | UTF32BE, | ||
| 13 | UTF32LE, | ||
| 14 | UNKNOWN_ENCODING, | ||
| 15 | NR_ENCODING_TYPES, | ||
| 16 | |||
| 17 | // This value is used by the "open" command to instruct other | ||
| 18 | // routines that no specific encoding was requested and that | ||
| 19 | // it should be detected instead. It is always replaced by | ||
| 20 | // some other value by the time a file is successfully opened. | ||
| 21 | ENCODING_AUTODETECT | ||
| 22 | } EncodingType; | ||
| 23 | |||
| 24 | typedef struct { | ||
| 25 | EncodingType type; | ||
| 26 | // An interned encoding name compatible with iconv_open(3) | ||
| 27 | const char *name; | ||
| 28 | } Encoding; | ||
| 29 | |||
| 30 | typedef struct { | ||
| 31 | const unsigned char bytes[4]; | ||
| 32 | unsigned int len; | ||
| 33 | } ByteOrderMark; | ||
| 34 | |||
| 35 | static inline bool same_encoding(const Encoding *a, const Encoding *b) | ||
| 36 | { | ||
| 37 | return a->type == b->type && a->name == b->name; | ||
| 38 | } | ||
| 39 | |||
| 40 | Encoding encoding_from_type(EncodingType type); | ||
| 41 | Encoding encoding_from_name(const char *name) NONNULL_ARGS; | ||
| 42 | EncodingType lookup_encoding(const char *name) NONNULL_ARGS; | ||
| 43 | EncodingType detect_encoding_from_bom(const unsigned char *buf, size_t size); | ||
| 44 | const ByteOrderMark *get_bom_for_encoding(EncodingType encoding); | ||
| 45 | |||
| 46 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdarg.h> | ||
| 3 | #include <stdio.h> | ||
| 4 | #include <string.h> | ||
| 5 | #include "error.h" | ||
| 6 | #include "command/run.h" | ||
| 7 | #include "config.h" | ||
| 8 | #include "util/log.h" | ||
| 9 | #include "util/xstdio.h" | ||
| 10 | |||
| 11 | static char error_buf[512]; | ||
| 12 | static unsigned int nr_errors; | ||
| 13 | static bool msg_is_error; | ||
| 14 | static bool print_errors_to_stderr; | ||
| 15 | |||
| 16 | void clear_error(void) | ||
| 17 | { | ||
| 18 | error_buf[0] = '\0'; | ||
| 19 | } | ||
| 20 | |||
| 21 | bool error_msg(const char *format, ...) | ||
| 22 | { | ||
| 23 | const char *cmd = current_command ? current_command->name : NULL; | ||
| 24 | const char *file = current_config.file; | ||
| 25 | const unsigned int line = current_config.line; | ||
| 26 | const size_t size = sizeof(error_buf); | ||
| 27 | int pos = 0; | ||
| 28 | |||
| 29 | if (file && cmd) { | ||
| 30 | pos = snprintf(error_buf, size, "%s:%u: %s: ", file, line, cmd); | ||
| 31 | } else if (file) { | ||
| 32 | pos = snprintf(error_buf, size, "%s:%u: ", file, line); | ||
| 33 | } else if (cmd) { | ||
| 34 | pos = snprintf(error_buf, size, "%s: ", cmd); | ||
| 35 | } | ||
| 36 | |||
| 37 | if (unlikely(pos < 0)) { | ||
| 38 | // Note: POSIX snprintf(3) *does* set errno on failure (unlike ISO C) | ||
| 39 | LOG_ERRNO("snprintf"); | ||
| 40 | pos = 0; | ||
| 41 | } | ||
| 42 | |||
| 43 | if (likely(pos < (size - 3))) { | ||
| 44 | va_list ap; | ||
| 45 | va_start(ap, format); | ||
| 46 | vsnprintf(error_buf + pos, size - pos, format, ap); | ||
| 47 | va_end(ap); | ||
| 48 | } else { | ||
| 49 | LOG_WARNING("no buffer space left for error message"); | ||
| 50 | } | ||
| 51 | |||
| 52 | msg_is_error = true; | ||
| 53 | nr_errors++; | ||
| 54 | |||
| 55 | if (print_errors_to_stderr) { | ||
| 56 | xfputs(error_buf, stderr); | ||
| 57 | xfputc('\n', stderr); | ||
| 58 | } | ||
| 59 | |||
| 60 | LOG_INFO("%s", error_buf); | ||
| 61 | |||
| 62 | // Always return false, to allow tail-calling as `return error_msg(...);` | ||
| 63 | // from command handlers, instead of `error_msg(...); return false;` | ||
| 64 | return false; | ||
| 65 | } | ||
| 66 | |||
| 67 | bool error_msg_errno(const char *prefix) | ||
| 68 | { | ||
| 69 | return error_msg("%s: %s", prefix, strerror(errno)); | ||
| 70 | } | ||
| 71 | |||
| 72 | void info_msg(const char *format, ...) | ||
| 73 | { | ||
| 74 | va_list ap; | ||
| 75 | va_start(ap, format); | ||
| 76 | vsnprintf(error_buf, sizeof(error_buf), format, ap); | ||
| 77 | va_end(ap); | ||
| 78 | msg_is_error = false; | ||
| 79 | } | ||
| 80 | |||
| 81 | const char *get_msg(bool *is_error) | ||
| 82 | { | ||
| 83 | *is_error = msg_is_error; | ||
| 84 | return error_buf; | ||
| 85 | } | ||
| 86 | |||
| 87 | unsigned int get_nr_errors(void) | ||
| 88 | { | ||
| 89 | return nr_errors; | ||
| 90 | } | ||
| 91 | |||
| 92 | void set_print_errors_to_stderr(bool enable) | ||
| 93 | { | ||
| 94 | print_errors_to_stderr = enable; | ||
| 95 | } | ||
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 @@ | |||
| 1 | #ifndef ERROR_H | ||
| 2 | #define ERROR_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "util/macros.h" | ||
| 6 | |||
| 7 | bool error_msg(const char *format, ...) COLD PRINTF(1); | ||
| 8 | bool error_msg_errno(const char *prefix) COLD NONNULL_ARGS; | ||
| 9 | void info_msg(const char *format, ...) PRINTF(1); | ||
| 10 | void clear_error(void); | ||
| 11 | const char *get_msg(bool *is_error) NONNULL_ARGS; | ||
| 12 | unsigned int get_nr_errors(void); | ||
| 13 | void set_print_errors_to_stderr(bool enable); | ||
| 14 | |||
| 15 | #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 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | #include "exec.h" | ||
| 6 | #include "block-iter.h" | ||
| 7 | #include "buffer.h" | ||
| 8 | #include "change.h" | ||
| 9 | #include "command/macro.h" | ||
| 10 | #include "commands.h" | ||
| 11 | #include "ctags.h" | ||
| 12 | #include "error.h" | ||
| 13 | #include "misc.h" | ||
| 14 | #include "move.h" | ||
| 15 | #include "msg.h" | ||
| 16 | #include "selection.h" | ||
| 17 | #include "show.h" | ||
| 18 | #include "tag.h" | ||
| 19 | #include "util/bsearch.h" | ||
| 20 | #include "util/debug.h" | ||
| 21 | #include "util/numtostr.h" | ||
| 22 | #include "util/ptr-array.h" | ||
| 23 | #include "util/str-util.h" | ||
| 24 | #include "util/string-view.h" | ||
| 25 | #include "util/string.h" | ||
| 26 | #include "util/strtonum.h" | ||
| 27 | #include "util/xsnprintf.h" | ||
| 28 | #include "view.h" | ||
| 29 | #include "window.h" | ||
| 30 | |||
| 31 | enum { | ||
| 32 | IN = 1 << 0, | ||
| 33 | OUT = 1 << 1, | ||
| 34 | ERR = 1 << 2, | ||
| 35 | ALL = IN | OUT | ERR, | ||
| 36 | }; | ||
| 37 | |||
| 38 | static const struct { | ||
| 39 | char name[8]; | ||
| 40 | uint8_t flags; | ||
| 41 | } exec_map[] = { | ||
| 42 | [EXEC_BUFFER] = {"buffer", IN | OUT}, | ||
| 43 | [EXEC_COMMAND] = {"command", IN}, | ||
| 44 | [EXEC_ERRMSG] = {"errmsg", ERR}, | ||
| 45 | [EXEC_EVAL] = {"eval", OUT}, | ||
| 46 | [EXEC_LINE] = {"line", IN}, | ||
| 47 | [EXEC_MSG] = {"msg", IN | OUT}, | ||
| 48 | [EXEC_NULL] = {"null", ALL}, | ||
| 49 | [EXEC_OPEN] = {"open", OUT}, | ||
| 50 | [EXEC_SEARCH] = {"search", IN}, | ||
| 51 | [EXEC_TAG] = {"tag", OUT}, | ||
| 52 | [EXEC_TTY] = {"tty", ALL}, | ||
| 53 | [EXEC_WORD] = {"word", IN}, | ||
| 54 | }; | ||
| 55 | |||
| 56 | UNITTEST { | ||
| 57 | CHECK_BSEARCH_ARRAY(exec_map, name, strcmp); | ||
| 58 | } | ||
| 59 | |||
| 60 | ExecAction lookup_exec_action(const char *name, int fd) | ||
| 61 | { | ||
| 62 | BUG_ON(fd < 0 || fd > 2); | ||
| 63 | ssize_t i = BSEARCH_IDX(name, exec_map, vstrcmp); | ||
| 64 | return (i >= 0 && (exec_map[i].flags & 1u << fd)) ? i : EXEC_INVALID; | ||
| 65 | } | ||
| 66 | |||
| 67 | static void open_files_from_string(EditorState *e, const String *str) | ||
| 68 | { | ||
| 69 | PointerArray filenames = PTR_ARRAY_INIT; | ||
| 70 | for (size_t pos = 0, size = str->len; pos < size; ) { | ||
| 71 | char *filename = buf_next_line(str->buffer, &pos, size); | ||
| 72 | if (filename[0] != '\0') { | ||
| 73 | ptr_array_append(&filenames, filename); | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | if (filenames.count == 0) { | ||
| 78 | return; | ||
| 79 | } | ||
| 80 | |||
| 81 | ptr_array_append(&filenames, NULL); | ||
| 82 | window_open_files(e->window, (char**)filenames.ptrs, NULL); | ||
| 83 | |||
| 84 | // TODO: re-enable this when the todo in allow_macro_recording() is done | ||
| 85 | // macro_command_hook(&e->macro, "open", (char**)filenames.ptrs); | ||
| 86 | |||
| 87 | ptr_array_free_array(&filenames); | ||
| 88 | } | ||
| 89 | |||
| 90 | static void parse_and_activate_message(EditorState *e, const String *str) | ||
| 91 | { | ||
| 92 | MessageArray *msgs = &e->messages; | ||
| 93 | size_t count = msgs->array.count; | ||
| 94 | size_t x; | ||
| 95 | if (!count || !buf_parse_size(str->buffer, str->len, &x) || !x) { | ||
| 96 | return; | ||
| 97 | } | ||
| 98 | msgs->pos = MIN(x - 1, count - 1); | ||
| 99 | activate_current_message(e); | ||
| 100 | } | ||
| 101 | |||
| 102 | static void parse_and_goto_tag(EditorState *e, const String *str) | ||
| 103 | { | ||
| 104 | if (unlikely(str->len == 0)) { | ||
| 105 | error_msg("child produced no output"); | ||
| 106 | return; | ||
| 107 | } | ||
| 108 | |||
| 109 | Tag tag; | ||
| 110 | size_t pos = 0; | ||
| 111 | StringView line = buf_slice_next_line(str->buffer, &pos, str->len); | ||
| 112 | if (pos == 0) { | ||
| 113 | return; | ||
| 114 | } | ||
| 115 | |||
| 116 | if (!parse_ctags_line(&tag, line.data, line.length)) { | ||
| 117 | // Treat line as simple tag name | ||
| 118 | tag_lookup(&e->tagfile, &line, e->buffer->abs_filename, &e->messages); | ||
| 119 | goto activate; | ||
| 120 | } | ||
| 121 | |||
| 122 | char buf[8192]; | ||
| 123 | const char *cwd = getcwd(buf, sizeof buf); | ||
| 124 | if (unlikely(!cwd)) { | ||
| 125 | error_msg_errno("getcwd() failed"); | ||
| 126 | return; | ||
| 127 | } | ||
| 128 | |||
| 129 | StringView dir = strview_from_cstring(cwd); | ||
| 130 | clear_messages(&e->messages); | ||
| 131 | add_message_for_tag(&e->messages, &tag, &dir); | ||
| 132 | |||
| 133 | activate: | ||
| 134 | activate_current_message_save(e); | ||
| 135 | } | ||
| 136 | |||
| 137 | static const char **lines_and_columns_env(const Window *window) | ||
| 138 | { | ||
| 139 | static char lines[DECIMAL_STR_MAX(window->edit_h)]; | ||
| 140 | static char columns[DECIMAL_STR_MAX(window->edit_w)]; | ||
| 141 | static const char *vars[] = { | ||
| 142 | "LINES", lines, | ||
| 143 | "COLUMNS", columns, | ||
| 144 | NULL, | ||
| 145 | }; | ||
| 146 | |||
| 147 | buf_uint_to_str(window->edit_h, lines); | ||
| 148 | buf_uint_to_str(window->edit_w, columns); | ||
| 149 | return vars; | ||
| 150 | } | ||
| 151 | |||
| 152 | static void show_spawn_error_msg(const String *errstr, int err) | ||
| 153 | { | ||
| 154 | if (err <= 0) { | ||
| 155 | return; | ||
| 156 | } | ||
| 157 | |||
| 158 | char msg[512]; | ||
| 159 | msg[0] = '\0'; | ||
| 160 | if (errstr->len) { | ||
| 161 | size_t pos = 0; | ||
| 162 | StringView line = buf_slice_next_line(errstr->buffer, &pos, errstr->len); | ||
| 163 | BUG_ON(pos == 0); | ||
| 164 | size_t len = MIN(line.length, sizeof(msg) - 8); | ||
| 165 | xsnprintf(msg, sizeof(msg), ": \"%.*s\"", (int)len, line.data); | ||
| 166 | } | ||
| 167 | |||
| 168 | if (err >= 256) { | ||
| 169 | int sig = err >> 8; | ||
| 170 | const char *str = strsignal(sig); | ||
| 171 | error_msg("Child received signal %d (%s)%s", sig, str ? str : "??", msg); | ||
| 172 | } else if (err) { | ||
| 173 | error_msg("Child returned %d%s", err, msg); | ||
| 174 | } | ||
| 175 | } | ||
| 176 | |||
| 177 | static SpawnAction spawn_action_from_exec_action(ExecAction action) | ||
| 178 | { | ||
| 179 | BUG_ON(action == EXEC_INVALID); | ||
| 180 | if (action == EXEC_NULL) { | ||
| 181 | return SPAWN_NULL; | ||
| 182 | } else if (action == EXEC_TTY) { | ||
| 183 | return SPAWN_TTY; | ||
| 184 | } else { | ||
| 185 | return SPAWN_PIPE; | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | ssize_t handle_exec ( | ||
| 190 | EditorState *e, | ||
| 191 | const char **argv, | ||
| 192 | ExecAction actions[3], | ||
| 193 | SpawnFlags spawn_flags, | ||
| 194 | bool strip_trailing_newline | ||
| 195 | ) { | ||
| 196 | View *view = e->view; | ||
| 197 | const BlockIter saved_cursor = view->cursor; | ||
| 198 | const ssize_t saved_sel_so = view->sel_so; | ||
| 199 | const ssize_t saved_sel_eo = view->sel_eo; | ||
| 200 | char *alloc = NULL; | ||
| 201 | bool output_to_buffer = (actions[STDOUT_FILENO] == EXEC_BUFFER); | ||
| 202 | bool replace_input = false; | ||
| 203 | |||
| 204 | SpawnContext ctx = { | ||
| 205 | .editor = e, | ||
| 206 | .argv = argv, | ||
| 207 | .outputs = {STRING_INIT, STRING_INIT}, | ||
| 208 | .flags = spawn_flags, | ||
| 209 | .env = output_to_buffer ? lines_and_columns_env(e->window) : NULL, | ||
| 210 | .actions = { | ||
| 211 | spawn_action_from_exec_action(actions[0]), | ||
| 212 | spawn_action_from_exec_action(actions[1]), | ||
| 213 | spawn_action_from_exec_action(actions[2]), | ||
| 214 | }, | ||
| 215 | }; | ||
| 216 | |||
| 217 | switch (actions[STDIN_FILENO]) { | ||
| 218 | case EXEC_LINE: | ||
| 219 | if (view->selection) { | ||
| 220 | ctx.input.length = prepare_selection(view); | ||
| 221 | } else { | ||
| 222 | StringView line; | ||
| 223 | move_bol(view); | ||
| 224 | fill_line_ref(&view->cursor, &line); | ||
| 225 | ctx.input.length = line.length; | ||
| 226 | } | ||
| 227 | replace_input = true; | ||
| 228 | get_bytes: | ||
| 229 | alloc = block_iter_get_bytes(&view->cursor, ctx.input.length); | ||
| 230 | ctx.input.data = alloc; | ||
| 231 | break; | ||
| 232 | case EXEC_BUFFER: | ||
| 233 | if (view->selection) { | ||
| 234 | ctx.input.length = prepare_selection(view); | ||
| 235 | } else { | ||
| 236 | Block *blk; | ||
| 237 | block_for_each(blk, &view->buffer->blocks) { | ||
| 238 | ctx.input.length += blk->size; | ||
| 239 | } | ||
| 240 | move_bof(view); | ||
| 241 | } | ||
| 242 | replace_input = true; | ||
| 243 | goto get_bytes; | ||
| 244 | case EXEC_WORD: | ||
| 245 | if (view->selection) { | ||
| 246 | ctx.input.length = prepare_selection(view); | ||
| 247 | replace_input = true; | ||
| 248 | } else { | ||
| 249 | size_t offset; | ||
| 250 | StringView word = view_do_get_word_under_cursor(e->view, &offset); | ||
| 251 | if (word.length == 0) { | ||
| 252 | break; | ||
| 253 | } | ||
| 254 | // TODO: optimize this, so that the BlockIter moves by just the | ||
| 255 | // minimal word offset instead of iterating to a line offset | ||
| 256 | ctx.input.length = word.length; | ||
| 257 | move_bol(view); | ||
| 258 | view->cursor.offset += offset; | ||
| 259 | BUG_ON(view->cursor.offset >= view->cursor.blk->size); | ||
| 260 | } | ||
| 261 | goto get_bytes; | ||
| 262 | case EXEC_MSG: { | ||
| 263 | String messages = dump_messages(&e->messages); | ||
| 264 | ctx.input = strview_from_string(&messages), | ||
| 265 | alloc = messages.buffer; | ||
| 266 | break; | ||
| 267 | } | ||
| 268 | case EXEC_COMMAND: { | ||
| 269 | String hist = dump_command_history(e); | ||
| 270 | ctx.input = strview_from_string(&hist), | ||
| 271 | alloc = hist.buffer; | ||
| 272 | break; | ||
| 273 | } | ||
| 274 | case EXEC_SEARCH: { | ||
| 275 | String hist = dump_search_history(e); | ||
| 276 | ctx.input = strview_from_string(&hist), | ||
| 277 | alloc = hist.buffer; | ||
| 278 | break; | ||
| 279 | } | ||
| 280 | case EXEC_NULL: | ||
| 281 | case EXEC_TTY: | ||
| 282 | break; | ||
| 283 | // These can't be used as input actions and should be prevented by | ||
| 284 | // the validity checks in cmd_exec(): | ||
| 285 | case EXEC_OPEN: | ||
| 286 | case EXEC_TAG: | ||
| 287 | case EXEC_EVAL: | ||
| 288 | case EXEC_ERRMSG: | ||
| 289 | case EXEC_INVALID: | ||
| 290 | default: | ||
| 291 | BUG("unhandled action"); | ||
| 292 | return -1; | ||
| 293 | } | ||
| 294 | |||
| 295 | int err = spawn(&ctx); | ||
| 296 | free(alloc); | ||
| 297 | if (err != 0) { | ||
| 298 | show_spawn_error_msg(&ctx.outputs[1], err); | ||
| 299 | string_free(&ctx.outputs[0]); | ||
| 300 | string_free(&ctx.outputs[1]); | ||
| 301 | view->cursor = saved_cursor; | ||
| 302 | return -1; | ||
| 303 | } | ||
| 304 | |||
| 305 | string_free(&ctx.outputs[1]); | ||
| 306 | String *output = &ctx.outputs[0]; | ||
| 307 | if ( | ||
| 308 | strip_trailing_newline | ||
| 309 | && output_to_buffer | ||
| 310 | && output->len > 0 | ||
| 311 | && output->buffer[output->len - 1] == '\n' | ||
| 312 | ) { | ||
| 313 | output->len--; | ||
| 314 | if (output->len > 0 && output->buffer[output->len - 1] == '\r') { | ||
| 315 | output->len--; | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | if (!output_to_buffer) { | ||
| 320 | view->cursor = saved_cursor; | ||
| 321 | view->sel_so = saved_sel_so; | ||
| 322 | view->sel_eo = saved_sel_eo; | ||
| 323 | mark_all_lines_changed(view->buffer); | ||
| 324 | } | ||
| 325 | |||
| 326 | switch (actions[STDOUT_FILENO]) { | ||
| 327 | case EXEC_BUFFER: | ||
| 328 | if (replace_input || view->selection) { | ||
| 329 | size_t del_count = replace_input ? ctx.input.length : prepare_selection(view); | ||
| 330 | buffer_replace_bytes(view, del_count, output->buffer, output->len); | ||
| 331 | unselect(view); | ||
| 332 | } else { | ||
| 333 | buffer_insert_bytes(view, output->buffer, output->len); | ||
| 334 | } | ||
| 335 | break; | ||
| 336 | case EXEC_MSG: | ||
| 337 | parse_and_activate_message(e, output); | ||
| 338 | break; | ||
| 339 | case EXEC_OPEN: | ||
| 340 | open_files_from_string(e, output); | ||
| 341 | break; | ||
| 342 | case EXEC_TAG: | ||
| 343 | parse_and_goto_tag(e, output); | ||
| 344 | break; | ||
| 345 | case EXEC_EVAL: | ||
| 346 | exec_normal_config(e, strview_from_string(output)); | ||
| 347 | break; | ||
| 348 | case EXEC_NULL: | ||
| 349 | case EXEC_TTY: | ||
| 350 | break; | ||
| 351 | // These can't be used as output actions | ||
| 352 | case EXEC_COMMAND: | ||
| 353 | case EXEC_ERRMSG: | ||
| 354 | case EXEC_LINE: | ||
| 355 | case EXEC_SEARCH: | ||
| 356 | case EXEC_WORD: | ||
| 357 | case EXEC_INVALID: | ||
| 358 | default: | ||
| 359 | BUG("unhandled action"); | ||
| 360 | return -1; | ||
| 361 | } | ||
| 362 | |||
| 363 | size_t output_len = output->len; | ||
| 364 | string_free(output); | ||
| 365 | return output_len; | ||
| 366 | } | ||
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 @@ | |||
| 1 | #ifndef EXEC_H | ||
| 2 | #define EXEC_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <sys/types.h> | ||
| 6 | #include "editor.h" | ||
| 7 | #include "spawn.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | |||
| 10 | typedef enum { | ||
| 11 | EXEC_INVALID = -1, | ||
| 12 | // Note: items below here need to be kept sorted | ||
| 13 | EXEC_BUFFER = 0, | ||
| 14 | EXEC_COMMAND, | ||
| 15 | EXEC_ERRMSG, | ||
| 16 | EXEC_EVAL, | ||
| 17 | EXEC_LINE, | ||
| 18 | EXEC_MSG, | ||
| 19 | EXEC_NULL, | ||
| 20 | EXEC_OPEN, | ||
| 21 | EXEC_SEARCH, | ||
| 22 | EXEC_TAG, | ||
| 23 | EXEC_TTY, | ||
| 24 | EXEC_WORD, | ||
| 25 | } ExecAction; | ||
| 26 | |||
| 27 | ssize_t handle_exec ( | ||
| 28 | EditorState *e, | ||
| 29 | const char **argv, | ||
| 30 | ExecAction actions[3], | ||
| 31 | SpawnFlags spawn_flags, | ||
| 32 | bool strip_trailing_newline | ||
| 33 | ) NONNULL_ARGS; | ||
| 34 | |||
| 35 | ExecAction lookup_exec_action(const char *name, int fd) NONNULL_ARGS; | ||
| 36 | |||
| 37 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include <sys/types.h> | ||
| 5 | #include "file-history.h" | ||
| 6 | #include "error.h" | ||
| 7 | #include "util/debug.h" | ||
| 8 | #include "util/readfile.h" | ||
| 9 | #include "util/str-util.h" | ||
| 10 | #include "util/string-view.h" | ||
| 11 | #include "util/strtonum.h" | ||
| 12 | #include "util/xmalloc.h" | ||
| 13 | #include "util/xstdio.h" | ||
| 14 | |||
| 15 | enum { | ||
| 16 | MAX_ENTRIES = 512 | ||
| 17 | }; | ||
| 18 | |||
| 19 | void file_history_add(FileHistory *history, unsigned long row, unsigned long col, const char *filename) | ||
| 20 | { | ||
| 21 | BUG_ON(row == 0); | ||
| 22 | BUG_ON(col == 0); | ||
| 23 | HashMap *map = &history->entries; | ||
| 24 | FileHistoryEntry *e = hashmap_get(map, filename); | ||
| 25 | |||
| 26 | if (e) { | ||
| 27 | if (e == history->last) { | ||
| 28 | e->row = row; | ||
| 29 | e->col = col; | ||
| 30 | return; | ||
| 31 | } | ||
| 32 | e->next->prev = e->prev; | ||
| 33 | if (unlikely(e == history->first)) { | ||
| 34 | history->first = e->next; | ||
| 35 | } else { | ||
| 36 | e->prev->next = e->next; | ||
| 37 | } | ||
| 38 | } else { | ||
| 39 | if (map->count == MAX_ENTRIES) { | ||
| 40 | // History is full; recycle the oldest entry | ||
| 41 | FileHistoryEntry *old_first = history->first; | ||
| 42 | FileHistoryEntry *new_first = old_first->next; | ||
| 43 | new_first->prev = NULL; | ||
| 44 | history->first = new_first; | ||
| 45 | e = hashmap_remove(map, old_first->filename); | ||
| 46 | BUG_ON(e != old_first); | ||
| 47 | } else { | ||
| 48 | e = xnew(FileHistoryEntry, 1); | ||
| 49 | } | ||
| 50 | e->filename = xstrdup(filename); | ||
| 51 | hashmap_insert(map, e->filename, e); | ||
| 52 | } | ||
| 53 | |||
| 54 | // Insert the entry at the end of the list | ||
| 55 | FileHistoryEntry *old_last = history->last; | ||
| 56 | e->next = NULL; | ||
| 57 | e->prev = old_last; | ||
| 58 | e->row = row; | ||
| 59 | e->col = col; | ||
| 60 | history->last = e; | ||
| 61 | if (likely(old_last)) { | ||
| 62 | old_last->next = e; | ||
| 63 | } else { | ||
| 64 | history->first = e; | ||
| 65 | } | ||
| 66 | } | ||
| 67 | |||
| 68 | static bool parse_ulong_field(StringView *sv, unsigned long *valp) | ||
| 69 | { | ||
| 70 | size_t n = buf_parse_ulong(sv->data, sv->length, valp); | ||
| 71 | if (n == 0 || *valp == 0 || sv->data[n] != ' ') { | ||
| 72 | return false; | ||
| 73 | } | ||
| 74 | strview_remove_prefix(sv, n + 1); | ||
| 75 | return true; | ||
| 76 | } | ||
| 77 | |||
| 78 | void file_history_load(FileHistory *history, char *filename) | ||
| 79 | { | ||
| 80 | BUG_ON(!history); | ||
| 81 | BUG_ON(!filename); | ||
| 82 | BUG_ON(history->filename); | ||
| 83 | |||
| 84 | hashmap_init(&history->entries, MAX_ENTRIES); | ||
| 85 | history->filename = filename; | ||
| 86 | |||
| 87 | char *buf; | ||
| 88 | const ssize_t ssize = read_file(filename, &buf); | ||
| 89 | if (ssize < 0) { | ||
| 90 | if (errno != ENOENT) { | ||
| 91 | error_msg("Error reading %s: %s", filename, strerror(errno)); | ||
| 92 | } | ||
| 93 | return; | ||
| 94 | } | ||
| 95 | |||
| 96 | for (size_t pos = 0, size = ssize; pos < size; ) { | ||
| 97 | unsigned long row, col; | ||
| 98 | StringView line = buf_slice_next_line(buf, &pos, size); | ||
| 99 | if (unlikely( | ||
| 100 | !parse_ulong_field(&line, &row) | ||
| 101 | || !parse_ulong_field(&line, &col) | ||
| 102 | || line.length < 2 | ||
| 103 | || line.data[0] != '/' | ||
| 104 | || buf[pos - 1] != '\n' | ||
| 105 | )) { | ||
| 106 | continue; | ||
| 107 | } | ||
| 108 | buf[pos - 1] = '\0'; // null-terminate line, by replacing '\n' with '\0' | ||
| 109 | file_history_add(history, row, col, line.data); | ||
| 110 | } | ||
| 111 | |||
| 112 | free(buf); | ||
| 113 | } | ||
| 114 | |||
| 115 | void file_history_save(const FileHistory *history) | ||
| 116 | { | ||
| 117 | const char *filename = history->filename; | ||
| 118 | if (!filename) { | ||
| 119 | return; | ||
| 120 | } | ||
| 121 | |||
| 122 | FILE *f = xfopen(filename, "w", O_CLOEXEC, 0666); | ||
| 123 | if (!f) { | ||
| 124 | error_msg("Error creating %s: %s", filename, strerror(errno)); | ||
| 125 | return; | ||
| 126 | } | ||
| 127 | |||
| 128 | for (const FileHistoryEntry *e = history->first; e; e = e->next) { | ||
| 129 | xfprintf(f, "%lu %lu %s\n", e->row, e->col, e->filename); | ||
| 130 | } | ||
| 131 | |||
| 132 | fclose(f); | ||
| 133 | } | ||
| 134 | |||
| 135 | bool file_history_find(const FileHistory *history, const char *filename, unsigned long *row, unsigned long *col) | ||
| 136 | { | ||
| 137 | const FileHistoryEntry *e = hashmap_get(&history->entries, filename); | ||
| 138 | if (!e) { | ||
| 139 | return false; | ||
| 140 | } | ||
| 141 | *row = e->row; | ||
| 142 | *col = e->col; | ||
| 143 | return true; | ||
| 144 | } | ||
| 145 | |||
| 146 | void file_history_free(FileHistory *history) | ||
| 147 | { | ||
| 148 | hashmap_free(&history->entries, free); | ||
| 149 | free(history->filename); | ||
| 150 | history->filename = NULL; | ||
| 151 | history->first = NULL; | ||
| 152 | history->last = NULL; | ||
| 153 | } | ||
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 @@ | |||
| 1 | #ifndef FILE_HISTORY_H | ||
| 2 | #define FILE_HISTORY_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "util/hashmap.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | |||
| 8 | typedef struct FileHistoryEntry { | ||
| 9 | struct FileHistoryEntry *next; | ||
| 10 | struct FileHistoryEntry *prev; | ||
| 11 | char *filename; | ||
| 12 | unsigned long row; | ||
| 13 | unsigned long col; | ||
| 14 | } FileHistoryEntry; | ||
| 15 | |||
| 16 | typedef struct { | ||
| 17 | char *filename; | ||
| 18 | HashMap entries; | ||
| 19 | FileHistoryEntry *first; | ||
| 20 | FileHistoryEntry *last; | ||
| 21 | } FileHistory; | ||
| 22 | |||
| 23 | void file_history_add(FileHistory *hist, unsigned long row, unsigned long col, const char *filename); | ||
| 24 | void file_history_load(FileHistory *hist, char *filename); | ||
| 25 | void file_history_save(const FileHistory *hist); | ||
| 26 | bool file_history_find(const FileHistory *hist, const char *filename, unsigned long *row, unsigned long *col) WARN_UNUSED_RESULT; | ||
| 27 | void file_history_free(FileHistory *history); | ||
| 28 | |||
| 29 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <unistd.h> | ||
| 4 | #include "file-option.h" | ||
| 5 | #include "command/serialize.h" | ||
| 6 | #include "editor.h" | ||
| 7 | #include "editorconfig/editorconfig.h" | ||
| 8 | #include "error.h" | ||
| 9 | #include "options.h" | ||
| 10 | #include "regexp.h" | ||
| 11 | #include "util/debug.h" | ||
| 12 | #include "util/str-util.h" | ||
| 13 | #include "util/xmalloc.h" | ||
| 14 | |||
| 15 | typedef struct { | ||
| 16 | FileOptionType type; | ||
| 17 | char **strs; | ||
| 18 | union { | ||
| 19 | char *filetype; | ||
| 20 | CachedRegexp *filename; | ||
| 21 | } u; | ||
| 22 | } FileOption; | ||
| 23 | |||
| 24 | static void set_options(EditorState *e, char **args) | ||
| 25 | { | ||
| 26 | for (size_t i = 0; args[i]; i += 2) { | ||
| 27 | set_option(e, args[i], args[i + 1], true, false); | ||
| 28 | } | ||
| 29 | } | ||
| 30 | |||
| 31 | void set_editorconfig_options(Buffer *buffer) | ||
| 32 | { | ||
| 33 | LocalOptions *options = &buffer->options; | ||
| 34 | if (!options->editorconfig) { | ||
| 35 | return; | ||
| 36 | } | ||
| 37 | |||
| 38 | const char *path = buffer->abs_filename; | ||
| 39 | char cwd[8192]; | ||
| 40 | if (!path) { | ||
| 41 | // For buffers with no associated filename, use a dummy path of | ||
| 42 | // "$PWD/__", to obtain generic settings for the working directory | ||
| 43 | // or the user's default settings | ||
| 44 | static const char suffix[] = "/__"; | ||
| 45 | if (unlikely(!getcwd(cwd, sizeof(cwd) - sizeof(suffix)))) { | ||
| 46 | return; | ||
| 47 | } | ||
| 48 | memcpy(cwd + strlen(cwd), suffix, sizeof(suffix)); | ||
| 49 | path = cwd; | ||
| 50 | } | ||
| 51 | |||
| 52 | EditorConfigOptions opts; | ||
| 53 | if (get_editorconfig_options(path, &opts) != 0) { | ||
| 54 | return; | ||
| 55 | } | ||
| 56 | |||
| 57 | switch (opts.indent_style) { | ||
| 58 | case INDENT_STYLE_SPACE: | ||
| 59 | options->expand_tab = true; | ||
| 60 | options->emulate_tab = true; | ||
| 61 | options->detect_indent = 0; | ||
| 62 | break; | ||
| 63 | case INDENT_STYLE_TAB: | ||
| 64 | options->expand_tab = false; | ||
| 65 | options->emulate_tab = false; | ||
| 66 | options->detect_indent = 0; | ||
| 67 | break; | ||
| 68 | case INDENT_STYLE_UNSPECIFIED: | ||
| 69 | break; | ||
| 70 | } | ||
| 71 | |||
| 72 | const unsigned int indent_size = opts.indent_size; | ||
| 73 | if (indent_size > 0 && indent_size <= INDENT_WIDTH_MAX) { | ||
| 74 | options->indent_width = indent_size; | ||
| 75 | options->detect_indent = 0; | ||
| 76 | } | ||
| 77 | |||
| 78 | const unsigned int tab_width = opts.tab_width; | ||
| 79 | if (tab_width > 0 && tab_width <= TAB_WIDTH_MAX) { | ||
| 80 | options->tab_width = tab_width; | ||
| 81 | } | ||
| 82 | |||
| 83 | const unsigned int max_line_length = opts.max_line_length; | ||
| 84 | if (max_line_length > 0 && max_line_length <= TEXT_WIDTH_MAX) { | ||
| 85 | options->text_width = max_line_length; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | void set_file_options(EditorState *e, Buffer *buffer) | ||
| 90 | { | ||
| 91 | for (size_t i = 0, n = e->file_options.count; i < n; i++) { | ||
| 92 | const FileOption *opt = e->file_options.ptrs[i]; | ||
| 93 | if (opt->type == FOPTS_FILETYPE) { | ||
| 94 | if (streq(opt->u.filetype, buffer->options.filetype)) { | ||
| 95 | set_options(e, opt->strs); | ||
| 96 | } | ||
| 97 | continue; | ||
| 98 | } | ||
| 99 | |||
| 100 | BUG_ON(opt->type != FOPTS_FILENAME); | ||
| 101 | const char *filename = buffer->abs_filename; | ||
| 102 | if (!filename) { | ||
| 103 | continue; | ||
| 104 | } | ||
| 105 | |||
| 106 | const regex_t *re = &opt->u.filename->re; | ||
| 107 | regmatch_t m; | ||
| 108 | if (regexp_exec(re, filename, strlen(filename), 0, &m, 0)) { | ||
| 109 | set_options(e, opt->strs); | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | |||
| 114 | bool add_file_options(PointerArray *file_options, FileOptionType type, StringView str, char **strs, size_t nstrs) | ||
| 115 | { | ||
| 116 | size_t len = str.length; | ||
| 117 | if (unlikely(len == 0)) { | ||
| 118 | const char *desc = (type == FOPTS_FILETYPE) ? "filetype" : "pattern"; | ||
| 119 | return error_msg("can't add option with empty %s", desc); | ||
| 120 | } | ||
| 121 | |||
| 122 | FileOption *opt = xnew(FileOption, 1); | ||
| 123 | if (type == FOPTS_FILETYPE) { | ||
| 124 | opt->u.filetype = xstrcut(str.data, len); | ||
| 125 | goto append; | ||
| 126 | } | ||
| 127 | |||
| 128 | BUG_ON(type != FOPTS_FILENAME); | ||
| 129 | CachedRegexp *r = xmalloc(sizeof(*r) + len + 1); | ||
| 130 | memcpy(r->str, str.data, len); | ||
| 131 | r->str[len] = '\0'; | ||
| 132 | opt->u.filename = r; | ||
| 133 | |||
| 134 | int err = regcomp(&r->re, r->str, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB); | ||
| 135 | if (unlikely(err)) { | ||
| 136 | regexp_error_msg(&r->re, r->str, err); | ||
| 137 | free(r); | ||
| 138 | free(opt); | ||
| 139 | return false; | ||
| 140 | } | ||
| 141 | |||
| 142 | append: | ||
| 143 | opt->type = type; | ||
| 144 | opt->strs = copy_string_array(strs, nstrs); | ||
| 145 | ptr_array_append(file_options, opt); | ||
| 146 | return true; | ||
| 147 | } | ||
| 148 | |||
| 149 | void dump_file_options(const PointerArray *file_options, String *buf) | ||
| 150 | { | ||
| 151 | for (size_t i = 0, n = file_options->count; i < n; i++) { | ||
| 152 | const FileOption *opt = file_options->ptrs[i]; | ||
| 153 | const char *tp; | ||
| 154 | if (opt->type == FOPTS_FILENAME) { | ||
| 155 | tp = opt->u.filename->str; | ||
| 156 | } else { | ||
| 157 | tp = opt->u.filetype; | ||
| 158 | } | ||
| 159 | char **strs = opt->strs; | ||
| 160 | string_append_literal(buf, "option "); | ||
| 161 | if (opt->type == FOPTS_FILENAME) { | ||
| 162 | string_append_literal(buf, "-r "); | ||
| 163 | } | ||
| 164 | if (str_has_prefix(tp, "-") || string_array_contains_prefix(strs, "-")) { | ||
| 165 | string_append_literal(buf, "-- "); | ||
| 166 | } | ||
| 167 | string_append_escaped_arg(buf, tp, true); | ||
| 168 | for (size_t j = 0; strs[j]; j += 2) { | ||
| 169 | string_append_byte(buf, ' '); | ||
| 170 | string_append_cstring(buf, strs[j]); | ||
| 171 | string_append_byte(buf, ' '); | ||
| 172 | string_append_escaped_arg(buf, strs[j + 1], true); | ||
| 173 | } | ||
| 174 | string_append_byte(buf, '\n'); | ||
| 175 | } | ||
| 176 | } | ||
| 177 | |||
| 178 | static void free_file_option(FileOption *opt) | ||
| 179 | { | ||
| 180 | if (opt->type == FOPTS_FILENAME) { | ||
| 181 | free_cached_regexp(opt->u.filename); | ||
| 182 | } else { | ||
| 183 | BUG_ON(opt->type != FOPTS_FILETYPE); | ||
| 184 | free(opt->u.filetype); | ||
| 185 | } | ||
| 186 | free_string_array(opt->strs); | ||
| 187 | free(opt); | ||
| 188 | } | ||
| 189 | |||
| 190 | void free_file_options(PointerArray *file_options) | ||
| 191 | { | ||
| 192 | ptr_array_free_cb(file_options, FREE_FUNC(free_file_option)); | ||
| 193 | } | ||
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 @@ | |||
| 1 | #ifndef FILE_OPTION_H | ||
| 2 | #define FILE_OPTION_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "buffer.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/ptr-array.h" | ||
| 8 | #include "util/string-view.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | |||
| 11 | typedef enum { | ||
| 12 | FOPTS_FILENAME, | ||
| 13 | FOPTS_FILETYPE, | ||
| 14 | } FileOptionType; | ||
| 15 | |||
| 16 | struct EditorState; | ||
| 17 | |||
| 18 | void set_file_options(struct EditorState *e, Buffer *buffer) NONNULL_ARGS; | ||
| 19 | void set_editorconfig_options(Buffer *buffer) NONNULL_ARGS; | ||
| 20 | void dump_file_options(const PointerArray *file_options, String *buf); | ||
| 21 | void free_file_options(PointerArray *file_options); | ||
| 22 | |||
| 23 | NONNULL_ARGS WARN_UNUSED_RESULT | ||
| 24 | bool add_file_options ( | ||
| 25 | PointerArray *file_options, | ||
| 26 | FileOptionType type, | ||
| 27 | StringView str, | ||
| 28 | char **strs, | ||
| 29 | size_t nstrs | ||
| 30 | ); | ||
| 31 | |||
| 32 | #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 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include "filetype.h" | ||
| 4 | #include "command/serialize.h" | ||
| 5 | #include "regexp.h" | ||
| 6 | #include "util/array.h" | ||
| 7 | #include "util/ascii.h" | ||
| 8 | #include "util/bsearch.h" | ||
| 9 | #include "util/debug.h" | ||
| 10 | #include "util/path.h" | ||
| 11 | #include "util/str-util.h" | ||
| 12 | #include "util/xmalloc.h" | ||
| 13 | |||
| 14 | static int ft_compare(const void *key, const void *elem) | ||
| 15 | { | ||
| 16 | const StringView *sv = key; | ||
| 17 | const char *ext = elem; // Cast to first member of struct | ||
| 18 | int res = memcmp(sv->data, ext, sv->length); | ||
| 19 | if (unlikely(res == 0 && ext[sv->length] != '\0')) { | ||
| 20 | res = -1; | ||
| 21 | } | ||
| 22 | return res; | ||
| 23 | } | ||
| 24 | |||
| 25 | // Built-in filetypes | ||
| 26 | #include "filetype/names.c" | ||
| 27 | #include "filetype/basenames.c" | ||
| 28 | #include "filetype/directories.c" | ||
| 29 | #include "filetype/extensions.c" | ||
| 30 | #include "filetype/interpreters.c" | ||
| 31 | #include "filetype/ignored-exts.c" | ||
| 32 | #include "filetype/signatures.c" | ||
| 33 | |||
| 34 | UNITTEST { | ||
| 35 | static_assert(NR_BUILTIN_FILETYPES < 256); | ||
| 36 | CHECK_BSEARCH_ARRAY(basenames, name, strcmp); | ||
| 37 | CHECK_BSEARCH_ARRAY(extensions, ext, strcmp); | ||
| 38 | CHECK_BSEARCH_ARRAY(interpreters, key, strcmp); | ||
| 39 | CHECK_BSEARCH_STR_ARRAY(ignored_extensions, strcmp); | ||
| 40 | CHECK_BSEARCH_STR_ARRAY(builtin_filetype_names, strcmp); | ||
| 41 | |||
| 42 | for (size_t i = 0; i < ARRAYLEN(builtin_filetype_names); i++) { | ||
| 43 | const char *name = builtin_filetype_names[i]; | ||
| 44 | if (unlikely(!is_valid_filetype_name(name))) { | ||
| 45 | BUG("invalid name at builtin_filetype_names[%zu]: \"%s\"", i, name); | ||
| 46 | } | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | typedef struct { | ||
| 51 | unsigned int str_len; | ||
| 52 | char str[]; | ||
| 53 | } FlexArrayStr; | ||
| 54 | |||
| 55 | // Filetypes dynamically added via the `ft` command. | ||
| 56 | // Not grouped by name to make it possible to order them freely. | ||
| 57 | typedef struct { | ||
| 58 | union { | ||
| 59 | FlexArrayStr *str; | ||
| 60 | CachedRegexp *regexp; | ||
| 61 | } u; | ||
| 62 | uint8_t type; // FileDetectionType | ||
| 63 | char name[]; | ||
| 64 | } UserFileTypeEntry; | ||
| 65 | |||
| 66 | static bool ft_uses_regex(FileDetectionType type) | ||
| 67 | { | ||
| 68 | return type == FT_CONTENT || type == FT_FILENAME; | ||
| 69 | } | ||
| 70 | |||
| 71 | bool add_filetype(PointerArray *filetypes, const char *name, const char *str, FileDetectionType type) | ||
| 72 | { | ||
| 73 | BUG_ON(!is_valid_filetype_name(name)); | ||
| 74 | regex_t re; | ||
| 75 | bool use_re = ft_uses_regex(type); | ||
| 76 | if (use_re) { | ||
| 77 | int err = regcomp(&re, str, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB); | ||
| 78 | if (unlikely(err)) { | ||
| 79 | return regexp_error_msg(&re, str, err); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | size_t name_len = strlen(name); | ||
| 84 | size_t str_len = strlen(str); | ||
| 85 | UserFileTypeEntry *ft = xmalloc(sizeof(*ft) + name_len + 1); | ||
| 86 | ft->type = type; | ||
| 87 | |||
| 88 | char *str_dest; | ||
| 89 | if (use_re) { | ||
| 90 | CachedRegexp *r = xmalloc(sizeof(*r) + str_len + 1); | ||
| 91 | r->re = re; | ||
| 92 | ft->u.regexp = r; | ||
| 93 | str_dest = r->str; | ||
| 94 | } else { | ||
| 95 | FlexArrayStr *s = xmalloc(sizeof(*s) + str_len + 1); | ||
| 96 | s->str_len = str_len; | ||
| 97 | ft->u.str = s; | ||
| 98 | str_dest = s->str; | ||
| 99 | } | ||
| 100 | |||
| 101 | memcpy(ft->name, name, name_len + 1); | ||
| 102 | memcpy(str_dest, str, str_len + 1); | ||
| 103 | ptr_array_append(filetypes, ft); | ||
| 104 | return true; | ||
| 105 | } | ||
| 106 | |||
| 107 | static StringView path_extension(StringView filename) | ||
| 108 | { | ||
| 109 | StringView ext = STRING_VIEW_INIT; | ||
| 110 | ext.data = strview_memrchr(&filename, '.'); | ||
| 111 | if (!ext.data || ext.data == filename.data) { | ||
| 112 | return ext; | ||
| 113 | } | ||
| 114 | ext.data++; | ||
| 115 | ext.length = filename.length - (ext.data - filename.data); | ||
| 116 | return ext; | ||
| 117 | } | ||
| 118 | |||
| 119 | static StringView get_filename_extension(StringView filename) | ||
| 120 | { | ||
| 121 | StringView ext = path_extension(filename); | ||
| 122 | if (is_ignored_extension(ext)) { | ||
| 123 | filename.length -= ext.length + 1; | ||
| 124 | ext = path_extension(filename); | ||
| 125 | } | ||
| 126 | if (strview_has_suffix(&ext, "~")) { | ||
| 127 | ext.length--; | ||
| 128 | } | ||
| 129 | return ext; | ||
| 130 | } | ||
| 131 | |||
| 132 | // Parse hashbang and return interpreter name, without version number. | ||
| 133 | // For example, if line is "#!/usr/bin/env python2", "python" is returned. | ||
| 134 | static StringView get_interpreter(StringView line) | ||
| 135 | { | ||
| 136 | StringView sv = STRING_VIEW_INIT; | ||
| 137 | if (!strview_has_prefix(&line, "#!")) { | ||
| 138 | return sv; | ||
| 139 | } | ||
| 140 | |||
| 141 | strview_remove_prefix(&line, 2); | ||
| 142 | strview_trim_left(&line); | ||
| 143 | if (line.length < 2 || line.data[0] != '/') { | ||
| 144 | return sv; | ||
| 145 | } | ||
| 146 | |||
| 147 | size_t pos = 0; | ||
| 148 | sv = get_delim(line.data, &pos, line.length, ' '); | ||
| 149 | if (pos < line.length && strview_equal_cstring(&sv, "/usr/bin/env")) { | ||
| 150 | while (pos + 1 < line.length && line.data[pos] == ' ') { | ||
| 151 | pos++; | ||
| 152 | } | ||
| 153 | sv = get_delim(line.data, &pos, line.length, ' '); | ||
| 154 | } | ||
| 155 | |||
| 156 | ssize_t last_slash_idx = strview_memrchr_idx(&sv, '/'); | ||
| 157 | if (last_slash_idx >= 0) { | ||
| 158 | strview_remove_prefix(&sv, last_slash_idx + 1); | ||
| 159 | } | ||
| 160 | |||
| 161 | while (sv.length && ascii_is_digit_or_dot(sv.data[sv.length - 1])) { | ||
| 162 | sv.length--; | ||
| 163 | } | ||
| 164 | |||
| 165 | return sv; | ||
| 166 | } | ||
| 167 | |||
| 168 | static bool ft_str_match(const UserFileTypeEntry *ft, const StringView sv) | ||
| 169 | { | ||
| 170 | const char *str = ft->u.str->str; | ||
| 171 | const size_t len = ft->u.str->str_len; | ||
| 172 | return sv.length > 0 && strview_equal_strn(&sv, str, len); | ||
| 173 | } | ||
| 174 | |||
| 175 | static bool ft_regex_match(const UserFileTypeEntry *ft, const StringView sv) | ||
| 176 | { | ||
| 177 | const regex_t *re = &ft->u.regexp->re; | ||
| 178 | regmatch_t m; | ||
| 179 | return sv.length > 0 && regexp_exec(re, sv.data, sv.length, 0, &m, 0); | ||
| 180 | } | ||
| 181 | |||
| 182 | static bool ft_match(const UserFileTypeEntry *ft, const StringView sv) | ||
| 183 | { | ||
| 184 | if (ft_uses_regex(ft->type)) { | ||
| 185 | return ft_regex_match(ft, sv); | ||
| 186 | } | ||
| 187 | return ft_str_match(ft, sv); | ||
| 188 | } | ||
| 189 | |||
| 190 | const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line) | ||
| 191 | { | ||
| 192 | const char *b = filename ? path_basename(filename) : NULL; | ||
| 193 | const StringView base = strview_from_cstring(b); | ||
| 194 | const StringView ext = get_filename_extension(base); | ||
| 195 | const StringView path = strview_from_cstring(filename); | ||
| 196 | const StringView interpreter = get_interpreter(line); | ||
| 197 | BUG_ON(path.length == 0 && (base.length != 0 || ext.length != 0)); | ||
| 198 | BUG_ON(line.length == 0 && interpreter.length != 0); | ||
| 199 | |||
| 200 | // The order of elements in this array determines the order of | ||
| 201 | // precedence for the lookup() functions (but note that changing | ||
| 202 | // the initializer below makes no difference to the array order) | ||
| 203 | const struct { | ||
| 204 | StringView sv; | ||
| 205 | FileTypeEnum (*lookup)(const StringView sv); | ||
| 206 | } table[] = { | ||
| 207 | [FT_INTERPRETER] = {interpreter, filetype_from_interpreter}, | ||
| 208 | [FT_BASENAME] = {base, filetype_from_basename}, | ||
| 209 | [FT_CONTENT] = {line, filetype_from_signature}, | ||
| 210 | [FT_EXTENSION] = {ext, filetype_from_extension}, | ||
| 211 | [FT_FILENAME] = {path, filetype_from_dir_prefix}, | ||
| 212 | }; | ||
| 213 | |||
| 214 | // Search user `ft` entries | ||
| 215 | for (size_t i = 0, n = filetypes->count; i < n; i++) { | ||
| 216 | const UserFileTypeEntry *ft = filetypes->ptrs[i]; | ||
| 217 | if (ft_match(ft, table[ft->type].sv)) { | ||
| 218 | return ft->name; | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | // Search built-in lookup tables | ||
| 223 | for (FileDetectionType i = 0; i < ARRAYLEN(table); i++) { | ||
| 224 | BUG_ON(!table[i].lookup); | ||
| 225 | FileTypeEnum ft = table[i].lookup(table[i].sv); | ||
| 226 | if (ft != NONE) { | ||
| 227 | return builtin_filetype_names[ft]; | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | // Use "ini" filetype if first line looks like an ini [section] | ||
| 232 | strview_trim_right(&line); | ||
| 233 | if (line.length >= 4) { | ||
| 234 | const char *s = line.data; | ||
| 235 | const size_t n = line.length; | ||
| 236 | if (s[0] == '[' && s[n - 1] == ']' && is_word_byte(s[1])) { | ||
| 237 | if (!strview_contains_char_type(&line, ASCII_CNTRL)) { | ||
| 238 | return builtin_filetype_names[INI]; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | |||
| 243 | if (strview_equal_cstring(&ext, "conf")) { | ||
| 244 | if (strview_has_prefix(&path, "/etc/systemd/")) { | ||
| 245 | return builtin_filetype_names[INI]; | ||
| 246 | } | ||
| 247 | BUG_ON(!filename); | ||
| 248 | const StringView dir = path_slice_dirname(filename); | ||
| 249 | if ( | ||
| 250 | strview_has_prefix(&path, "/etc/") | ||
| 251 | || strview_has_prefix(&path, "/usr/share/") | ||
| 252 | || strview_has_prefix(&path, "/usr/local/share/") | ||
| 253 | || strview_has_suffix(&dir, "/tmpfiles.d") | ||
| 254 | ) { | ||
| 255 | return builtin_filetype_names[CONFIG]; | ||
| 256 | } | ||
| 257 | } | ||
| 258 | |||
| 259 | return NULL; | ||
| 260 | } | ||
| 261 | |||
| 262 | bool is_ft(const PointerArray *filetypes, const char *name) | ||
| 263 | { | ||
| 264 | if (BSEARCH(name, builtin_filetype_names, vstrcmp)) { | ||
| 265 | return true; | ||
| 266 | } | ||
| 267 | |||
| 268 | for (size_t i = 0, n = filetypes->count; i < n; i++) { | ||
| 269 | const UserFileTypeEntry *ft = filetypes->ptrs[i]; | ||
| 270 | if (streq(ft->name, name)) { | ||
| 271 | return true; | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | return false; | ||
| 276 | } | ||
| 277 | |||
| 278 | void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix) | ||
| 279 | { | ||
| 280 | COLLECT_STRINGS(builtin_filetype_names, a, prefix); | ||
| 281 | for (size_t i = 0, n = filetypes->count; i < n; i++) { | ||
| 282 | const UserFileTypeEntry *ft = filetypes->ptrs[i]; | ||
| 283 | const char *name = ft->name; | ||
| 284 | if (str_has_prefix(name, prefix)) { | ||
| 285 | ptr_array_append(a, xstrdup(name)); | ||
| 286 | } | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | static const char *ft_get_str(const UserFileTypeEntry *ft) | ||
| 291 | { | ||
| 292 | return ft_uses_regex(ft->type) ? ft->u.regexp->str : ft->u.str->str; | ||
| 293 | } | ||
| 294 | |||
| 295 | String dump_filetypes(const PointerArray *filetypes) | ||
| 296 | { | ||
| 297 | static const char flags[][4] = { | ||
| 298 | [FT_EXTENSION] = "", | ||
| 299 | [FT_FILENAME] = "-f ", | ||
| 300 | [FT_CONTENT] = "-c ", | ||
| 301 | [FT_INTERPRETER] = "-i ", | ||
| 302 | [FT_BASENAME] = "-b ", | ||
| 303 | }; | ||
| 304 | |||
| 305 | String s = string_new(4096); | ||
| 306 | for (size_t i = 0, n = filetypes->count; i < n; i++) { | ||
| 307 | const UserFileTypeEntry *ft = filetypes->ptrs[i]; | ||
| 308 | BUG_ON(ft->type >= ARRAYLEN(flags)); | ||
| 309 | BUG_ON(ft->name[0] == '-'); | ||
| 310 | string_append_literal(&s, "ft "); | ||
| 311 | string_append_cstring(&s, flags[ft->type]); | ||
| 312 | string_append_escaped_arg(&s, ft->name, true); | ||
| 313 | string_append_byte(&s, ' '); | ||
| 314 | string_append_escaped_arg(&s, ft_get_str(ft), true); | ||
| 315 | string_append_byte(&s, '\n'); | ||
| 316 | } | ||
| 317 | return s; | ||
| 318 | } | ||
| 319 | |||
| 320 | static void free_filetype_entry(UserFileTypeEntry *ft) | ||
| 321 | { | ||
| 322 | if (ft_uses_regex(ft->type)) { | ||
| 323 | free_cached_regexp(ft->u.regexp); | ||
| 324 | } else { | ||
| 325 | free(ft->u.str); | ||
| 326 | } | ||
| 327 | free(ft); | ||
| 328 | } | ||
| 329 | |||
| 330 | void free_filetypes(PointerArray *filetypes) | ||
| 331 | { | ||
| 332 | ptr_array_free_cb(filetypes, FREE_FUNC(free_filetype_entry)); | ||
| 333 | } | ||
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 @@ | |||
| 1 | #ifndef FILETYPE_H | ||
| 2 | #define FILETYPE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <string.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/ptr-array.h" | ||
| 8 | #include "util/string-view.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | |||
| 11 | // Note: the order of these values changes the order of iteration | ||
| 12 | // in find_ft() | ||
| 13 | typedef enum { | ||
| 14 | FT_INTERPRETER, | ||
| 15 | FT_BASENAME, | ||
| 16 | FT_CONTENT, | ||
| 17 | FT_EXTENSION, | ||
| 18 | FT_FILENAME, | ||
| 19 | } FileDetectionType; | ||
| 20 | |||
| 21 | PURE | ||
| 22 | static inline bool is_valid_filetype_name(const char *name) | ||
| 23 | { | ||
| 24 | size_t n = strcspn(name, " \t/"); | ||
| 25 | return n > 0 && n < 64 && name[n] == '\0' && name[0] != '-'; | ||
| 26 | } | ||
| 27 | |||
| 28 | bool add_filetype(PointerArray *filetypes, const char *name, const char *str, FileDetectionType type) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 29 | bool is_ft(const PointerArray *filetypes, const char *name); | ||
| 30 | const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line); | ||
| 31 | void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix); | ||
| 32 | String dump_filetypes(const PointerArray *filetypes); | ||
| 33 | void free_filetypes(PointerArray *filetypes); | ||
| 34 | |||
| 35 | #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 @@ | |||
| 1 | #include "frame.h" | ||
| 2 | #include "editor.h" | ||
| 3 | #include "util/debug.h" | ||
| 4 | #include "util/xmalloc.h" | ||
| 5 | #include "window.h" | ||
| 6 | |||
| 7 | enum { | ||
| 8 | WINDOW_MIN_WIDTH = 8, | ||
| 9 | WINDOW_MIN_HEIGHT = 3, | ||
| 10 | }; | ||
| 11 | |||
| 12 | static void sanity_check_frame(const Frame *frame) | ||
| 13 | { | ||
| 14 | bool has_window = !!frame->window; | ||
| 15 | bool has_frames = frame->frames.count > 0; | ||
| 16 | if (has_window == has_frames) { | ||
| 17 | BUG("frames must contain a window or subframe(s), but never both"); | ||
| 18 | } | ||
| 19 | BUG_ON(has_window && frame != frame->window->frame); | ||
| 20 | } | ||
| 21 | |||
| 22 | static int get_min_w(const Frame *frame) | ||
| 23 | { | ||
| 24 | if (frame->window) { | ||
| 25 | return WINDOW_MIN_WIDTH; | ||
| 26 | } | ||
| 27 | |||
| 28 | const PointerArray *subframes = &frame->frames; | ||
| 29 | const size_t count = subframes->count; | ||
| 30 | if (!frame->vertical) { | ||
| 31 | int w = count - 1; // Separators | ||
| 32 | for (size_t i = 0; i < count; i++) { | ||
| 33 | w += get_min_w(subframes->ptrs[i]); | ||
| 34 | } | ||
| 35 | return w; | ||
| 36 | } | ||
| 37 | |||
| 38 | int max = 0; | ||
| 39 | for (size_t i = 0; i < count; i++) { | ||
| 40 | int w = get_min_w(subframes->ptrs[i]); | ||
| 41 | max = MAX(w, max); | ||
| 42 | } | ||
| 43 | return max; | ||
| 44 | } | ||
| 45 | |||
| 46 | static int get_min_h(const Frame *frame) | ||
| 47 | { | ||
| 48 | if (frame->window) { | ||
| 49 | return WINDOW_MIN_HEIGHT; | ||
| 50 | } | ||
| 51 | |||
| 52 | const PointerArray *subframes = &frame->frames; | ||
| 53 | const size_t count = subframes->count; | ||
| 54 | if (frame->vertical) { | ||
| 55 | int h = 0; | ||
| 56 | for (size_t i = 0; i < count; i++) { | ||
| 57 | h += get_min_h(subframes->ptrs[i]); | ||
| 58 | } | ||
| 59 | return h; | ||
| 60 | } | ||
| 61 | |||
| 62 | int max = 0; | ||
| 63 | for (size_t i = 0; i < count; i++) { | ||
| 64 | int h = get_min_h(subframes->ptrs[i]); | ||
| 65 | max = MAX(h, max); | ||
| 66 | } | ||
| 67 | return max; | ||
| 68 | } | ||
| 69 | |||
| 70 | static int get_min(const Frame *frame) | ||
| 71 | { | ||
| 72 | return frame->parent->vertical ? get_min_h(frame) : get_min_w(frame); | ||
| 73 | } | ||
| 74 | |||
| 75 | static int get_size(const Frame *frame) | ||
| 76 | { | ||
| 77 | return frame->parent->vertical ? frame->h : frame->w; | ||
| 78 | } | ||
| 79 | |||
| 80 | static int get_container_size(const Frame *frame) | ||
| 81 | { | ||
| 82 | return frame->vertical ? frame->h : frame->w; | ||
| 83 | } | ||
| 84 | |||
| 85 | static void set_size(Frame *frame, int size) | ||
| 86 | { | ||
| 87 | bool vertical = frame->parent->vertical; | ||
| 88 | int w = vertical ? frame->parent->w : size; | ||
| 89 | int h = vertical ? size : frame->parent->h; | ||
| 90 | set_frame_size(frame, w, h); | ||
| 91 | } | ||
| 92 | |||
| 93 | static void divide_equally(const Frame *frame) | ||
| 94 | { | ||
| 95 | size_t count = frame->frames.count; | ||
| 96 | BUG_ON(count == 0); | ||
| 97 | |||
| 98 | int *min = xnew(int, count); | ||
| 99 | for (size_t i = 0; i < count; i++) { | ||
| 100 | min[i] = get_min(frame->frames.ptrs[i]); | ||
| 101 | } | ||
| 102 | |||
| 103 | int *size = xnew0(int, count); | ||
| 104 | int s = get_container_size(frame); | ||
| 105 | int q, r, used; | ||
| 106 | size_t n = count; | ||
| 107 | |||
| 108 | // Consume q and r as equally as possible | ||
| 109 | do { | ||
| 110 | used = 0; | ||
| 111 | q = s / n; | ||
| 112 | r = s % n; | ||
| 113 | for (size_t i = 0; i < count; i++) { | ||
| 114 | if (size[i] == 0 && min[i] > q) { | ||
| 115 | size[i] = min[i]; | ||
| 116 | used += min[i]; | ||
| 117 | n--; | ||
| 118 | } | ||
| 119 | } | ||
| 120 | s -= used; | ||
| 121 | } while (used && n > 0); | ||
| 122 | |||
| 123 | for (size_t i = 0; i < count; i++) { | ||
| 124 | Frame *c = frame->frames.ptrs[i]; | ||
| 125 | if (size[i] == 0) { | ||
| 126 | size[i] = q + (r-- > 0); | ||
| 127 | } | ||
| 128 | set_size(c, size[i]); | ||
| 129 | } | ||
| 130 | |||
| 131 | free(size); | ||
| 132 | free(min); | ||
| 133 | } | ||
| 134 | |||
| 135 | static void fix_size(const Frame *frame) | ||
| 136 | { | ||
| 137 | size_t count = frame->frames.count; | ||
| 138 | int *size = xnew(int, count); | ||
| 139 | int *min = xnew(int, count); | ||
| 140 | int total = 0; | ||
| 141 | for (size_t i = 0; i < count; i++) { | ||
| 142 | const Frame *c = frame->frames.ptrs[i]; | ||
| 143 | min[i] = get_min(c); | ||
| 144 | size[i] = MAX(get_size(c), min[i]); | ||
| 145 | total += size[i]; | ||
| 146 | } | ||
| 147 | |||
| 148 | int s = get_container_size(frame); | ||
| 149 | if (total > s) { | ||
| 150 | int n = total - s; | ||
| 151 | for (ssize_t i = count - 1; n > 0 && i >= 0; i--) { | ||
| 152 | int new_size = MAX(size[i] - n, min[i]); | ||
| 153 | n -= size[i] - new_size; | ||
| 154 | size[i] = new_size; | ||
| 155 | } | ||
| 156 | } else { | ||
| 157 | size[count - 1] += s - total; | ||
| 158 | } | ||
| 159 | |||
| 160 | for (size_t i = 0; i < count; i++) { | ||
| 161 | set_size(frame->frames.ptrs[i], size[i]); | ||
| 162 | } | ||
| 163 | |||
| 164 | free(size); | ||
| 165 | free(min); | ||
| 166 | } | ||
| 167 | |||
| 168 | static void add_to_sibling_size(Frame *frame, int count) | ||
| 169 | { | ||
| 170 | const Frame *parent = frame->parent; | ||
| 171 | size_t idx = ptr_array_idx(&parent->frames, frame); | ||
| 172 | BUG_ON(idx >= parent->frames.count); | ||
| 173 | if (idx == parent->frames.count - 1) { | ||
| 174 | frame = parent->frames.ptrs[idx - 1]; | ||
| 175 | } else { | ||
| 176 | frame = parent->frames.ptrs[idx + 1]; | ||
| 177 | } | ||
| 178 | set_size(frame, get_size(frame) + count); | ||
| 179 | } | ||
| 180 | |||
| 181 | static int sub(Frame *frame, int count) | ||
| 182 | { | ||
| 183 | int min = get_min(frame); | ||
| 184 | int old = get_size(frame); | ||
| 185 | int new = MAX(min, old - count); | ||
| 186 | if (new != old) { | ||
| 187 | set_size(frame, new); | ||
| 188 | } | ||
| 189 | return count - (old - new); | ||
| 190 | } | ||
| 191 | |||
| 192 | static void subtract_from_sibling_size(const Frame *frame, int count) | ||
| 193 | { | ||
| 194 | const Frame *parent = frame->parent; | ||
| 195 | size_t idx = ptr_array_idx(&parent->frames, frame); | ||
| 196 | BUG_ON(idx >= parent->frames.count); | ||
| 197 | |||
| 198 | for (size_t i = idx + 1, n = parent->frames.count; i < n; i++) { | ||
| 199 | count = sub(parent->frames.ptrs[i], count); | ||
| 200 | if (count == 0) { | ||
| 201 | return; | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | for (size_t i = idx; i > 0; i--) { | ||
| 206 | count = sub(parent->frames.ptrs[i - 1], count); | ||
| 207 | if (count == 0) { | ||
| 208 | return; | ||
| 209 | } | ||
| 210 | } | ||
| 211 | } | ||
| 212 | |||
| 213 | static void resize_to(Frame *frame, int size) | ||
| 214 | { | ||
| 215 | const Frame *parent = frame->parent; | ||
| 216 | int total = parent->vertical ? parent->h : parent->w; | ||
| 217 | int count = parent->frames.count; | ||
| 218 | int min = get_min(frame); | ||
| 219 | int max = total - (count - 1) * min; | ||
| 220 | max = MAX(min, max); | ||
| 221 | size = CLAMP(size, min, max); | ||
| 222 | |||
| 223 | int change = size - get_size(frame); | ||
| 224 | if (change == 0) { | ||
| 225 | return; | ||
| 226 | } | ||
| 227 | |||
| 228 | set_size(frame, size); | ||
| 229 | if (change < 0) { | ||
| 230 | add_to_sibling_size(frame, -change); | ||
| 231 | } else { | ||
| 232 | subtract_from_sibling_size(frame, change); | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | static bool rightmost_frame(const Frame *frame) | ||
| 237 | { | ||
| 238 | const Frame *parent = frame->parent; | ||
| 239 | if (!parent) { | ||
| 240 | return true; | ||
| 241 | } | ||
| 242 | if (!parent->vertical) { | ||
| 243 | if (frame != parent->frames.ptrs[parent->frames.count - 1]) { | ||
| 244 | return false; | ||
| 245 | } | ||
| 246 | } | ||
| 247 | return rightmost_frame(parent); | ||
| 248 | } | ||
| 249 | |||
| 250 | static Frame *new_frame(void) | ||
| 251 | { | ||
| 252 | Frame *frame = xnew0(Frame, 1); | ||
| 253 | frame->equal_size = true; | ||
| 254 | return frame; | ||
| 255 | } | ||
| 256 | |||
| 257 | static Frame *add_frame(Frame *parent, Window *window, size_t idx) | ||
| 258 | { | ||
| 259 | Frame *frame = new_frame(); | ||
| 260 | frame->parent = parent; | ||
| 261 | frame->window = window; | ||
| 262 | window->frame = frame; | ||
| 263 | if (parent) { | ||
| 264 | BUG_ON(idx > parent->frames.count); | ||
| 265 | ptr_array_insert(&parent->frames, frame, idx); | ||
| 266 | parent->window = NULL; | ||
| 267 | } | ||
| 268 | return frame; | ||
| 269 | } | ||
| 270 | |||
| 271 | Frame *new_root_frame(Window *window) | ||
| 272 | { | ||
| 273 | return add_frame(NULL, window, 0); | ||
| 274 | } | ||
| 275 | |||
| 276 | static Frame *find_resizable(Frame *frame, ResizeDirection dir) | ||
| 277 | { | ||
| 278 | if (dir == RESIZE_DIRECTION_AUTO) { | ||
| 279 | return frame; | ||
| 280 | } | ||
| 281 | |||
| 282 | while (frame->parent) { | ||
| 283 | if (dir == RESIZE_DIRECTION_VERTICAL && frame->parent->vertical) { | ||
| 284 | return frame; | ||
| 285 | } | ||
| 286 | if (dir == RESIZE_DIRECTION_HORIZONTAL && !frame->parent->vertical) { | ||
| 287 | return frame; | ||
| 288 | } | ||
| 289 | frame = frame->parent; | ||
| 290 | } | ||
| 291 | return NULL; | ||
| 292 | } | ||
| 293 | |||
| 294 | void set_frame_size(Frame *frame, int w, int h) | ||
| 295 | { | ||
| 296 | int min_w = get_min_w(frame); | ||
| 297 | int min_h = get_min_h(frame); | ||
| 298 | w = MAX(w, min_w); | ||
| 299 | h = MAX(h, min_h); | ||
| 300 | frame->w = w; | ||
| 301 | frame->h = h; | ||
| 302 | |||
| 303 | if (frame->window) { | ||
| 304 | w -= rightmost_frame(frame) ? 0 : 1; // Separator | ||
| 305 | set_window_size(frame->window, w, h); | ||
| 306 | return; | ||
| 307 | } | ||
| 308 | |||
| 309 | if (frame->equal_size) { | ||
| 310 | divide_equally(frame); | ||
| 311 | } else { | ||
| 312 | fix_size(frame); | ||
| 313 | } | ||
| 314 | } | ||
| 315 | |||
| 316 | void equalize_frame_sizes(Frame *parent) | ||
| 317 | { | ||
| 318 | parent->equal_size = true; | ||
| 319 | divide_equally(parent); | ||
| 320 | update_window_coordinates(parent); | ||
| 321 | } | ||
| 322 | |||
| 323 | void resize_frame(Frame *frame, ResizeDirection dir, int size) | ||
| 324 | { | ||
| 325 | frame = find_resizable(frame, dir); | ||
| 326 | if (!frame) { | ||
| 327 | return; | ||
| 328 | } | ||
| 329 | |||
| 330 | Frame *parent = frame->parent; | ||
| 331 | parent->equal_size = false; | ||
| 332 | resize_to(frame, size); | ||
| 333 | update_window_coordinates(parent); | ||
| 334 | } | ||
| 335 | |||
| 336 | void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount) | ||
| 337 | { | ||
| 338 | resize_frame(frame, dir, get_size(frame) + amount); | ||
| 339 | } | ||
| 340 | |||
| 341 | static void update_frame_coordinates(const Frame *frame, int x, int y) | ||
| 342 | { | ||
| 343 | if (frame->window) { | ||
| 344 | set_window_coordinates(frame->window, x, y); | ||
| 345 | return; | ||
| 346 | } | ||
| 347 | |||
| 348 | for (size_t i = 0, n = frame->frames.count; i < n; i++) { | ||
| 349 | const Frame *c = frame->frames.ptrs[i]; | ||
| 350 | update_frame_coordinates(c, x, y); | ||
| 351 | if (frame->vertical) { | ||
| 352 | y += c->h; | ||
| 353 | } else { | ||
| 354 | x += c->w; | ||
| 355 | } | ||
| 356 | } | ||
| 357 | } | ||
| 358 | |||
| 359 | static Frame *get_root_frame(Frame *frame) | ||
| 360 | { | ||
| 361 | BUG_ON(!frame); | ||
| 362 | while (frame->parent) { | ||
| 363 | frame = frame->parent; | ||
| 364 | } | ||
| 365 | return frame; | ||
| 366 | } | ||
| 367 | |||
| 368 | void update_window_coordinates(Frame *frame) | ||
| 369 | { | ||
| 370 | update_frame_coordinates(get_root_frame(frame), 0, 0); | ||
| 371 | } | ||
| 372 | |||
| 373 | Frame *split_frame(Window *window, bool vertical, bool before) | ||
| 374 | { | ||
| 375 | Frame *frame = window->frame; | ||
| 376 | Frame *parent = frame->parent; | ||
| 377 | if (!parent || parent->vertical != vertical) { | ||
| 378 | // Reparent window | ||
| 379 | frame->vertical = vertical; | ||
| 380 | add_frame(frame, window, 0); | ||
| 381 | parent = frame; | ||
| 382 | } | ||
| 383 | |||
| 384 | size_t idx = ptr_array_idx(&parent->frames, window->frame); | ||
| 385 | BUG_ON(idx >= parent->frames.count); | ||
| 386 | idx += before ? 0 : 1; | ||
| 387 | frame = add_frame(parent, new_window(window->editor), idx); | ||
| 388 | parent->equal_size = true; | ||
| 389 | |||
| 390 | // Recalculate | ||
| 391 | set_frame_size(parent, parent->w, parent->h); | ||
| 392 | update_window_coordinates(parent); | ||
| 393 | return frame; | ||
| 394 | } | ||
| 395 | |||
| 396 | // Doesn't really split root but adds new frame between root and its contents | ||
| 397 | Frame *split_root_frame(EditorState *e, bool vertical, bool before) | ||
| 398 | { | ||
| 399 | Frame *old_root = e->root_frame; | ||
| 400 | Frame *new_root = new_frame(); | ||
| 401 | ptr_array_append(&new_root->frames, old_root); | ||
| 402 | old_root->parent = new_root; | ||
| 403 | new_root->vertical = vertical; | ||
| 404 | e->root_frame = new_root; | ||
| 405 | |||
| 406 | Frame *frame = add_frame(new_root, new_window(e), before ? 0 : 1); | ||
| 407 | set_frame_size(new_root, old_root->w, old_root->h); | ||
| 408 | update_window_coordinates(new_root); | ||
| 409 | return frame; | ||
| 410 | } | ||
| 411 | |||
| 412 | // NOTE: does not remove frame from frame->parent->frames | ||
| 413 | static void free_frame(Frame *frame) | ||
| 414 | { | ||
| 415 | frame->parent = NULL; | ||
| 416 | ptr_array_free_cb(&frame->frames, FREE_FUNC(free_frame)); | ||
| 417 | |||
| 418 | if (frame->window) { | ||
| 419 | window_free(frame->window); | ||
| 420 | frame->window = NULL; | ||
| 421 | } | ||
| 422 | |||
| 423 | free(frame); | ||
| 424 | } | ||
| 425 | |||
| 426 | void remove_frame(EditorState *e, Frame *frame) | ||
| 427 | { | ||
| 428 | Frame *parent = frame->parent; | ||
| 429 | if (!parent) { | ||
| 430 | free_frame(frame); | ||
| 431 | return; | ||
| 432 | } | ||
| 433 | |||
| 434 | ptr_array_remove(&parent->frames, frame); | ||
| 435 | free_frame(frame); | ||
| 436 | |||
| 437 | if (parent->frames.count == 1) { | ||
| 438 | // Replace parent with the only child frame | ||
| 439 | Frame *gp = parent->parent; | ||
| 440 | Frame *c = parent->frames.ptrs[0]; | ||
| 441 | c->parent = gp; | ||
| 442 | c->w = parent->w; | ||
| 443 | c->h = parent->h; | ||
| 444 | if (gp) { | ||
| 445 | size_t idx = ptr_array_idx(&gp->frames, parent); | ||
| 446 | BUG_ON(idx >= gp->frames.count); | ||
| 447 | gp->frames.ptrs[idx] = c; | ||
| 448 | } else { | ||
| 449 | e->root_frame = c; | ||
| 450 | } | ||
| 451 | free(parent->frames.ptrs); | ||
| 452 | free(parent); | ||
| 453 | parent = c; | ||
| 454 | } | ||
| 455 | |||
| 456 | // Recalculate | ||
| 457 | set_frame_size(parent, parent->w, parent->h); | ||
| 458 | update_window_coordinates(parent); | ||
| 459 | } | ||
| 460 | |||
| 461 | void dump_frame(const Frame *frame, size_t level, String *str) | ||
| 462 | { | ||
| 463 | sanity_check_frame(frame); | ||
| 464 | string_append_memset(str, ' ', level * 4); | ||
| 465 | string_sprintf(str, "%dx%d", frame->w, frame->h); | ||
| 466 | |||
| 467 | const Window *win = frame->window; | ||
| 468 | if (win) { | ||
| 469 | string_append_byte(str, '\n'); | ||
| 470 | string_append_memset(str, ' ', (level + 1) * 4); | ||
| 471 | string_sprintf(str, "%d,%d %dx%d ", win->x, win->y, win->w, win->h); | ||
| 472 | string_append_cstring(str, buffer_filename(win->view->buffer)); | ||
| 473 | string_append_byte(str, '\n'); | ||
| 474 | return; | ||
| 475 | } | ||
| 476 | |||
| 477 | string_append_cstring(str, frame->vertical ? " V" : " H"); | ||
| 478 | string_append_cstring(str, frame->equal_size ? "\n" : " !\n"); | ||
| 479 | |||
| 480 | for (size_t i = 0, n = frame->frames.count; i < n; i++) { | ||
| 481 | const Frame *c = frame->frames.ptrs[i]; | ||
| 482 | dump_frame(c, level + 1, str); | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | #if DEBUG >= 1 | ||
| 487 | void debug_frame(const Frame *frame) | ||
| 488 | { | ||
| 489 | sanity_check_frame(frame); | ||
| 490 | for (size_t i = 0, n = frame->frames.count; i < n; i++) { | ||
| 491 | const Frame *c = frame->frames.ptrs[i]; | ||
| 492 | BUG_ON(c->parent != frame); | ||
| 493 | debug_frame(c); | ||
| 494 | } | ||
| 495 | } | ||
| 496 | #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 @@ | |||
| 1 | #ifndef FRAME_H | ||
| 2 | #define FRAME_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/ptr-array.h" | ||
| 8 | #include "util/string.h" | ||
| 9 | |||
| 10 | // A container for other Frames or Windows. Frames and Windows form | ||
| 11 | // a tree structure, wherein Windows are the terminal (leaf) nodes. | ||
| 12 | typedef struct Frame { | ||
| 13 | struct Frame *parent; | ||
| 14 | // Every frame contains either one window or multiple subframes | ||
| 15 | PointerArray frames; | ||
| 16 | struct Window *window; | ||
| 17 | int w; // Width | ||
| 18 | int h; // Height | ||
| 19 | bool vertical; | ||
| 20 | bool equal_size; | ||
| 21 | } Frame; | ||
| 22 | |||
| 23 | typedef enum { | ||
| 24 | RESIZE_DIRECTION_AUTO, | ||
| 25 | RESIZE_DIRECTION_HORIZONTAL, | ||
| 26 | RESIZE_DIRECTION_VERTICAL, | ||
| 27 | } ResizeDirection; | ||
| 28 | |||
| 29 | struct EditorState; | ||
| 30 | |||
| 31 | Frame *new_root_frame(struct Window *window); | ||
| 32 | void set_frame_size(Frame *frame, int w, int h); | ||
| 33 | void equalize_frame_sizes(Frame *parent); | ||
| 34 | void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount); | ||
| 35 | void resize_frame(Frame *frame, ResizeDirection dir, int size); | ||
| 36 | void update_window_coordinates(Frame *frame); | ||
| 37 | Frame *split_frame(struct Window *window, bool vertical, bool before); | ||
| 38 | Frame *split_root_frame(struct EditorState *e, bool vertical, bool before); | ||
| 39 | void remove_frame(struct EditorState *e, Frame *frame); | ||
| 40 | void dump_frame(const Frame *frame, size_t level, String *str); | ||
| 41 | |||
| 42 | #if DEBUG >= 1 | ||
| 43 | void debug_frame(const Frame *frame); | ||
| 44 | #else | ||
| 45 | static inline void debug_frame(const Frame* UNUSED_ARG(frame)) {} | ||
| 46 | #endif | ||
| 47 | |||
| 48 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "history.h" | ||
| 5 | #include "error.h" | ||
| 6 | #include "util/debug.h" | ||
| 7 | #include "util/readfile.h" | ||
| 8 | #include "util/str-util.h" | ||
| 9 | #include "util/xmalloc.h" | ||
| 10 | #include "util/xstdio.h" | ||
| 11 | |||
| 12 | void history_add(History *history, const char *text) | ||
| 13 | { | ||
| 14 | BUG_ON(history->max_entries < 2); | ||
| 15 | if (text[0] == '\0') { | ||
| 16 | return; | ||
| 17 | } | ||
| 18 | |||
| 19 | HashMap *map = &history->entries; | ||
| 20 | HistoryEntry *e = hashmap_get(map, text); | ||
| 21 | |||
| 22 | if (e) { | ||
| 23 | if (e == history->last) { | ||
| 24 | // Existing entry already at end of list; nothing more to do | ||
| 25 | return; | ||
| 26 | } | ||
| 27 | // Remove existing entry from list | ||
| 28 | e->next->prev = e->prev; | ||
| 29 | if (unlikely(e == history->first)) { | ||
| 30 | history->first = e->next; | ||
| 31 | } else { | ||
| 32 | e->prev->next = e->next; | ||
| 33 | } | ||
| 34 | } else { | ||
| 35 | if (map->count == history->max_entries) { | ||
| 36 | // History is full; recycle oldest entry | ||
| 37 | HistoryEntry *old_first = history->first; | ||
| 38 | HistoryEntry *new_first = old_first->next; | ||
| 39 | new_first->prev = NULL; | ||
| 40 | history->first = new_first; | ||
| 41 | e = hashmap_remove(map, old_first->text); | ||
| 42 | BUG_ON(e != old_first); | ||
| 43 | } else { | ||
| 44 | e = xnew(HistoryEntry, 1); | ||
| 45 | } | ||
| 46 | e->text = xstrdup(text); | ||
| 47 | hashmap_insert(map, e->text, e); | ||
| 48 | } | ||
| 49 | |||
| 50 | // Insert entry at end of list | ||
| 51 | HistoryEntry *old_last = history->last; | ||
| 52 | e->next = NULL; | ||
| 53 | e->prev = old_last; | ||
| 54 | history->last = e; | ||
| 55 | if (likely(old_last)) { | ||
| 56 | old_last->next = e; | ||
| 57 | } else { | ||
| 58 | history->first = e; | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | bool history_search_forward ( | ||
| 63 | const History *history, | ||
| 64 | const HistoryEntry **pos, | ||
| 65 | const char *text | ||
| 66 | ) { | ||
| 67 | const HistoryEntry *start = *pos ? (*pos)->prev : history->last; | ||
| 68 | for (const HistoryEntry *e = start; e; e = e->prev) { | ||
| 69 | if (str_has_prefix(e->text, text)) { | ||
| 70 | *pos = e; | ||
| 71 | return true; | ||
| 72 | } | ||
| 73 | } | ||
| 74 | return false; | ||
| 75 | } | ||
| 76 | |||
| 77 | bool history_search_backward ( | ||
| 78 | const History *history, | ||
| 79 | const HistoryEntry **pos, | ||
| 80 | const char *text | ||
| 81 | ) { | ||
| 82 | const HistoryEntry *start = *pos ? (*pos)->next : history->first; | ||
| 83 | for (const HistoryEntry *e = start; e; e = e->next) { | ||
| 84 | if (str_has_prefix(e->text, text)) { | ||
| 85 | *pos = e; | ||
| 86 | return true; | ||
| 87 | } | ||
| 88 | } | ||
| 89 | return false; | ||
| 90 | } | ||
| 91 | |||
| 92 | void history_load(History *history, char *filename) | ||
| 93 | { | ||
| 94 | BUG_ON(!history); | ||
| 95 | BUG_ON(!filename); | ||
| 96 | BUG_ON(history->filename); | ||
| 97 | BUG_ON(history->max_entries < 2); | ||
| 98 | |||
| 99 | hashmap_init(&history->entries, history->max_entries); | ||
| 100 | history->filename = filename; | ||
| 101 | |||
| 102 | char *buf; | ||
| 103 | const ssize_t ssize = read_file(filename, &buf); | ||
| 104 | if (ssize < 0) { | ||
| 105 | if (errno != ENOENT) { | ||
| 106 | error_msg("Error reading %s: %s", filename, strerror(errno)); | ||
| 107 | } | ||
| 108 | return; | ||
| 109 | } | ||
| 110 | |||
| 111 | for (size_t pos = 0, size = ssize; pos < size; ) { | ||
| 112 | history_add(history, buf_next_line(buf, &pos, size)); | ||
| 113 | } | ||
| 114 | |||
| 115 | free(buf); | ||
| 116 | } | ||
| 117 | |||
| 118 | void history_save(const History *history) | ||
| 119 | { | ||
| 120 | const char *filename = history->filename; | ||
| 121 | if (!filename) { | ||
| 122 | return; | ||
| 123 | } | ||
| 124 | |||
| 125 | FILE *f = xfopen(filename, "w", O_CLOEXEC, 0666); | ||
| 126 | if (!f) { | ||
| 127 | error_msg("Error creating %s: %s", filename, strerror(errno)); | ||
| 128 | return; | ||
| 129 | } | ||
| 130 | |||
| 131 | for (const HistoryEntry *e = history->first; e; e = e->next) { | ||
| 132 | xfputs(e->text, f); | ||
| 133 | xfputc('\n', f); | ||
| 134 | } | ||
| 135 | |||
| 136 | fclose(f); | ||
| 137 | } | ||
| 138 | |||
| 139 | void history_free(History *history) | ||
| 140 | { | ||
| 141 | hashmap_free(&history->entries, free); | ||
| 142 | free(history->filename); | ||
| 143 | history->filename = NULL; | ||
| 144 | history->first = NULL; | ||
| 145 | history->last = NULL; | ||
| 146 | } | ||
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 @@ | |||
| 1 | #ifndef HISTORY_H | ||
| 2 | #define HISTORY_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/hashmap.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | |||
| 9 | typedef struct HistoryEntry { | ||
| 10 | struct HistoryEntry *next; | ||
| 11 | struct HistoryEntry *prev; | ||
| 12 | char *text; | ||
| 13 | } HistoryEntry; | ||
| 14 | |||
| 15 | // This is a HashMap with a doubly-linked list running through the | ||
| 16 | // entries, in a way similar to the Java LinkedHashMap class. The | ||
| 17 | // HashMap allows duplicates to be found and re-inserted at the end | ||
| 18 | // of the list in O(1) time and the doubly-linked entries allow | ||
| 19 | // ordered traversal. | ||
| 20 | typedef struct { | ||
| 21 | char *filename; | ||
| 22 | HashMap entries; | ||
| 23 | HistoryEntry *first; | ||
| 24 | HistoryEntry *last; | ||
| 25 | size_t max_entries; | ||
| 26 | } History; | ||
| 27 | |||
| 28 | void history_add(History *history, const char *text); | ||
| 29 | bool history_search_forward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT; | ||
| 30 | bool history_search_backward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT; | ||
| 31 | void history_load(History *history, char *filename); | ||
| 32 | void history_save(const History *history); | ||
| 33 | void history_free(History *history); | ||
| 34 | |||
| 35 | #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 @@ | |||
| 1 | #include <sys/types.h> | ||
| 2 | #include "indent.h" | ||
| 3 | #include "regexp.h" | ||
| 4 | #include "util/xmalloc.h" | ||
| 5 | |||
| 6 | char *make_indent(const LocalOptions *options, size_t width) | ||
| 7 | { | ||
| 8 | if (width == 0) { | ||
| 9 | return NULL; | ||
| 10 | } | ||
| 11 | |||
| 12 | if (use_spaces_for_indent(options)) { | ||
| 13 | char *str = xmalloc(width + 1); | ||
| 14 | str[width] = '\0'; | ||
| 15 | return memset(str, ' ', width); | ||
| 16 | } | ||
| 17 | |||
| 18 | size_t tw = options->tab_width; | ||
| 19 | size_t ntabs = indent_level(width, tw); | ||
| 20 | size_t nspaces = indent_remainder(width, tw); | ||
| 21 | size_t n = ntabs + nspaces; | ||
| 22 | char *str = xmalloc(n + 1); | ||
| 23 | memset(str + ntabs, ' ', nspaces); | ||
| 24 | str[n] = '\0'; | ||
| 25 | return memset(str, '\t', ntabs); | ||
| 26 | } | ||
| 27 | |||
| 28 | static bool indent_inc(const LocalOptions *options, const StringView *line) | ||
| 29 | { | ||
| 30 | static regex_t re1, re2; | ||
| 31 | static bool compiled; | ||
| 32 | if (!compiled) { | ||
| 33 | // TODO: Make these patterns configurable via a local option | ||
| 34 | static const char pat1[] = "\\{[\t ]*(//.*|/\\*.*\\*/[\t ]*)?$"; | ||
| 35 | static const char pat2[] = "\\}[\t ]*(//.*|/\\*.*\\*/[\t ]*)?$"; | ||
| 36 | regexp_compile_or_fatal_error(&re1, pat1, REG_NEWLINE | REG_NOSUB); | ||
| 37 | regexp_compile_or_fatal_error(&re2, pat2, REG_NEWLINE | REG_NOSUB); | ||
| 38 | compiled = true; | ||
| 39 | } | ||
| 40 | |||
| 41 | if (options->brace_indent) { | ||
| 42 | regmatch_t m; | ||
| 43 | if (regexp_exec(&re1, line->data, line->length, 0, &m, 0)) { | ||
| 44 | return true; | ||
| 45 | } | ||
| 46 | if (regexp_exec(&re2, line->data, line->length, 0, &m, 0)) { | ||
| 47 | return false; | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | const InternedRegexp *ir = options->indent_regex; | ||
| 52 | if (!ir) { | ||
| 53 | return false; | ||
| 54 | } | ||
| 55 | |||
| 56 | BUG_ON(ir->str[0] == '\0'); | ||
| 57 | regmatch_t m; | ||
| 58 | return regexp_exec(&ir->re, line->data, line->length, 0, &m, 0); | ||
| 59 | } | ||
| 60 | |||
| 61 | char *get_indent_for_next_line(const LocalOptions *options, const StringView *line) | ||
| 62 | { | ||
| 63 | size_t width = get_indent_width(options, line); | ||
| 64 | if (indent_inc(options, line)) { | ||
| 65 | width = next_indent_width(width, options->indent_width); | ||
| 66 | } | ||
| 67 | return make_indent(options, width); | ||
| 68 | } | ||
| 69 | |||
| 70 | IndentInfo get_indent_info(const LocalOptions *options, const StringView *line) | ||
| 71 | { | ||
| 72 | const char *buf = line->data; | ||
| 73 | const size_t len = line->length; | ||
| 74 | const size_t tw = options->tab_width; | ||
| 75 | const size_t iw = options->indent_width; | ||
| 76 | const bool space_indent = use_spaces_for_indent(options); | ||
| 77 | IndentInfo info = {.sane = true}; | ||
| 78 | size_t spaces = 0; | ||
| 79 | size_t tabs = 0; | ||
| 80 | size_t pos = 0; | ||
| 81 | |||
| 82 | for (; pos < len; pos++) { | ||
| 83 | if (buf[pos] == ' ') { | ||
| 84 | info.width++; | ||
| 85 | spaces++; | ||
| 86 | } else if (buf[pos] == '\t') { | ||
| 87 | info.width = next_indent_width(info.width, tw); | ||
| 88 | tabs++; | ||
| 89 | } else { | ||
| 90 | break; | ||
| 91 | } | ||
| 92 | if (indent_remainder(info.width, iw) == 0 && info.sane) { | ||
| 93 | info.sane = space_indent ? !tabs : !spaces; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | info.level = indent_level(info.width, iw); | ||
| 98 | info.wsonly = (pos == len); | ||
| 99 | info.bytes = spaces + tabs; | ||
| 100 | return info; | ||
| 101 | } | ||
| 102 | |||
| 103 | size_t get_indent_width(const LocalOptions *options, const StringView *line) | ||
| 104 | { | ||
| 105 | const char *buf = line->data; | ||
| 106 | size_t width = 0; | ||
| 107 | for (size_t i = 0, n = line->length, tw = options->tab_width; i < n; i++) { | ||
| 108 | if (buf[i] == ' ') { | ||
| 109 | width++; | ||
| 110 | } else if (buf[i] == '\t') { | ||
| 111 | width = next_indent_width(width, tw); | ||
| 112 | } else { | ||
| 113 | break; | ||
| 114 | } | ||
| 115 | } | ||
| 116 | return width; | ||
| 117 | } | ||
| 118 | |||
| 119 | static ssize_t get_current_indent_bytes(const LocalOptions *options, const char *buf, size_t cursor_offset) | ||
| 120 | { | ||
| 121 | const size_t tw = options->tab_width; | ||
| 122 | const size_t iw = options->indent_width; | ||
| 123 | size_t ibytes = 0; | ||
| 124 | size_t iwidth = 0; | ||
| 125 | |||
| 126 | for (size_t i = 0; i < cursor_offset; i++) { | ||
| 127 | if (indent_remainder(iwidth, iw) == 0) { | ||
| 128 | ibytes = 0; | ||
| 129 | iwidth = 0; | ||
| 130 | } | ||
| 131 | switch (buf[i]) { | ||
| 132 | case '\t': | ||
| 133 | iwidth = next_indent_width(iwidth, tw); | ||
| 134 | break; | ||
| 135 | case ' ': | ||
| 136 | iwidth++; | ||
| 137 | break; | ||
| 138 | default: | ||
| 139 | // Cursor not at indentation | ||
| 140 | return -1; | ||
| 141 | } | ||
| 142 | ibytes++; | ||
| 143 | } | ||
| 144 | |||
| 145 | if (indent_remainder(iwidth, iw)) { | ||
| 146 | // Cursor at middle of indentation level | ||
| 147 | return -1; | ||
| 148 | } | ||
| 149 | |||
| 150 | return (ssize_t)ibytes; | ||
| 151 | } | ||
| 152 | |||
| 153 | size_t get_indent_level_bytes_left(const LocalOptions *options, BlockIter *cursor) | ||
| 154 | { | ||
| 155 | StringView line; | ||
| 156 | size_t cursor_offset = fetch_this_line(cursor, &line); | ||
| 157 | if (!cursor_offset) { | ||
| 158 | return 0; | ||
| 159 | } | ||
| 160 | ssize_t ibytes = get_current_indent_bytes(options, line.data, cursor_offset); | ||
| 161 | return (ibytes < 0) ? 0 : (size_t)ibytes; | ||
| 162 | } | ||
| 163 | |||
| 164 | size_t get_indent_level_bytes_right(const LocalOptions *options, BlockIter *cursor) | ||
| 165 | { | ||
| 166 | StringView line; | ||
| 167 | size_t cursor_offset = fetch_this_line(cursor, &line); | ||
| 168 | ssize_t ibytes = get_current_indent_bytes(options, line.data, cursor_offset); | ||
| 169 | if (ibytes < 0) { | ||
| 170 | return 0; | ||
| 171 | } | ||
| 172 | |||
| 173 | const size_t tw = options->tab_width; | ||
| 174 | const size_t iw = options->indent_width; | ||
| 175 | size_t iwidth = 0; | ||
| 176 | for (size_t i = cursor_offset, n = line.length; i < n; i++) { | ||
| 177 | switch (line.data[i]) { | ||
| 178 | case '\t': | ||
| 179 | iwidth = next_indent_width(iwidth, tw); | ||
| 180 | break; | ||
| 181 | case ' ': | ||
| 182 | iwidth++; | ||
| 183 | break; | ||
| 184 | default: | ||
| 185 | // No full indentation level at cursor position | ||
| 186 | return 0; | ||
| 187 | } | ||
| 188 | if (indent_remainder(iwidth, iw) == 0) { | ||
| 189 | return i - cursor_offset + 1; | ||
| 190 | } | ||
| 191 | } | ||
| 192 | return 0; | ||
| 193 | } | ||
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 @@ | |||
| 1 | #ifndef INDENT_H | ||
| 2 | #define INDENT_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <strings.h> | ||
| 8 | #include "block-iter.h" | ||
| 9 | #include "options.h" | ||
| 10 | #include "util/debug.h" | ||
| 11 | #include "util/macros.h" | ||
| 12 | #include "util/string-view.h" | ||
| 13 | |||
| 14 | typedef struct { | ||
| 15 | size_t bytes; // Size in bytes | ||
| 16 | size_t width; // Width in columns | ||
| 17 | size_t level; // Number of whole `indent-width` levels | ||
| 18 | bool wsonly; // Empty or whitespace-only line | ||
| 19 | |||
| 20 | // Only spaces or tabs, depending on `use_spaces_for_indent()`. | ||
| 21 | // Note that a "sane" line can contain spaces after tabs for alignment. | ||
| 22 | bool sane; | ||
| 23 | } IndentInfo; | ||
| 24 | |||
| 25 | // Divide `x` by `d`, to obtain the number of whole indent levels. | ||
| 26 | // If `d` is a power of 2, shift right by `ffs(d) - 1` instead, to | ||
| 27 | // avoid the expensive divide operation. This optimization applies | ||
| 28 | // to widths of 1, 2, 4 and 8, which covers all of the sensible ones. | ||
| 29 | static inline size_t indent_level(size_t x, size_t d) | ||
| 30 | { | ||
| 31 | BUG_ON(d - 1 > 7); | ||
| 32 | return likely(IS_POWER_OF_2(d)) ? x >> (ffs(d) - 1) : x / d; | ||
| 33 | } | ||
| 34 | |||
| 35 | static inline size_t indent_remainder(size_t x, size_t m) | ||
| 36 | { | ||
| 37 | BUG_ON(m - 1 > 7); | ||
| 38 | return likely(IS_POWER_OF_2(m)) ? x & (m - 1) : x % m; | ||
| 39 | } | ||
| 40 | |||
| 41 | static inline size_t next_indent_width(size_t x, size_t mul) | ||
| 42 | { | ||
| 43 | BUG_ON(mul - 1 > 7); | ||
| 44 | if (likely(IS_POWER_OF_2(mul))) { | ||
| 45 | size_t mask = ~(mul - 1); | ||
| 46 | return (x & mask) + mul; | ||
| 47 | } | ||
| 48 | return ((x + mul) / mul) * mul; | ||
| 49 | } | ||
| 50 | |||
| 51 | char *make_indent(const LocalOptions *options, size_t width); | ||
| 52 | char *get_indent_for_next_line(const LocalOptions *options, const StringView *line); | ||
| 53 | IndentInfo get_indent_info(const LocalOptions *options, const StringView *line); | ||
| 54 | size_t get_indent_width(const LocalOptions *options, const StringView *line); | ||
| 55 | size_t get_indent_level_bytes_left(const LocalOptions *options, BlockIter *cursor); | ||
| 56 | size_t get_indent_level_bytes_right(const LocalOptions *options, BlockIter *cursor); | ||
| 57 | |||
| 58 | #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 @@ | |||
| 1 | #include "compat.h" | ||
| 2 | #include <errno.h> | ||
| 3 | #include <stdint.h> | ||
| 4 | #include <stdio.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <sys/mman.h> | ||
| 8 | #include <sys/stat.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | #include "load-save.h" | ||
| 11 | #include "block.h" | ||
| 12 | #include "convert.h" | ||
| 13 | #include "encoding.h" | ||
| 14 | #include "error.h" | ||
| 15 | #include "util/debug.h" | ||
| 16 | #include "util/fd.h" | ||
| 17 | #include "util/list.h" | ||
| 18 | #include "util/log.h" | ||
| 19 | #include "util/path.h" | ||
| 20 | #include "util/str-util.h" | ||
| 21 | #include "util/time-util.h" | ||
| 22 | #include "util/xmalloc.h" | ||
| 23 | #include "util/xreadwrite.h" | ||
| 24 | |||
| 25 | static void add_block(Buffer *buffer, Block *blk) | ||
| 26 | { | ||
| 27 | buffer->nl += blk->nl; | ||
| 28 | list_add_before(&blk->node, &buffer->blocks); | ||
| 29 | } | ||
| 30 | |||
| 31 | static Block *add_utf8_line ( | ||
| 32 | Buffer *buffer, | ||
| 33 | Block *blk, | ||
| 34 | const unsigned char *line, | ||
| 35 | size_t len | ||
| 36 | ) { | ||
| 37 | size_t size = len + 1; | ||
| 38 | if (blk) { | ||
| 39 | size_t avail = blk->alloc - blk->size; | ||
| 40 | if (size <= avail) { | ||
| 41 | goto copy; | ||
| 42 | } | ||
| 43 | add_block(buffer, blk); | ||
| 44 | } | ||
| 45 | size = MAX(size, 8192); | ||
| 46 | blk = block_new(size); | ||
| 47 | copy: | ||
| 48 | memcpy(blk->data + blk->size, line, len); | ||
| 49 | blk->size += len; | ||
| 50 | blk->data[blk->size++] = '\n'; | ||
| 51 | blk->nl++; | ||
| 52 | return blk; | ||
| 53 | } | ||
| 54 | |||
| 55 | static bool decode_and_add_blocks(Buffer *buffer, const unsigned char *buf, size_t size, bool utf8_bom) | ||
| 56 | { | ||
| 57 | EncodingType bom_type = detect_encoding_from_bom(buf, size); | ||
| 58 | EncodingType enc_type = buffer->encoding.type; | ||
| 59 | if (enc_type == ENCODING_AUTODETECT) { | ||
| 60 | if (bom_type != UNKNOWN_ENCODING) { | ||
| 61 | BUG_ON(buffer->encoding.name); | ||
| 62 | Encoding e = encoding_from_type(bom_type); | ||
| 63 | if (conversion_supported_by_iconv(e.name, "UTF-8")) { | ||
| 64 | buffer_set_encoding(buffer, e, utf8_bom); | ||
| 65 | } else { | ||
| 66 | buffer_set_encoding(buffer, encoding_from_type(UTF8), utf8_bom); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | } | ||
| 70 | |||
| 71 | // Skip BOM only if it matches the specified file encoding | ||
| 72 | if (bom_type != UNKNOWN_ENCODING && bom_type == buffer->encoding.type) { | ||
| 73 | const ByteOrderMark *bom = get_bom_for_encoding(bom_type); | ||
| 74 | if (bom) { | ||
| 75 | const size_t bom_len = bom->len; | ||
| 76 | buf += bom_len; | ||
| 77 | size -= bom_len; | ||
| 78 | buffer->bom = true; | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | FileDecoder *dec = new_file_decoder(buffer->encoding.name, buf, size); | ||
| 83 | if (!dec) { | ||
| 84 | return false; | ||
| 85 | } | ||
| 86 | |||
| 87 | const char *line; | ||
| 88 | size_t len; | ||
| 89 | if (file_decoder_read_line(dec, &line, &len)) { | ||
| 90 | if (len && line[len - 1] == '\r') { | ||
| 91 | buffer->crlf_newlines = true; | ||
| 92 | len--; | ||
| 93 | } | ||
| 94 | Block *blk = add_utf8_line(buffer, NULL, line, len); | ||
| 95 | while (file_decoder_read_line(dec, &line, &len)) { | ||
| 96 | if (buffer->crlf_newlines && len && line[len - 1] == '\r') { | ||
| 97 | len--; | ||
| 98 | } | ||
| 99 | blk = add_utf8_line(buffer, blk, line, len); | ||
| 100 | } | ||
| 101 | if (blk) { | ||
| 102 | add_block(buffer, blk); | ||
| 103 | } | ||
| 104 | } | ||
| 105 | |||
| 106 | if (buffer->encoding.type == ENCODING_AUTODETECT) { | ||
| 107 | const char *enc = file_decoder_get_encoding(dec); | ||
| 108 | buffer_set_encoding(buffer, encoding_from_name(enc ? enc : "UTF-8"), utf8_bom); | ||
| 109 | } | ||
| 110 | |||
| 111 | free_file_decoder(dec); | ||
| 112 | return true; | ||
| 113 | } | ||
| 114 | |||
| 115 | static void fixup_blocks(Buffer *buffer) | ||
| 116 | { | ||
| 117 | if (list_empty(&buffer->blocks)) { | ||
| 118 | Block *blk = block_new(1); | ||
| 119 | list_add_before(&blk->node, &buffer->blocks); | ||
| 120 | } else { | ||
| 121 | // Incomplete lines are not allowed because they are special cases | ||
| 122 | // and cause lots of trouble | ||
| 123 | Block *blk = BLOCK(buffer->blocks.prev); | ||
| 124 | if (blk->size && blk->data[blk->size - 1] != '\n') { | ||
| 125 | if (blk->size == blk->alloc) { | ||
| 126 | blk->alloc = round_size_to_next_multiple(blk->size + 1, 64); | ||
| 127 | xrenew(blk->data, blk->alloc); | ||
| 128 | } | ||
| 129 | blk->data[blk->size++] = '\n'; | ||
| 130 | blk->nl++; | ||
| 131 | buffer->nl++; | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | static int xmadvise_sequential(void *addr, size_t len) | ||
| 137 | { | ||
| 138 | #if HAVE_POSIX_MADVISE | ||
| 139 | return posix_madvise(addr, len, POSIX_MADV_SEQUENTIAL); | ||
| 140 | #else | ||
| 141 | // "The posix_madvise() function shall have no effect on the semantics | ||
| 142 | // of access to memory in the specified range, although it may affect | ||
| 143 | // the performance of access". Ergo, doing nothing is a valid fallback. | ||
| 144 | (void)addr; | ||
| 145 | (void)len; | ||
| 146 | return 0; | ||
| 147 | #endif | ||
| 148 | } | ||
| 149 | |||
| 150 | static bool update_file_info(FileInfo *info, const struct stat *st) | ||
| 151 | { | ||
| 152 | *info = (FileInfo) { | ||
| 153 | .size = st->st_size, | ||
| 154 | .mode = st->st_mode, | ||
| 155 | .gid = st->st_gid, | ||
| 156 | .uid = st->st_uid, | ||
| 157 | .dev = st->st_dev, | ||
| 158 | .ino = st->st_ino, | ||
| 159 | .mtime = *get_stat_mtime(st), | ||
| 160 | }; | ||
| 161 | return true; | ||
| 162 | } | ||
| 163 | |||
| 164 | static bool buffer_stat(FileInfo *info, const char *filename) | ||
| 165 | { | ||
| 166 | struct stat st; | ||
| 167 | return !stat(filename, &st) && update_file_info(info, &st); | ||
| 168 | } | ||
| 169 | |||
| 170 | static bool buffer_fstat(FileInfo *info, int fd) | ||
| 171 | { | ||
| 172 | struct stat st; | ||
| 173 | return !fstat(fd, &st) && update_file_info(info, &st); | ||
| 174 | } | ||
| 175 | |||
| 176 | bool read_blocks(Buffer *buffer, int fd, bool utf8_bom) | ||
| 177 | { | ||
| 178 | const size_t map_size = 64 * 1024; | ||
| 179 | size_t size = buffer->file.size; | ||
| 180 | unsigned char *buf = NULL; | ||
| 181 | bool mapped = false; | ||
| 182 | bool ret = false; | ||
| 183 | |||
| 184 | if (size >= map_size) { | ||
| 185 | // NOTE: size must be greater than 0 | ||
| 186 | buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); | ||
| 187 | if (buf != MAP_FAILED) { | ||
| 188 | xmadvise_sequential(buf, size); | ||
| 189 | mapped = true; | ||
| 190 | goto decode; | ||
| 191 | } | ||
| 192 | buf = NULL; | ||
| 193 | } | ||
| 194 | |||
| 195 | if (likely(size > 0)) { | ||
| 196 | buf = malloc(size); | ||
| 197 | if (unlikely(!buf)) { | ||
| 198 | goto error; | ||
| 199 | } | ||
| 200 | ssize_t rc = xread_all(fd, buf, size); | ||
| 201 | if (unlikely(rc < 0)) { | ||
| 202 | goto error; | ||
| 203 | } | ||
| 204 | size = rc; | ||
| 205 | } else { | ||
| 206 | // st_size is zero for some files in /proc | ||
| 207 | size_t alloc = map_size; | ||
| 208 | BUG_ON(!IS_POWER_OF_2(alloc)); | ||
| 209 | buf = malloc(alloc); | ||
| 210 | if (unlikely(!buf)) { | ||
| 211 | goto error; | ||
| 212 | } | ||
| 213 | size_t pos = 0; | ||
| 214 | while (1) { | ||
| 215 | ssize_t rc = xread_all(fd, buf + pos, alloc - pos); | ||
| 216 | if (rc < 0) { | ||
| 217 | goto error; | ||
| 218 | } | ||
| 219 | if (rc == 0) { | ||
| 220 | break; | ||
| 221 | } | ||
| 222 | pos += rc; | ||
| 223 | if (pos == alloc) { | ||
| 224 | size_t new_alloc = alloc << 1; | ||
| 225 | if (unlikely(alloc >= new_alloc)) { | ||
| 226 | errno = EOVERFLOW; | ||
| 227 | goto error; | ||
| 228 | } | ||
| 229 | alloc = new_alloc; | ||
| 230 | char *new_buf = realloc(buf, alloc); | ||
| 231 | if (unlikely(!new_buf)) { | ||
| 232 | goto error; | ||
| 233 | } | ||
| 234 | buf = new_buf; | ||
| 235 | } | ||
| 236 | } | ||
| 237 | size = pos; | ||
| 238 | } | ||
| 239 | |||
| 240 | decode: | ||
| 241 | ret = decode_and_add_blocks(buffer, buf, size, utf8_bom); | ||
| 242 | |||
| 243 | error: | ||
| 244 | if (mapped) { | ||
| 245 | munmap(buf, size); | ||
| 246 | } else { | ||
| 247 | free(buf); | ||
| 248 | } | ||
| 249 | |||
| 250 | if (ret) { | ||
| 251 | fixup_blocks(buffer); | ||
| 252 | } | ||
| 253 | |||
| 254 | return ret; | ||
| 255 | } | ||
| 256 | |||
| 257 | bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist) | ||
| 258 | { | ||
| 259 | int fd = xopen(filename, O_RDONLY | O_CLOEXEC, 0); | ||
| 260 | |||
| 261 | if (fd < 0) { | ||
| 262 | if (errno != ENOENT) { | ||
| 263 | return error_msg("Error opening %s: %s", filename, strerror(errno)); | ||
| 264 | } | ||
| 265 | if (must_exist) { | ||
| 266 | return error_msg("File %s does not exist", filename); | ||
| 267 | } | ||
| 268 | fixup_blocks(buffer); | ||
| 269 | } else { | ||
| 270 | if (!buffer_fstat(&buffer->file, fd)) { | ||
| 271 | error_msg("fstat failed on %s: %s", filename, strerror(errno)); | ||
| 272 | goto error; | ||
| 273 | } | ||
| 274 | if (!S_ISREG(buffer->file.mode)) { | ||
| 275 | error_msg("Not a regular file %s", filename); | ||
| 276 | goto error; | ||
| 277 | } | ||
| 278 | if (unlikely(buffer->file.size < 0)) { | ||
| 279 | error_msg("Invalid file size: %jd", (intmax_t)buffer->file.size); | ||
| 280 | goto error; | ||
| 281 | } | ||
| 282 | if (buffer->file.size / 1024 / 1024 > gopts->filesize_limit) { | ||
| 283 | error_msg ( | ||
| 284 | "File size exceeds 'filesize-limit' option (%uMiB): %s", | ||
| 285 | gopts->filesize_limit, filename | ||
| 286 | ); | ||
| 287 | goto error; | ||
| 288 | } | ||
| 289 | if (!read_blocks(buffer, fd, gopts->utf8_bom)) { | ||
| 290 | error_msg("Error reading %s: %s", filename, strerror(errno)); | ||
| 291 | goto error; | ||
| 292 | } | ||
| 293 | xclose(fd); | ||
| 294 | } | ||
| 295 | |||
| 296 | if (buffer->encoding.type == ENCODING_AUTODETECT) { | ||
| 297 | Encoding enc = encoding_from_type(UTF8); | ||
| 298 | buffer_set_encoding(buffer, enc, gopts->utf8_bom); | ||
| 299 | } | ||
| 300 | |||
| 301 | return true; | ||
| 302 | |||
| 303 | error: | ||
| 304 | xclose(fd); | ||
| 305 | return false; | ||
| 306 | } | ||
| 307 | |||
| 308 | static mode_t get_umask(void) | ||
| 309 | { | ||
| 310 | // Wonderful get-and-set API | ||
| 311 | mode_t old = umask(0); | ||
| 312 | umask(old); | ||
| 313 | return old; | ||
| 314 | } | ||
| 315 | |||
| 316 | static bool write_buffer(Buffer *buffer, FileEncoder *enc, int fd, EncodingType bom_type) | ||
| 317 | { | ||
| 318 | size_t size = 0; | ||
| 319 | const ByteOrderMark *bom = get_bom_for_encoding(bom_type); | ||
| 320 | if (bom) { | ||
| 321 | size = bom->len; | ||
| 322 | BUG_ON(size == 0); | ||
| 323 | if (xwrite_all(fd, bom->bytes, size) < 0) { | ||
| 324 | return error_msg_errno("write"); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | Block *blk; | ||
| 329 | block_for_each(blk, &buffer->blocks) { | ||
| 330 | ssize_t rc = file_encoder_write(enc, blk->data, blk->size); | ||
| 331 | if (rc < 0) { | ||
| 332 | return error_msg_errno("write"); | ||
| 333 | } | ||
| 334 | size += rc; | ||
| 335 | } | ||
| 336 | |||
| 337 | size_t nr_errors = file_encoder_get_nr_errors(enc); | ||
| 338 | if (nr_errors > 0) { | ||
| 339 | // Any real error hides this message | ||
| 340 | error_msg ( | ||
| 341 | "Warning: %zu non-reversible character conversion%s; file saved", | ||
| 342 | nr_errors, | ||
| 343 | (nr_errors > 1) ? "s" : "" | ||
| 344 | ); | ||
| 345 | } | ||
| 346 | |||
| 347 | // Need to truncate if writing to existing file | ||
| 348 | if (xftruncate(fd, size)) { | ||
| 349 | return error_msg_errno("ftruncate"); | ||
| 350 | } | ||
| 351 | |||
| 352 | return true; | ||
| 353 | } | ||
| 354 | |||
| 355 | static int tmp_file(const char *filename, const FileInfo *info, char *buf, size_t buflen) | ||
| 356 | { | ||
| 357 | if (str_has_prefix(filename, "/tmp/")) { | ||
| 358 | // Don't use temporary file when saving file in /tmp because crontab | ||
| 359 | // command doesn't like the file to be replaced | ||
| 360 | return -1; | ||
| 361 | } | ||
| 362 | |||
| 363 | const char *base = path_basename(filename); | ||
| 364 | const StringView dir = path_slice_dirname(filename); | ||
| 365 | const int dlen = (int)dir.length; | ||
| 366 | int n = snprintf(buf, buflen, "%.*s/.tmp.%s.XXXXXX", dlen, dir.data, base); | ||
| 367 | if (unlikely(n <= 0 || n >= buflen)) { | ||
| 368 | buf[0] = '\0'; | ||
| 369 | return -1; | ||
| 370 | } | ||
| 371 | |||
| 372 | int fd = mkstemp(buf); | ||
| 373 | if (fd < 0) { | ||
| 374 | // No write permission to the directory? | ||
| 375 | buf[0] = '\0'; | ||
| 376 | return -1; | ||
| 377 | } | ||
| 378 | |||
| 379 | if (!info->mode) { | ||
| 380 | // New file | ||
| 381 | if (xfchmod(fd, 0666 & ~get_umask()) != 0) { | ||
| 382 | LOG_WARNING("failed to set file mode: %s", strerror(errno)); | ||
| 383 | } | ||
| 384 | return fd; | ||
| 385 | } | ||
| 386 | |||
| 387 | // Preserve ownership and mode of the original file if possible | ||
| 388 | if (xfchown(fd, info->uid, info->gid) != 0) { | ||
| 389 | LOG_WARNING("failed to preserve file ownership: %s", strerror(errno)); | ||
| 390 | } | ||
| 391 | if (xfchmod(fd, info->mode) != 0) { | ||
| 392 | LOG_WARNING("failed to preserve file mode: %s", strerror(errno)); | ||
| 393 | } | ||
| 394 | |||
| 395 | return fd; | ||
| 396 | } | ||
| 397 | |||
| 398 | static int xfsync(int fd) | ||
| 399 | { | ||
| 400 | #if HAVE_FSYNC | ||
| 401 | retry: | ||
| 402 | if (fsync(fd) == 0) { | ||
| 403 | return 0; | ||
| 404 | } | ||
| 405 | |||
| 406 | switch (errno) { | ||
| 407 | // EINVAL is ignored because it just means "operation not possible | ||
| 408 | // on this descriptor" rather than indicating an actual error | ||
| 409 | case EINVAL: | ||
| 410 | case ENOTSUP: | ||
| 411 | case ENOSYS: | ||
| 412 | return 0; | ||
| 413 | case EINTR: | ||
| 414 | goto retry; | ||
| 415 | } | ||
| 416 | |||
| 417 | return -1; | ||
| 418 | #else | ||
| 419 | (void)fd; | ||
| 420 | return 0; | ||
| 421 | #endif | ||
| 422 | } | ||
| 423 | |||
| 424 | bool save_buffer ( | ||
| 425 | Buffer *buffer, | ||
| 426 | const char *filename, | ||
| 427 | const Encoding *encoding, | ||
| 428 | bool crlf, | ||
| 429 | bool write_bom, | ||
| 430 | bool hardlinks | ||
| 431 | ) { | ||
| 432 | char tmp[8192]; | ||
| 433 | tmp[0] = '\0'; | ||
| 434 | int fd = -1; | ||
| 435 | if (hardlinks) { | ||
| 436 | LOG_INFO("target file has hard links; writing in-place"); | ||
| 437 | } else { | ||
| 438 | // Try to use temporary file (safer) | ||
| 439 | fd = tmp_file(filename, &buffer->file, tmp, sizeof(tmp)); | ||
| 440 | } | ||
| 441 | |||
| 442 | if (fd < 0) { | ||
| 443 | // Overwrite the original file directly (if it exists). | ||
| 444 | // Ownership is preserved automatically if the file exists. | ||
| 445 | mode_t mode = buffer->file.mode; | ||
| 446 | if (mode == 0) { | ||
| 447 | // New file | ||
| 448 | mode = 0666 & ~get_umask(); | ||
| 449 | } | ||
| 450 | fd = xopen(filename, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC, mode); | ||
| 451 | if (fd < 0) { | ||
| 452 | return error_msg_errno("open"); | ||
| 453 | } | ||
| 454 | } | ||
| 455 | |||
| 456 | FileEncoder *enc = new_file_encoder(encoding, crlf, fd); | ||
| 457 | if (unlikely(!enc)) { | ||
| 458 | // This should never happen because encoding is validated early | ||
| 459 | error_msg_errno("new_file_encoder"); | ||
| 460 | goto error; | ||
| 461 | } | ||
| 462 | |||
| 463 | EncodingType bom_type = write_bom ? encoding->type : UNKNOWN_ENCODING; | ||
| 464 | if (!write_buffer(buffer, enc, fd, bom_type)) { | ||
| 465 | goto error; | ||
| 466 | } | ||
| 467 | |||
| 468 | if (buffer->options.fsync && xfsync(fd) != 0) { | ||
| 469 | error_msg_errno("fsync"); | ||
| 470 | goto error; | ||
| 471 | } | ||
| 472 | |||
| 473 | int r = xclose(fd); | ||
| 474 | fd = -1; | ||
| 475 | if (r != 0) { | ||
| 476 | error_msg_errno("close"); | ||
| 477 | goto error; | ||
| 478 | } | ||
| 479 | |||
| 480 | if (tmp[0] && rename(tmp, filename)) { | ||
| 481 | error_msg_errno("rename"); | ||
| 482 | goto error; | ||
| 483 | } | ||
| 484 | |||
| 485 | free_file_encoder(enc); | ||
| 486 | buffer_stat(&buffer->file, filename); | ||
| 487 | return true; | ||
| 488 | |||
| 489 | error: | ||
| 490 | if (fd >= 0) { | ||
| 491 | xclose(fd); | ||
| 492 | } | ||
| 493 | if (enc) { | ||
| 494 | free_file_encoder(enc); | ||
| 495 | } | ||
| 496 | if (tmp[0]) { | ||
| 497 | unlink(tmp); | ||
| 498 | } else { | ||
| 499 | // Not using temporary file, therefore mtime may have changed. | ||
| 500 | // Update stat to avoid "File has been modified by someone else" | ||
| 501 | // error later when saving the file again. | ||
| 502 | buffer_stat(&buffer->file, filename); | ||
| 503 | } | ||
| 504 | return false; | ||
| 505 | } | ||
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 @@ | |||
| 1 | #ifndef LOAD_SAVE_H | ||
| 2 | #define LOAD_SAVE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "buffer.h" | ||
| 6 | #include "encoding.h" | ||
| 7 | #include "options.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | |||
| 10 | bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist) WARN_UNUSED_RESULT; | ||
| 11 | bool save_buffer(Buffer *buffer, const char *filename, const Encoding *encoding, bool crlf, bool write_bom, bool hardlinks) WARN_UNUSED_RESULT; | ||
| 12 | bool read_blocks(Buffer *buffer, int fd, bool utf8_bom) WARN_UNUSED_RESULT; | ||
| 13 | |||
| 14 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <signal.h> | ||
| 3 | #include <stdint.h> | ||
| 4 | #include <stdio.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <sys/stat.h> | ||
| 8 | #include <time.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | #include "lock.h" | ||
| 11 | #include "error.h" | ||
| 12 | #include "util/debug.h" | ||
| 13 | #include "util/log.h" | ||
| 14 | #include "util/path.h" | ||
| 15 | #include "util/readfile.h" | ||
| 16 | #include "util/str-util.h" | ||
| 17 | #include "util/string-view.h" | ||
| 18 | #include "util/strtonum.h" | ||
| 19 | #include "util/xmalloc.h" | ||
| 20 | #include "util/xreadwrite.h" | ||
| 21 | #include "util/xsnprintf.h" | ||
| 22 | |||
| 23 | // These are initialized during early startup and then never changed, | ||
| 24 | // so they're deemed an "acceptable" use of globals: | ||
| 25 | static const char *file_locks; | ||
| 26 | static const char *file_locks_lock; | ||
| 27 | static mode_t file_locks_mode = 0666; | ||
| 28 | static pid_t editor_pid; | ||
| 29 | |||
| 30 | void init_file_locks_context(const char *fallback_dir, pid_t pid) | ||
| 31 | { | ||
| 32 | BUG_ON(file_locks); | ||
| 33 | const char *dir = xgetenv("XDG_RUNTIME_DIR"); | ||
| 34 | if (!dir) { | ||
| 35 | LOG_INFO("$XDG_RUNTIME_DIR not set"); | ||
| 36 | dir = fallback_dir; | ||
| 37 | } else if (unlikely(!path_is_absolute(dir))) { | ||
| 38 | LOG_WARNING("$XDG_RUNTIME_DIR invalid (not an absolute path)"); | ||
| 39 | dir = fallback_dir; | ||
| 40 | } else { | ||
| 41 | // Set sticky bit (see XDG Base Directory Specification) | ||
| 42 | #ifdef S_ISVTX | ||
| 43 | file_locks_mode |= S_ISVTX; | ||
| 44 | #endif | ||
| 45 | } | ||
| 46 | |||
| 47 | file_locks = path_join(dir, "dte-locks"); | ||
| 48 | file_locks_lock = path_join(dir, "dte-locks.lock"); | ||
| 49 | editor_pid = pid; | ||
| 50 | LOG_INFO("locks file: %s", file_locks); | ||
| 51 | } | ||
| 52 | |||
| 53 | static bool process_exists(pid_t pid) | ||
| 54 | { | ||
| 55 | return !kill(pid, 0); | ||
| 56 | } | ||
| 57 | |||
| 58 | static pid_t rewrite_lock_file(char *buf, size_t *sizep, const char *filename) | ||
| 59 | { | ||
| 60 | const size_t filename_len = strlen(filename); | ||
| 61 | size_t size = *sizep; | ||
| 62 | pid_t other_pid = 0; | ||
| 63 | |||
| 64 | for (size_t pos = 0, bol = 0; pos < size; bol = pos) { | ||
| 65 | StringView line = buf_slice_next_line(buf, &pos, size); | ||
| 66 | uintmax_t num; | ||
| 67 | size_t numlen = buf_parse_uintmax(line.data, line.length, &num); | ||
| 68 | if (unlikely(numlen == 0 || num != (pid_t)num)) { | ||
| 69 | goto remove_line; | ||
| 70 | } | ||
| 71 | |||
| 72 | strview_remove_prefix(&line, numlen); | ||
| 73 | if (unlikely(!strview_has_prefix(&line, " /"))) { | ||
| 74 | goto remove_line; | ||
| 75 | } | ||
| 76 | strview_remove_prefix(&line, 1); | ||
| 77 | |||
| 78 | bool same = strview_equal_strn(&line, filename, filename_len); | ||
| 79 | pid_t pid = (pid_t)num; | ||
| 80 | if (pid == editor_pid) { | ||
| 81 | if (same) { | ||
| 82 | goto remove_line; | ||
| 83 | } | ||
| 84 | continue; | ||
| 85 | } else if (process_exists(pid)) { | ||
| 86 | if (same) { | ||
| 87 | other_pid = pid; | ||
| 88 | } | ||
| 89 | continue; | ||
| 90 | } | ||
| 91 | |||
| 92 | remove_line: | ||
| 93 | memmove(buf + bol, buf + pos, size - pos); | ||
| 94 | size -= pos - bol; | ||
| 95 | pos = bol; | ||
| 96 | } | ||
| 97 | |||
| 98 | *sizep = size; | ||
| 99 | return other_pid; | ||
| 100 | } | ||
| 101 | |||
| 102 | static bool lock_or_unlock(const char *filename, bool lock) | ||
| 103 | { | ||
| 104 | BUG_ON(!file_locks); | ||
| 105 | if (streq(filename, file_locks) || streq(filename, file_locks_lock)) { | ||
| 106 | return true; | ||
| 107 | } | ||
| 108 | |||
| 109 | mode_t mode = file_locks_mode; | ||
| 110 | int tries = 0; | ||
| 111 | int wfd; | ||
| 112 | while (1) { | ||
| 113 | wfd = xopen(file_locks_lock, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, mode); | ||
| 114 | if (wfd >= 0) { | ||
| 115 | break; | ||
| 116 | } | ||
| 117 | |||
| 118 | if (errno != EEXIST) { | ||
| 119 | return error_msg("Error creating %s: %s", file_locks_lock, strerror(errno)); | ||
| 120 | } | ||
| 121 | if (++tries == 3) { | ||
| 122 | if (unlink(file_locks_lock)) { | ||
| 123 | return error_msg ( | ||
| 124 | "Error removing stale lock file %s: %s", | ||
| 125 | file_locks_lock, | ||
| 126 | strerror(errno) | ||
| 127 | ); | ||
| 128 | } | ||
| 129 | error_msg("Stale lock file %s removed", file_locks_lock); | ||
| 130 | } else { | ||
| 131 | const struct timespec req = { | ||
| 132 | .tv_sec = 0, | ||
| 133 | .tv_nsec = 100 * 1000000, | ||
| 134 | }; | ||
| 135 | nanosleep(&req, NULL); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | char *buf = NULL; | ||
| 140 | ssize_t ssize = read_file(file_locks, &buf); | ||
| 141 | if (ssize < 0) { | ||
| 142 | if (errno != ENOENT) { | ||
| 143 | error_msg("Error reading %s: %s", file_locks, strerror(errno)); | ||
| 144 | goto error; | ||
| 145 | } | ||
| 146 | ssize = 0; | ||
| 147 | } | ||
| 148 | |||
| 149 | size_t size = (size_t)ssize; | ||
| 150 | pid_t pid = rewrite_lock_file(buf, &size, filename); | ||
| 151 | if (lock) { | ||
| 152 | if (pid == 0) { | ||
| 153 | intmax_t p = (intmax_t)editor_pid; | ||
| 154 | size_t n = strlen(filename) + DECIMAL_STR_MAX(pid) + 4; | ||
| 155 | xrenew(buf, size + n); | ||
| 156 | size += xsnprintf(buf + size, n, "%jd %s\n", p, filename); | ||
| 157 | } else { | ||
| 158 | intmax_t p = (intmax_t)pid; | ||
| 159 | error_msg("File is locked (%s) by process %jd", file_locks, p); | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | if (xwrite_all(wfd, buf, size) < 0) { | ||
| 164 | error_msg("Error writing %s: %s", file_locks_lock, strerror(errno)); | ||
| 165 | goto error; | ||
| 166 | } | ||
| 167 | |||
| 168 | int r = xclose(wfd); | ||
| 169 | wfd = -1; | ||
| 170 | if (r != 0) { | ||
| 171 | error_msg("Error closing %s: %s", file_locks_lock, strerror(errno)); | ||
| 172 | goto error; | ||
| 173 | } | ||
| 174 | |||
| 175 | if (rename(file_locks_lock, file_locks)) { | ||
| 176 | const char *err = strerror(errno); | ||
| 177 | error_msg("Renaming %s to %s: %s", file_locks_lock, file_locks, err); | ||
| 178 | goto error; | ||
| 179 | } | ||
| 180 | |||
| 181 | free(buf); | ||
| 182 | return (pid == 0); | ||
| 183 | |||
| 184 | error: | ||
| 185 | unlink(file_locks_lock); | ||
| 186 | free(buf); | ||
| 187 | if (wfd >= 0) { | ||
| 188 | xclose(wfd); | ||
| 189 | } | ||
| 190 | return false; | ||
| 191 | } | ||
| 192 | |||
| 193 | bool lock_file(const char *filename) | ||
| 194 | { | ||
| 195 | return lock_or_unlock(filename, true); | ||
| 196 | } | ||
| 197 | |||
| 198 | void unlock_file(const char *filename) | ||
| 199 | { | ||
| 200 | lock_or_unlock(filename, false); | ||
| 201 | } | ||
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 @@ | |||
| 1 | #ifndef LOCK_H | ||
| 2 | #define LOCK_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <sys/types.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | |||
| 8 | void init_file_locks_context(const char *fallback_dir, pid_t pid); | ||
| 9 | bool lock_file(const char *filename) WARN_UNUSED_RESULT; | ||
| 10 | void unlock_file(const char *filename); | ||
| 11 | |||
| 12 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <fcntl.h> | ||
| 3 | #include <stdbool.h> | ||
| 4 | #include <stdio.h> | ||
| 5 | #include <stdlib.h> | ||
| 6 | #include <string.h> | ||
| 7 | #include <sys/stat.h> | ||
| 8 | #include <sys/utsname.h> | ||
| 9 | #include <unistd.h> | ||
| 10 | #include "block.h" | ||
| 11 | #include "commands.h" | ||
| 12 | #include "compiler.h" | ||
| 13 | #include "config.h" | ||
| 14 | #include "editor.h" | ||
| 15 | #include "error.h" | ||
| 16 | #include "file-history.h" | ||
| 17 | #include "frame.h" | ||
| 18 | #include "history.h" | ||
| 19 | #include "load-save.h" | ||
| 20 | #include "move.h" | ||
| 21 | #include "screen.h" | ||
| 22 | #include "search.h" | ||
| 23 | #include "signals.h" | ||
| 24 | #include "syntax/state.h" | ||
| 25 | #include "syntax/syntax.h" | ||
| 26 | #include "tag.h" | ||
| 27 | #include "terminal/input.h" | ||
| 28 | #include "terminal/key.h" | ||
| 29 | #include "terminal/mode.h" | ||
| 30 | #include "terminal/output.h" | ||
| 31 | #include "terminal/terminal.h" | ||
| 32 | #include "util/debug.h" | ||
| 33 | #include "util/exitcode.h" | ||
| 34 | #include "util/fd.h" | ||
| 35 | #include "util/log.h" | ||
| 36 | #include "util/macros.h" | ||
| 37 | #include "util/path.h" | ||
| 38 | #include "util/ptr-array.h" | ||
| 39 | #include "util/strtonum.h" | ||
| 40 | #include "util/xmalloc.h" | ||
| 41 | #include "util/xreadwrite.h" | ||
| 42 | #include "util/xsnprintf.h" | ||
| 43 | #include "view.h" | ||
| 44 | #include "window.h" | ||
| 45 | #include "../build/version.h" | ||
| 46 | |||
| 47 | static void term_cleanup(EditorState *e) | ||
| 48 | { | ||
| 49 | set_fatal_error_cleanup_handler(NULL, NULL); | ||
| 50 | if (!e->child_controls_terminal) { | ||
| 51 | ui_end(e); | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | static void cleanup_handler(void *userdata) | ||
| 56 | { | ||
| 57 | term_cleanup(userdata); | ||
| 58 | } | ||
| 59 | |||
| 60 | static ExitCode write_stdout(const char *str, size_t len) | ||
| 61 | { | ||
| 62 | if (xwrite_all(STDOUT_FILENO, str, len) < 0) { | ||
| 63 | perror("write"); | ||
| 64 | return EX_IOERR; | ||
| 65 | } | ||
| 66 | return EX_OK; | ||
| 67 | } | ||
| 68 | |||
| 69 | static ExitCode list_builtin_configs(void) | ||
| 70 | { | ||
| 71 | String str = dump_builtin_configs(); | ||
| 72 | BUG_ON(!str.buffer); | ||
| 73 | ExitCode e = write_stdout(str.buffer, str.len); | ||
| 74 | string_free(&str); | ||
| 75 | return e; | ||
| 76 | } | ||
| 77 | |||
| 78 | static ExitCode dump_builtin_config(const char *name) | ||
| 79 | { | ||
| 80 | const BuiltinConfig *cfg = get_builtin_config(name); | ||
| 81 | if (!cfg) { | ||
| 82 | fprintf(stderr, "Error: no built-in config with name '%s'\n", name); | ||
| 83 | return EX_USAGE; | ||
| 84 | } | ||
| 85 | return write_stdout(cfg->text.data, cfg->text.length); | ||
| 86 | } | ||
| 87 | |||
| 88 | static ExitCode lint_syntax(const char *filename) | ||
| 89 | { | ||
| 90 | EditorState *e = init_editor_state(); | ||
| 91 | int err; | ||
| 92 | BUG_ON(e->status != EDITOR_INITIALIZING); | ||
| 93 | const Syntax *s = load_syntax_file(e, filename, CFG_MUST_EXIST, &err); | ||
| 94 | if (s) { | ||
| 95 | const size_t n = s->states.count; | ||
| 96 | const char *plural = (n == 1) ? "" : "s"; | ||
| 97 | printf("OK: loaded syntax '%s' with %zu state%s\n", s->name, n, plural); | ||
| 98 | } else if (err == EINVAL) { | ||
| 99 | error_msg("%s: no default syntax found", filename); | ||
| 100 | } | ||
| 101 | free_editor_state(e); | ||
| 102 | return get_nr_errors() ? EX_DATAERR : EX_OK; | ||
| 103 | } | ||
| 104 | |||
| 105 | static ExitCode showkey_loop(const char *term_name, const char *colorterm) | ||
| 106 | { | ||
| 107 | if (unlikely(!term_raw())) { | ||
| 108 | perror("tcsetattr"); | ||
| 109 | return EX_IOERR; | ||
| 110 | } | ||
| 111 | |||
| 112 | Terminal term; | ||
| 113 | TermOutputBuffer *obuf = &term.obuf; | ||
| 114 | TermInputBuffer *ibuf = &term.ibuf; | ||
| 115 | term_init(&term, term_name, colorterm); | ||
| 116 | term_input_init(ibuf); | ||
| 117 | term_output_init(obuf); | ||
| 118 | term_enable_private_modes(&term); | ||
| 119 | term_add_literal(obuf, "Press any key combination, or use Ctrl+D to exit\r\n"); | ||
| 120 | term_output_flush(obuf); | ||
| 121 | |||
| 122 | char keystr[KEYCODE_STR_MAX]; | ||
| 123 | for (bool loop = true; loop; ) { | ||
| 124 | KeyCode key = term_read_key(&term, 100); | ||
| 125 | switch (key) { | ||
| 126 | case KEY_NONE: | ||
| 127 | case KEY_IGNORE: | ||
| 128 | continue; | ||
| 129 | case KEY_BRACKETED_PASTE: | ||
| 130 | case KEY_DETECTED_PASTE: | ||
| 131 | term_discard_paste(ibuf, key == KEY_BRACKETED_PASTE); | ||
| 132 | continue; | ||
| 133 | case MOD_CTRL | 'd': | ||
| 134 | loop = false; | ||
| 135 | } | ||
| 136 | size_t keylen = keycode_to_string(key, keystr); | ||
| 137 | term_add_literal(obuf, " "); | ||
| 138 | term_add_bytes(obuf, keystr, keylen); | ||
| 139 | term_add_literal(obuf, "\r\n"); | ||
| 140 | term_output_flush(obuf); | ||
| 141 | } | ||
| 142 | |||
| 143 | term_restore_private_modes(&term); | ||
| 144 | term_output_flush(obuf); | ||
| 145 | term_cooked(); | ||
| 146 | term_input_free(ibuf); | ||
| 147 | term_output_free(obuf); | ||
| 148 | return EX_OK; | ||
| 149 | } | ||
| 150 | |||
| 151 | static ExitCode init_std_fds(int std_fds[2]) | ||
| 152 | { | ||
| 153 | FILE *streams[3] = {stdin, stdout, stderr}; | ||
| 154 | for (int i = 0; i < ARRAYLEN(streams); i++) { | ||
| 155 | if (is_controlling_tty(i)) { | ||
| 156 | continue; | ||
| 157 | } | ||
| 158 | |||
| 159 | if (i < STDERR_FILENO) { | ||
| 160 | // Try to create a duplicate fd for redirected stdin/stdout; to | ||
| 161 | // allow reading/writing after freopen(3) closes the original | ||
| 162 | int fd = fcntl(i, F_DUPFD_CLOEXEC, 3); | ||
| 163 | if (fd == -1 && errno != EBADF) { | ||
| 164 | perror("fcntl"); | ||
| 165 | return EX_OSERR; | ||
| 166 | } | ||
| 167 | std_fds[i] = fd; | ||
| 168 | } | ||
| 169 | |||
| 170 | // Ensure standard streams are connected to the terminal during | ||
| 171 | // editor operation, regardless of how they were redirected | ||
| 172 | if (unlikely(!freopen("/dev/tty", i ? "w" : "r", streams[i]))) { | ||
| 173 | const char *err = strerror(errno); | ||
| 174 | fprintf(stderr, "Failed to open tty for fd %d: %s\n", i, err); | ||
| 175 | return EX_IOERR; | ||
| 176 | } | ||
| 177 | |||
| 178 | int new_fd = fileno(streams[i]); | ||
| 179 | if (unlikely(new_fd != i)) { | ||
| 180 | // This should never happen in a single-threaded program. | ||
| 181 | // freopen() should call fclose() followed by open() and | ||
| 182 | // POSIX requires a successful call to open() to return the | ||
| 183 | // lowest available file descriptor. | ||
| 184 | fprintf(stderr, "freopen() changed fd from %d to %d\n", i, new_fd); | ||
| 185 | return EX_OSERR; | ||
| 186 | } | ||
| 187 | |||
| 188 | if (unlikely(!is_controlling_tty(new_fd))) { | ||
| 189 | perror("tcgetpgrp"); | ||
| 190 | return EX_OSERR; | ||
| 191 | } | ||
| 192 | } | ||
| 193 | |||
| 194 | return EX_OK; | ||
| 195 | } | ||
| 196 | |||
| 197 | static Buffer *init_std_buffer(EditorState *e, int fds[2]) | ||
| 198 | { | ||
| 199 | const char *name = NULL; | ||
| 200 | Buffer *buffer = NULL; | ||
| 201 | |||
| 202 | if (fds[STDIN_FILENO] >= 3) { | ||
| 203 | Encoding enc = encoding_from_type(UTF8); | ||
| 204 | buffer = buffer_new(&e->buffers, &e->options, &enc); | ||
| 205 | if (read_blocks(buffer, fds[STDIN_FILENO], false)) { | ||
| 206 | name = "(stdin)"; | ||
| 207 | buffer->temporary = true; | ||
| 208 | } else { | ||
| 209 | error_msg("Unable to read redirected stdin"); | ||
| 210 | remove_and_free_buffer(&e->buffers, buffer); | ||
| 211 | buffer = NULL; | ||
| 212 | } | ||
| 213 | } | ||
| 214 | |||
| 215 | if (fds[STDOUT_FILENO] >= 3) { | ||
| 216 | if (!buffer) { | ||
| 217 | buffer = open_empty_buffer(&e->buffers, &e->options); | ||
| 218 | name = "(stdout)"; | ||
| 219 | } else { | ||
| 220 | name = "(stdin|stdout)"; | ||
| 221 | } | ||
| 222 | buffer->stdout_buffer = true; | ||
| 223 | buffer->temporary = true; | ||
| 224 | } | ||
| 225 | |||
| 226 | BUG_ON(!buffer != !name); | ||
| 227 | if (name) { | ||
| 228 | set_display_filename(buffer, xstrdup(name)); | ||
| 229 | } | ||
| 230 | |||
| 231 | return buffer; | ||
| 232 | } | ||
| 233 | |||
| 234 | static ExitCode init_logging(const char *filename, const char *req_level_str) | ||
| 235 | { | ||
| 236 | if (!filename || filename[0] == '\0') { | ||
| 237 | return EX_OK; | ||
| 238 | } | ||
| 239 | |||
| 240 | LogLevel req_level = log_level_from_str(req_level_str); | ||
| 241 | if (req_level == LOG_LEVEL_NONE) { | ||
| 242 | return EX_OK; | ||
| 243 | } | ||
| 244 | if (req_level == LOG_LEVEL_INVALID) { | ||
| 245 | fprintf(stderr, "Invalid $DTE_LOG_LEVEL value: '%s'\n", req_level_str); | ||
| 246 | return EX_USAGE; | ||
| 247 | } | ||
| 248 | |||
| 249 | // https://no-color.org/ | ||
| 250 | const char *no_color = xgetenv("NO_COLOR"); | ||
| 251 | |||
| 252 | LogLevel got_level = log_open(filename, req_level, !no_color); | ||
| 253 | if (got_level == LOG_LEVEL_NONE) { | ||
| 254 | const char *err = strerror(errno); | ||
| 255 | fprintf(stderr, "Failed to open $DTE_LOG (%s): %s\n", filename, err); | ||
| 256 | return EX_IOERR; | ||
| 257 | } | ||
| 258 | |||
| 259 | const char *got_level_str = log_level_to_str(got_level); | ||
| 260 | if (got_level != req_level) { | ||
| 261 | const char *r = req_level_str; | ||
| 262 | const char *g = got_level_str; | ||
| 263 | LOG_WARNING("log level '%s' unavailable; falling back to '%s'", r, g); | ||
| 264 | } | ||
| 265 | |||
| 266 | LOG_INFO("logging to '%s' (level: %s)", filename, got_level_str); | ||
| 267 | |||
| 268 | if (no_color) { | ||
| 269 | LOG_INFO("log colors disabled ($NO_COLOR)"); | ||
| 270 | } | ||
| 271 | |||
| 272 | struct utsname u; | ||
| 273 | if (likely(uname(&u) >= 0)) { | ||
| 274 | LOG_INFO("system: %s/%s %s", u.sysname, u.machine, u.release); | ||
| 275 | } else { | ||
| 276 | LOG_ERRNO("uname"); | ||
| 277 | } | ||
| 278 | return EX_OK; | ||
| 279 | } | ||
| 280 | |||
| 281 | static void log_config_counts(const EditorState *e) | ||
| 282 | { | ||
| 283 | if (!log_level_enabled(LOG_LEVEL_INFO)) { | ||
| 284 | return; | ||
| 285 | } | ||
| 286 | |||
| 287 | size_t nbinds = 0; | ||
| 288 | for (size_t i = 0; i < ARRAYLEN(e->modes); i++) { | ||
| 289 | nbinds += e->modes[i].key_bindings.count; | ||
| 290 | } | ||
| 291 | |||
| 292 | size_t nerrorfmts = 0; | ||
| 293 | for (HashMapIter it = hashmap_iter(&e->compilers); hashmap_next(&it); ) { | ||
| 294 | const Compiler *compiler = it.entry->value; | ||
| 295 | nerrorfmts += compiler->error_formats.count; | ||
| 296 | } | ||
| 297 | |||
| 298 | LOG_INFO ( | ||
| 299 | "binds=%zu aliases=%zu hi=%zu ft=%zu option=%zu errorfmt=%zu(%zu)", | ||
| 300 | nbinds, | ||
| 301 | e->aliases.count, | ||
| 302 | e->colors.other.count + NR_BC, | ||
| 303 | e->filetypes.count, | ||
| 304 | e->file_options.count, | ||
| 305 | e->compilers.count, | ||
| 306 | nerrorfmts | ||
| 307 | ); | ||
| 308 | } | ||
| 309 | |||
| 310 | static const char copyright[] = | ||
| 311 | "dte " VERSION "\n" | ||
| 312 | "(C) 2013-2023 Craig Barnes\n" | ||
| 313 | "(C) 2010-2015 Timo Hirvonen\n" | ||
| 314 | "This program is free software; you can redistribute and/or modify\n" | ||
| 315 | "it under the terms of the GNU General Public License version 2\n" | ||
| 316 | "<https://www.gnu.org/licenses/old-licenses/gpl-2.0.html>.\n" | ||
| 317 | "There is NO WARRANTY, to the extent permitted by law.\n"; | ||
| 318 | |||
| 319 | static const char usage[] = | ||
| 320 | "Usage: %s [OPTIONS] [[+LINE] FILE]...\n\n" | ||
| 321 | "Options:\n" | ||
| 322 | " -c COMMAND Run COMMAND after editor starts\n" | ||
| 323 | " -t CTAG Jump to source location of CTAG\n" | ||
| 324 | " -r RCFILE Read user config from RCFILE instead of ~/.dte/rc\n" | ||
| 325 | " -s FILE Validate dte-syntax commands in FILE and exit\n" | ||
| 326 | " -b NAME Print built-in config matching NAME and exit\n" | ||
| 327 | " -B Print list of built-in config names and exit\n" | ||
| 328 | " -H Don't load or save history files\n" | ||
| 329 | " -R Don't read user config file\n" | ||
| 330 | " -K Start editor in \"showkey\" mode\n" | ||
| 331 | " -h Display help summary and exit\n" | ||
| 332 | " -V Display version number and exit\n" | ||
| 333 | "\n"; | ||
| 334 | |||
| 335 | int main(int argc, char *argv[]) | ||
| 336 | { | ||
| 337 | static const char optstring[] = "hBHKRVb:c:t:r:s:"; | ||
| 338 | const char *tag = NULL; | ||
| 339 | const char *rc = NULL; | ||
| 340 | const char *commands[8]; | ||
| 341 | size_t nr_commands = 0; | ||
| 342 | bool read_rc = true; | ||
| 343 | bool use_showkey = false; | ||
| 344 | bool load_and_save_history = true; | ||
| 345 | set_print_errors_to_stderr(true); | ||
| 346 | |||
| 347 | for (int ch; (ch = getopt(argc, argv, optstring)) != -1; ) { | ||
| 348 | switch (ch) { | ||
| 349 | case 'c': | ||
| 350 | if (unlikely(nr_commands >= ARRAYLEN(commands))) { | ||
| 351 | fputs("Error: too many -c options used\n", stderr); | ||
| 352 | return EX_USAGE; | ||
| 353 | } | ||
| 354 | commands[nr_commands++] = optarg; | ||
| 355 | break; | ||
| 356 | case 't': | ||
| 357 | tag = optarg; | ||
| 358 | break; | ||
| 359 | case 'r': | ||
| 360 | rc = optarg; | ||
| 361 | break; | ||
| 362 | case 's': | ||
| 363 | return lint_syntax(optarg); | ||
| 364 | case 'R': | ||
| 365 | read_rc = false; | ||
| 366 | break; | ||
| 367 | case 'b': | ||
| 368 | return dump_builtin_config(optarg); | ||
| 369 | case 'B': | ||
| 370 | return list_builtin_configs(); | ||
| 371 | case 'H': | ||
| 372 | load_and_save_history = false; | ||
| 373 | break; | ||
| 374 | case 'K': | ||
| 375 | use_showkey = true; | ||
| 376 | goto loop_break; | ||
| 377 | case 'V': | ||
| 378 | return write_stdout(copyright, sizeof(copyright)); | ||
| 379 | case 'h': | ||
| 380 | printf(usage, (argv[0] && argv[0][0]) ? argv[0] : "dte"); | ||
| 381 | return EX_OK; | ||
| 382 | default: | ||
| 383 | return EX_USAGE; | ||
| 384 | } | ||
| 385 | } | ||
| 386 | |||
| 387 | loop_break:; | ||
| 388 | |||
| 389 | const char *term_name = xgetenv("TERM"); | ||
| 390 | if (!term_name) { | ||
| 391 | fputs("Error: $TERM not set\n", stderr); | ||
| 392 | // This is considered a "usage" error, because the program | ||
| 393 | // must be started from a properly configured terminal | ||
| 394 | return EX_USAGE; | ||
| 395 | } | ||
| 396 | |||
| 397 | // This must be done before calling init_logging(), otherwise an | ||
| 398 | // invocation like e.g. `DTE_LOG=/dev/pts/2 dte 0<&-` could | ||
| 399 | // cause the logging fd to be opened as STDIN_FILENO | ||
| 400 | int std_fds[2] = {-1, -1}; | ||
| 401 | ExitCode r = init_std_fds(std_fds); | ||
| 402 | if (unlikely(r != EX_OK)) { | ||
| 403 | return r; | ||
| 404 | } | ||
| 405 | |||
| 406 | r = init_logging(getenv("DTE_LOG"), getenv("DTE_LOG_LEVEL")); | ||
| 407 | if (unlikely(r != EX_OK)) { | ||
| 408 | return r; | ||
| 409 | } | ||
| 410 | |||
| 411 | if (!term_mode_init()) { | ||
| 412 | perror("tcgetattr"); | ||
| 413 | return EX_IOERR; | ||
| 414 | } | ||
| 415 | |||
| 416 | const char *colorterm = getenv("COLORTERM"); | ||
| 417 | if (use_showkey) { | ||
| 418 | return showkey_loop(term_name, colorterm); | ||
| 419 | } | ||
| 420 | |||
| 421 | EditorState *e = init_editor_state(); | ||
| 422 | Terminal *term = &e->terminal; | ||
| 423 | term_init(term, term_name, colorterm); | ||
| 424 | |||
| 425 | Buffer *std_buffer = init_std_buffer(e, std_fds); | ||
| 426 | bool have_stdout_buffer = std_buffer && std_buffer->stdout_buffer; | ||
| 427 | |||
| 428 | // Create this early (needed if "lock-files" is true) | ||
| 429 | const char *cfgdir = e->user_config_dir; | ||
| 430 | BUG_ON(!cfgdir); | ||
| 431 | if (mkdir(cfgdir, 0755) != 0 && errno != EEXIST) { | ||
| 432 | error_msg("Error creating %s: %s", cfgdir, strerror(errno)); | ||
| 433 | load_and_save_history = false; | ||
| 434 | e->options.lock_files = false; | ||
| 435 | } | ||
| 436 | |||
| 437 | term_save_title(term); | ||
| 438 | exec_builtin_rc(e); | ||
| 439 | |||
| 440 | if (read_rc) { | ||
| 441 | ConfigFlags flags = CFG_NOFLAGS; | ||
| 442 | char buf[4096]; | ||
| 443 | if (rc) { | ||
| 444 | flags |= CFG_MUST_EXIST; | ||
| 445 | } else { | ||
| 446 | xsnprintf(buf, sizeof buf, "%s/%s", cfgdir, "rc"); | ||
| 447 | rc = buf; | ||
| 448 | } | ||
| 449 | LOG_INFO("loading configuration from %s", rc); | ||
| 450 | read_normal_config(e, rc, flags); | ||
| 451 | } | ||
| 452 | |||
| 453 | log_config_counts(e); | ||
| 454 | update_all_syntax_colors(&e->syntaxes, &e->colors); | ||
| 455 | |||
| 456 | Window *window = new_window(e); | ||
| 457 | e->window = window; | ||
| 458 | e->root_frame = new_root_frame(window); | ||
| 459 | |||
| 460 | set_signal_handlers(); | ||
| 461 | set_fatal_error_cleanup_handler(cleanup_handler, e); | ||
| 462 | |||
| 463 | if (load_and_save_history) { | ||
| 464 | file_history_load(&e->file_history, path_join(cfgdir, "file-history")); | ||
| 465 | history_load(&e->command_history, path_join(cfgdir, "command-history")); | ||
| 466 | history_load(&e->search_history, path_join(cfgdir, "search-history")); | ||
| 467 | if (e->search_history.last) { | ||
| 468 | search_set_regexp(&e->search, e->search_history.last->text); | ||
| 469 | } | ||
| 470 | } | ||
| 471 | |||
| 472 | set_print_errors_to_stderr(false); | ||
| 473 | |||
| 474 | // Initialize terminal but don't update screen yet. Also display | ||
| 475 | // "Press any key to continue" prompt if there were any errors | ||
| 476 | // during reading configuration files. | ||
| 477 | if (!term_raw()) { | ||
| 478 | perror("tcsetattr"); | ||
| 479 | return EX_IOERR; | ||
| 480 | } | ||
| 481 | if (get_nr_errors()) { | ||
| 482 | any_key(term, e->options.esc_timeout); | ||
| 483 | clear_error(); | ||
| 484 | } | ||
| 485 | |||
| 486 | e->status = EDITOR_RUNNING; | ||
| 487 | |||
| 488 | for (size_t i = optind, line = 0, col = 0; i < argc; i++) { | ||
| 489 | const char *str = argv[i]; | ||
| 490 | if (line == 0 && *str == '+' && str_to_filepos(str + 1, &line, &col)) { | ||
| 491 | continue; | ||
| 492 | } | ||
| 493 | View *view = window_open_buffer(window, str, false, NULL); | ||
| 494 | if (line == 0) { | ||
| 495 | continue; | ||
| 496 | } | ||
| 497 | set_view(view); | ||
| 498 | move_to_filepos(view, line, col); | ||
| 499 | line = 0; | ||
| 500 | } | ||
| 501 | |||
| 502 | if (std_buffer) { | ||
| 503 | window_add_buffer(window, std_buffer); | ||
| 504 | } | ||
| 505 | |||
| 506 | View *dview = NULL; | ||
| 507 | if (window->views.count == 0) { | ||
| 508 | // Open a default buffer, if none were opened for arguments | ||
| 509 | dview = window_open_empty_buffer(window); | ||
| 510 | BUG_ON(!dview); | ||
| 511 | BUG_ON(window->views.count != 1); | ||
| 512 | BUG_ON(dview != window->views.ptrs[0]); | ||
| 513 | } | ||
| 514 | |||
| 515 | set_view(window->views.ptrs[0]); | ||
| 516 | ui_start(e); | ||
| 517 | |||
| 518 | for (size_t i = 0; i < nr_commands; i++) { | ||
| 519 | handle_normal_command(e, commands[i], false); | ||
| 520 | } | ||
| 521 | |||
| 522 | if (tag) { | ||
| 523 | StringView tag_sv = strview_from_cstring(tag); | ||
| 524 | if (tag_lookup(&e->tagfile, &tag_sv, NULL, &e->messages)) { | ||
| 525 | activate_current_message(e); | ||
| 526 | if (dview && nr_commands == 0 && window->views.count > 1) { | ||
| 527 | // Close default/empty buffer, if `-t` jumped to a tag | ||
| 528 | // and no commands were executed via `-c` | ||
| 529 | remove_view(dview); | ||
| 530 | dview = NULL; | ||
| 531 | } | ||
| 532 | } | ||
| 533 | } | ||
| 534 | |||
| 535 | if (nr_commands > 0 || tag) { | ||
| 536 | normal_update(e); | ||
| 537 | } | ||
| 538 | |||
| 539 | int exit_code = main_loop(e); | ||
| 540 | |||
| 541 | term_restore_title(term); | ||
| 542 | ui_end(e); | ||
| 543 | term_output_flush(&term->obuf); | ||
| 544 | set_print_errors_to_stderr(true); | ||
| 545 | |||
| 546 | // Unlock files and add to file history | ||
| 547 | remove_frame(e, e->root_frame); | ||
| 548 | |||
| 549 | if (load_and_save_history) { | ||
| 550 | history_save(&e->command_history); | ||
| 551 | history_save(&e->search_history); | ||
| 552 | file_history_save(&e->file_history); | ||
| 553 | } | ||
| 554 | |||
| 555 | if (have_stdout_buffer) { | ||
| 556 | int fd = std_fds[STDOUT_FILENO]; | ||
| 557 | Block *blk; | ||
| 558 | block_for_each(blk, &std_buffer->blocks) { | ||
| 559 | if (xwrite_all(fd, blk->data, blk->size) < 0) { | ||
| 560 | error_msg_errno("failed to write (stdout) buffer"); | ||
| 561 | if (exit_code == EDITOR_EXIT_OK) { | ||
| 562 | exit_code = EX_IOERR; | ||
| 563 | } | ||
| 564 | break; | ||
| 565 | } | ||
| 566 | } | ||
| 567 | free_blocks(std_buffer); | ||
| 568 | free(std_buffer); | ||
| 569 | } | ||
| 570 | |||
| 571 | free_editor_state(e); | ||
| 572 | LOG_INFO("exiting with status %d", exit_code); | ||
| 573 | log_close(); | ||
| 574 | return exit_code; | ||
| 575 | } | ||
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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "misc.h" | ||
| 4 | #include "buffer.h" | ||
| 5 | #include "change.h" | ||
| 6 | #include "indent.h" | ||
| 7 | #include "move.h" | ||
| 8 | #include "options.h" | ||
| 9 | #include "regexp.h" | ||
| 10 | #include "selection.h" | ||
| 11 | #include "util/debug.h" | ||
| 12 | #include "util/macros.h" | ||
| 13 | #include "util/string.h" | ||
| 14 | #include "util/string-view.h" | ||
| 15 | #include "util/utf8.h" | ||
| 16 | |||
| 17 | typedef struct { | ||
| 18 | String buf; | ||
| 19 | char *indent; | ||
| 20 | size_t indent_len; | ||
| 21 | size_t indent_width; | ||
| 22 | size_t cur_width; | ||
| 23 | size_t text_width; | ||
| 24 | } ParagraphFormatter; | ||
| 25 | |||
| 26 | static bool line_has_opening_brace(StringView line) | ||
| 27 | { | ||
| 28 | static regex_t re; | ||
| 29 | static bool compiled; | ||
| 30 | if (!compiled) { | ||
| 31 | // TODO: Reimplement without using regex | ||
| 32 | static const char pat[] = "\\{[ \t]*(//.*|/\\*.*\\*/[ \t]*)?$"; | ||
| 33 | regexp_compile_or_fatal_error(&re, pat, REG_NEWLINE | REG_NOSUB); | ||
| 34 | compiled = true; | ||
| 35 | } | ||
| 36 | |||
| 37 | regmatch_t m; | ||
| 38 | return regexp_exec(&re, line.data, line.length, 0, &m, 0); | ||
| 39 | } | ||
| 40 | |||
| 41 | static bool line_has_closing_brace(StringView line) | ||
| 42 | { | ||
| 43 | strview_trim_left(&line); | ||
| 44 | return line.length > 0 && line.data[0] == '}'; | ||
| 45 | } | ||
| 46 | |||
| 47 | /* | ||
| 48 | * Stupid { ... } block selector. | ||
| 49 | * | ||
| 50 | * Because braces can be inside strings or comments and writing real | ||
| 51 | * parser for many programming languages does not make sense the rules | ||
| 52 | * for selecting a block are made very simple. Line that matches \{\s*$ | ||
| 53 | * starts a block and line that matches ^\s*\} ends it. | ||
| 54 | */ | ||
| 55 | void select_block(View *view) | ||
| 56 | { | ||
| 57 | BlockIter sbi, ebi, bi = view->cursor; | ||
| 58 | StringView line; | ||
| 59 | int level = 0; | ||
| 60 | |||
| 61 | // If current line does not match \{\s*$ but matches ^\s*\} then | ||
| 62 | // cursor is likely at end of the block you want to select | ||
| 63 | fetch_this_line(&bi, &line); | ||
| 64 | if (!line_has_opening_brace(line) && line_has_closing_brace(line)) { | ||
| 65 | block_iter_prev_line(&bi); | ||
| 66 | } | ||
| 67 | |||
| 68 | while (1) { | ||
| 69 | fetch_this_line(&bi, &line); | ||
| 70 | if (line_has_opening_brace(line)) { | ||
| 71 | if (level++ == 0) { | ||
| 72 | sbi = bi; | ||
| 73 | block_iter_next_line(&bi); | ||
| 74 | break; | ||
| 75 | } | ||
| 76 | } | ||
| 77 | if (line_has_closing_brace(line)) { | ||
| 78 | level--; | ||
| 79 | } | ||
| 80 | |||
| 81 | if (!block_iter_prev_line(&bi)) { | ||
| 82 | return; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | while (1) { | ||
| 87 | fetch_this_line(&bi, &line); | ||
| 88 | if (line_has_closing_brace(line)) { | ||
| 89 | if (--level == 0) { | ||
| 90 | ebi = bi; | ||
| 91 | break; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | if (line_has_opening_brace(line)) { | ||
| 95 | level++; | ||
| 96 | } | ||
| 97 | |||
| 98 | if (!block_iter_next_line(&bi)) { | ||
| 99 | return; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | view->cursor = sbi; | ||
| 104 | view->sel_so = block_iter_get_offset(&ebi); | ||
| 105 | view->sel_eo = SEL_EO_RECALC; | ||
| 106 | view->selection = SELECT_LINES; | ||
| 107 | |||
| 108 | mark_all_lines_changed(view->buffer); | ||
| 109 | } | ||
| 110 | |||
| 111 | static int get_indent_of_matching_brace(const View *view) | ||
| 112 | { | ||
| 113 | const LocalOptions *options = &view->buffer->options; | ||
| 114 | BlockIter bi = view->cursor; | ||
| 115 | StringView line; | ||
| 116 | int level = 0; | ||
| 117 | |||
| 118 | while (block_iter_prev_line(&bi)) { | ||
| 119 | fetch_this_line(&bi, &line); | ||
| 120 | if (line_has_opening_brace(line)) { | ||
| 121 | if (level++ == 0) { | ||
| 122 | return get_indent_width(options, &line); | ||
| 123 | } | ||
| 124 | } | ||
| 125 | if (line_has_closing_brace(line)) { | ||
| 126 | level--; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | return -1; | ||
| 131 | } | ||
| 132 | |||
| 133 | void unselect(View *view) | ||
| 134 | { | ||
| 135 | view->select_mode = SELECT_NONE; | ||
| 136 | if (view->selection) { | ||
| 137 | view->selection = SELECT_NONE; | ||
| 138 | mark_all_lines_changed(view->buffer); | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | void insert_text(View *view, const char *text, size_t size, bool move_after) | ||
| 143 | { | ||
| 144 | size_t del_count = 0; | ||
| 145 | if (view->selection) { | ||
| 146 | del_count = prepare_selection(view); | ||
| 147 | unselect(view); | ||
| 148 | } | ||
| 149 | buffer_replace_bytes(view, del_count, text, size); | ||
| 150 | if (move_after) { | ||
| 151 | block_iter_skip_bytes(&view->cursor, size); | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | void delete_ch(View *view) | ||
| 156 | { | ||
| 157 | size_t size = 0; | ||
| 158 | if (view->selection) { | ||
| 159 | size = prepare_selection(view); | ||
| 160 | unselect(view); | ||
| 161 | } else { | ||
| 162 | const LocalOptions *options = &view->buffer->options; | ||
| 163 | begin_change(CHANGE_MERGE_DELETE); | ||
| 164 | if (options->emulate_tab) { | ||
| 165 | size = get_indent_level_bytes_right(options, &view->cursor); | ||
| 166 | } | ||
| 167 | if (size == 0) { | ||
| 168 | BlockIter bi = view->cursor; | ||
| 169 | size = block_iter_next_column(&bi); | ||
| 170 | } | ||
| 171 | } | ||
| 172 | buffer_delete_bytes(view, size); | ||
| 173 | } | ||
| 174 | |||
| 175 | void erase(View *view) | ||
| 176 | { | ||
| 177 | size_t size = 0; | ||
| 178 | if (view->selection) { | ||
| 179 | size = prepare_selection(view); | ||
| 180 | unselect(view); | ||
| 181 | } else { | ||
| 182 | const LocalOptions *options = &view->buffer->options; | ||
| 183 | begin_change(CHANGE_MERGE_ERASE); | ||
| 184 | if (options->emulate_tab) { | ||
| 185 | size = get_indent_level_bytes_left(options, &view->cursor); | ||
| 186 | block_iter_back_bytes(&view->cursor, size); | ||
| 187 | } | ||
| 188 | if (size == 0) { | ||
| 189 | CodePoint u; | ||
| 190 | size = block_iter_prev_char(&view->cursor, &u); | ||
| 191 | } | ||
| 192 | } | ||
| 193 | buffer_erase_bytes(view, size); | ||
| 194 | } | ||
| 195 | |||
| 196 | // Go to beginning of whitespace (tabs and spaces) under cursor and | ||
| 197 | // return number of whitespace bytes after cursor after moving cursor | ||
| 198 | static size_t goto_beginning_of_whitespace(View *view) | ||
| 199 | { | ||
| 200 | BlockIter bi = view->cursor; | ||
| 201 | size_t count = 0; | ||
| 202 | CodePoint u; | ||
| 203 | |||
| 204 | // Count spaces and tabs at or after cursor | ||
| 205 | while (block_iter_next_char(&bi, &u)) { | ||
| 206 | if (u != '\t' && u != ' ') { | ||
| 207 | break; | ||
| 208 | } | ||
| 209 | count++; | ||
| 210 | } | ||
| 211 | |||
| 212 | // Count spaces and tabs before cursor | ||
| 213 | while (block_iter_prev_char(&view->cursor, &u)) { | ||
| 214 | if (u != '\t' && u != ' ') { | ||
| 215 | block_iter_next_char(&view->cursor, &u); | ||
| 216 | break; | ||
| 217 | } | ||
| 218 | count++; | ||
| 219 | } | ||
| 220 | return count; | ||
| 221 | } | ||
| 222 | |||
| 223 | static bool ws_only(const StringView *line) | ||
| 224 | { | ||
| 225 | for (size_t i = 0, n = line->length; i < n; i++) { | ||
| 226 | char ch = line->data[i]; | ||
| 227 | if (ch != ' ' && ch != '\t') { | ||
| 228 | return false; | ||
| 229 | } | ||
| 230 | } | ||
| 231 | return true; | ||
| 232 | } | ||
| 233 | |||
| 234 | // Non-empty line can be used to determine size of indentation for the next line | ||
| 235 | static bool find_non_empty_line_bwd(BlockIter *bi) | ||
| 236 | { | ||
| 237 | block_iter_bol(bi); | ||
| 238 | do { | ||
| 239 | StringView line; | ||
| 240 | fill_line_ref(bi, &line); | ||
| 241 | if (!ws_only(&line)) { | ||
| 242 | return true; | ||
| 243 | } | ||
| 244 | } while (block_iter_prev_line(bi)); | ||
| 245 | return false; | ||
| 246 | } | ||
| 247 | |||
| 248 | static void insert_nl(View *view) | ||
| 249 | { | ||
| 250 | size_t del_count = 0; | ||
| 251 | size_t ins_count = 1; | ||
| 252 | char *ins = NULL; | ||
| 253 | |||
| 254 | // Prepare deleted text (selection or whitespace around cursor) | ||
| 255 | if (view->selection) { | ||
| 256 | del_count = prepare_selection(view); | ||
| 257 | unselect(view); | ||
| 258 | } else { | ||
| 259 | // Trim whitespace around cursor | ||
| 260 | del_count = goto_beginning_of_whitespace(view); | ||
| 261 | } | ||
| 262 | |||
| 263 | // Prepare inserted indentation | ||
| 264 | const LocalOptions *options = &view->buffer->options; | ||
| 265 | if (options->auto_indent) { | ||
| 266 | // Current line will be split at cursor position | ||
| 267 | BlockIter bi = view->cursor; | ||
| 268 | size_t len = block_iter_bol(&bi); | ||
| 269 | StringView line; | ||
| 270 | fill_line_ref(&bi, &line); | ||
| 271 | line.length = len; | ||
| 272 | if (ws_only(&line)) { | ||
| 273 | // This line is (or will become) white space only; find previous, | ||
| 274 | // non whitespace only line | ||
| 275 | if (block_iter_prev_line(&bi) && find_non_empty_line_bwd(&bi)) { | ||
| 276 | fill_line_ref(&bi, &line); | ||
| 277 | ins = get_indent_for_next_line(options, &line); | ||
| 278 | } | ||
| 279 | } else { | ||
| 280 | ins = get_indent_for_next_line(options, &line); | ||
| 281 | } | ||
| 282 | } | ||
| 283 | |||
| 284 | begin_change(CHANGE_MERGE_NONE); | ||
| 285 | if (ins) { | ||
| 286 | // Add newline before indent | ||
| 287 | ins_count = strlen(ins); | ||
| 288 | memmove(ins + 1, ins, ins_count); | ||
| 289 | ins[0] = '\n'; | ||
| 290 | ins_count++; | ||
| 291 | |||
| 292 | buffer_replace_bytes(view, del_count, ins, ins_count); | ||
| 293 | free(ins); | ||
| 294 | } else { | ||
| 295 | buffer_replace_bytes(view, del_count, "\n", ins_count); | ||
| 296 | } | ||
| 297 | end_change(); | ||
| 298 | |||
| 299 | // Move after inserted text | ||
| 300 | block_iter_skip_bytes(&view->cursor, ins_count); | ||
| 301 | } | ||
| 302 | |||
| 303 | void insert_ch(View *view, CodePoint ch) | ||
| 304 | { | ||
| 305 | if (ch == '\n') { | ||
| 306 | insert_nl(view); | ||
| 307 | return; | ||
| 308 | } | ||
| 309 | |||
| 310 | const Buffer *buffer = view->buffer; | ||
| 311 | const LocalOptions *options = &buffer->options; | ||
| 312 | char buf[8]; | ||
| 313 | char *ins = buf; | ||
| 314 | char *alloc = NULL; | ||
| 315 | size_t del_count = 0; | ||
| 316 | size_t ins_count = 0; | ||
| 317 | |||
| 318 | if (view->selection) { | ||
| 319 | // Prepare deleted text (selection) | ||
| 320 | del_count = prepare_selection(view); | ||
| 321 | unselect(view); | ||
| 322 | } else if (options->overwrite) { | ||
| 323 | // Delete character under cursor unless we're at end of line | ||
| 324 | BlockIter bi = view->cursor; | ||
| 325 | del_count = block_iter_is_eol(&bi) ? 0 : block_iter_next_column(&bi); | ||
| 326 | } else if (ch == '}' && options->auto_indent && options->brace_indent) { | ||
| 327 | BlockIter bi = view->cursor; | ||
| 328 | StringView curlr; | ||
| 329 | block_iter_bol(&bi); | ||
| 330 | fill_line_ref(&bi, &curlr); | ||
| 331 | if (ws_only(&curlr)) { | ||
| 332 | int width = get_indent_of_matching_brace(view); | ||
| 333 | if (width >= 0) { | ||
| 334 | // Replace current (ws only) line with some indent + '}' | ||
| 335 | block_iter_bol(&view->cursor); | ||
| 336 | del_count = curlr.length; | ||
| 337 | if (width) { | ||
| 338 | alloc = make_indent(options, width); | ||
| 339 | ins = alloc; | ||
| 340 | ins_count = strlen(ins); | ||
| 341 | // '}' will be replace the terminating NUL | ||
| 342 | } | ||
| 343 | } | ||
| 344 | } | ||
| 345 | } | ||
| 346 | |||
| 347 | // Prepare inserted text | ||
| 348 | if (ch == '\t' && options->expand_tab) { | ||
| 349 | ins_count = options->indent_width; | ||
| 350 | static_assert(sizeof(buf) >= INDENT_WIDTH_MAX); | ||
| 351 | memset(ins, ' ', ins_count); | ||
| 352 | } else { | ||
| 353 | u_set_char_raw(ins, &ins_count, ch); | ||
| 354 | } | ||
| 355 | |||
| 356 | // Record change | ||
| 357 | begin_change(del_count ? CHANGE_MERGE_NONE : CHANGE_MERGE_INSERT); | ||
| 358 | buffer_replace_bytes(view, del_count, ins, ins_count); | ||
| 359 | end_change(); | ||
| 360 | free(alloc); | ||
| 361 | |||
| 362 | // Move after inserted text | ||
| 363 | block_iter_skip_bytes(&view->cursor, ins_count); | ||
| 364 | } | ||
| 365 | |||
| 366 | static void join_selection(View *view) | ||
| 367 | { | ||
| 368 | size_t count = prepare_selection(view); | ||
| 369 | size_t len = 0, join = 0; | ||
| 370 | BlockIter bi; | ||
| 371 | CodePoint ch = 0; | ||
| 372 | |||
| 373 | unselect(view); | ||
| 374 | bi = view->cursor; | ||
| 375 | |||
| 376 | begin_change_chain(); | ||
| 377 | while (count > 0) { | ||
| 378 | if (!len) { | ||
| 379 | view->cursor = bi; | ||
| 380 | } | ||
| 381 | |||
| 382 | count -= block_iter_next_char(&bi, &ch); | ||
| 383 | if (ch == '\t' || ch == ' ') { | ||
| 384 | len++; | ||
| 385 | } else if (ch == '\n') { | ||
| 386 | len++; | ||
| 387 | join++; | ||
| 388 | } else { | ||
| 389 | if (join) { | ||
| 390 | buffer_replace_bytes(view, len, " ", 1); | ||
| 391 | // Skip the space we inserted and the char we read last | ||
| 392 | block_iter_next_char(&view->cursor, &ch); | ||
| 393 | block_iter_next_char(&view->cursor, &ch); | ||
| 394 | bi = view->cursor; | ||
| 395 | } | ||
| 396 | len = 0; | ||
| 397 | join = 0; | ||
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 401 | // Don't replace last \n that is at end of the selection | ||
| 402 | if (join && ch == '\n') { | ||
| 403 | join--; | ||
| 404 | len--; | ||
| 405 | } | ||
| 406 | |||
| 407 | if (join) { | ||
| 408 | if (ch == '\n') { | ||
| 409 | // Don't add space to end of line | ||
| 410 | buffer_delete_bytes(view, len); | ||
| 411 | } else { | ||
| 412 | buffer_replace_bytes(view, len, " ", 1); | ||
| 413 | } | ||
| 414 | } | ||
| 415 | end_change_chain(view); | ||
| 416 | } | ||
| 417 | |||
| 418 | void join_lines(View *view) | ||
| 419 | { | ||
| 420 | BlockIter bi = view->cursor; | ||
| 421 | |||
| 422 | if (view->selection) { | ||
| 423 | join_selection(view); | ||
| 424 | return; | ||
| 425 | } | ||
| 426 | |||
| 427 | if (!block_iter_next_line(&bi)) { | ||
| 428 | return; | ||
| 429 | } | ||
| 430 | if (block_iter_is_eof(&bi)) { | ||
| 431 | return; | ||
| 432 | } | ||
| 433 | |||
| 434 | BlockIter next = bi; | ||
| 435 | CodePoint u; | ||
| 436 | size_t count = 1; | ||
| 437 | block_iter_prev_char(&bi, &u); | ||
| 438 | while (block_iter_prev_char(&bi, &u)) { | ||
| 439 | if (u != '\t' && u != ' ') { | ||
| 440 | block_iter_next_char(&bi, &u); | ||
| 441 | break; | ||
| 442 | } | ||
| 443 | count++; | ||
| 444 | } | ||
| 445 | while (block_iter_next_char(&next, &u)) { | ||
| 446 | if (u != '\t' && u != ' ') { | ||
| 447 | break; | ||
| 448 | } | ||
| 449 | count++; | ||
| 450 | } | ||
| 451 | |||
| 452 | view->cursor = bi; | ||
| 453 | if (u == '\n') { | ||
| 454 | buffer_delete_bytes(view, count); | ||
| 455 | } else { | ||
| 456 | buffer_replace_bytes(view, count, " ", 1); | ||
| 457 | } | ||
| 458 | } | ||
| 459 | |||
| 460 | void clear_lines(View *view, bool auto_indent) | ||
| 461 | { | ||
| 462 | char *indent = NULL; | ||
| 463 | if (auto_indent) { | ||
| 464 | BlockIter bi = view->cursor; | ||
| 465 | if (block_iter_prev_line(&bi) && find_non_empty_line_bwd(&bi)) { | ||
| 466 | StringView line; | ||
| 467 | fill_line_ref(&bi, &line); | ||
| 468 | indent = get_indent_for_next_line(&view->buffer->options, &line); | ||
| 469 | } | ||
| 470 | } | ||
| 471 | |||
| 472 | size_t del_count = 0; | ||
| 473 | if (view->selection) { | ||
| 474 | view->selection = SELECT_LINES; | ||
| 475 | del_count = prepare_selection(view); | ||
| 476 | unselect(view); | ||
| 477 | // Don't delete last newline | ||
| 478 | if (del_count) { | ||
| 479 | del_count--; | ||
| 480 | } | ||
| 481 | } else { | ||
| 482 | block_iter_eol(&view->cursor); | ||
| 483 | del_count = block_iter_bol(&view->cursor); | ||
| 484 | } | ||
| 485 | |||
| 486 | if (!indent && !del_count) { | ||
| 487 | return; | ||
| 488 | } | ||
| 489 | |||
| 490 | size_t ins_count = indent ? strlen(indent) : 0; | ||
| 491 | buffer_replace_bytes(view, del_count, indent, ins_count); | ||
| 492 | free(indent); | ||
| 493 | block_iter_skip_bytes(&view->cursor, ins_count); | ||
| 494 | } | ||
| 495 | |||
| 496 | void delete_lines(View *view) | ||
| 497 | { | ||
| 498 | long x = view_get_preferred_x(view); | ||
| 499 | size_t del_count; | ||
| 500 | if (view->selection) { | ||
| 501 | view->selection = SELECT_LINES; | ||
| 502 | del_count = prepare_selection(view); | ||
| 503 | unselect(view); | ||
| 504 | } else { | ||
| 505 | block_iter_bol(&view->cursor); | ||
| 506 | BlockIter tmp = view->cursor; | ||
| 507 | del_count = block_iter_eat_line(&tmp); | ||
| 508 | } | ||
| 509 | buffer_delete_bytes(view, del_count); | ||
| 510 | move_to_preferred_x(view, x); | ||
| 511 | } | ||
| 512 | |||
| 513 | void new_line(View *view, bool above) | ||
| 514 | { | ||
| 515 | if (above && block_iter_prev_line(&view->cursor) == 0) { | ||
| 516 | // Already on first line; insert newline at bof | ||
| 517 | block_iter_bol(&view->cursor); | ||
| 518 | buffer_insert_bytes(view, "\n", 1); | ||
| 519 | return; | ||
| 520 | } | ||
| 521 | |||
| 522 | const LocalOptions *options = &view->buffer->options; | ||
| 523 | char *ins = NULL; | ||
| 524 | block_iter_eol(&view->cursor); | ||
| 525 | |||
| 526 | if (options->auto_indent) { | ||
| 527 | BlockIter bi = view->cursor; | ||
| 528 | if (find_non_empty_line_bwd(&bi)) { | ||
| 529 | StringView line; | ||
| 530 | fill_line_ref(&bi, &line); | ||
| 531 | ins = get_indent_for_next_line(options, &line); | ||
| 532 | } | ||
| 533 | } | ||
| 534 | |||
| 535 | size_t ins_count; | ||
| 536 | if (ins) { | ||
| 537 | ins_count = strlen(ins); | ||
| 538 | memmove(ins + 1, ins, ins_count); | ||
| 539 | ins[0] = '\n'; | ||
| 540 | ins_count++; | ||
| 541 | buffer_insert_bytes(view, ins, ins_count); | ||
| 542 | free(ins); | ||
| 543 | } else { | ||
| 544 | ins_count = 1; | ||
| 545 | buffer_insert_bytes(view, "\n", 1); | ||
| 546 | } | ||
| 547 | |||
| 548 | block_iter_skip_bytes(&view->cursor, ins_count); | ||
| 549 | } | ||
| 550 | |||
| 551 | static void add_word(ParagraphFormatter *pf, const char *word, size_t len) | ||
| 552 | { | ||
| 553 | size_t i = 0; | ||
| 554 | size_t word_width = 0; | ||
| 555 | while (i < len) { | ||
| 556 | word_width += u_char_width(u_get_char(word, len, &i)); | ||
| 557 | } | ||
| 558 | |||
| 559 | if (pf->cur_width && pf->cur_width + 1 + word_width > pf->text_width) { | ||
| 560 | string_append_byte(&pf->buf, '\n'); | ||
| 561 | pf->cur_width = 0; | ||
| 562 | } | ||
| 563 | |||
| 564 | if (pf->cur_width == 0) { | ||
| 565 | if (pf->indent_len) { | ||
| 566 | string_append_buf(&pf->buf, pf->indent, pf->indent_len); | ||
| 567 | } | ||
| 568 | pf->cur_width = pf->indent_width; | ||
| 569 | } else { | ||
| 570 | string_append_byte(&pf->buf, ' '); | ||
| 571 | pf->cur_width++; | ||
| 572 | } | ||
| 573 | |||
| 574 | string_append_buf(&pf->buf, word, len); | ||
| 575 | pf->cur_width += word_width; | ||
| 576 | } | ||
| 577 | |||
| 578 | static bool is_paragraph_separator(const StringView *line) | ||
| 579 | { | ||
| 580 | StringView trimmed = *line; | ||
| 581 | strview_trim(&trimmed); | ||
| 582 | |||
| 583 | return | ||
| 584 | trimmed.length == 0 | ||
| 585 | // TODO: make this configurable | ||
| 586 | || strview_equal_cstring(&trimmed, "/*") | ||
| 587 | || strview_equal_cstring(&trimmed, "*/") | ||
| 588 | ; | ||
| 589 | } | ||
| 590 | |||
| 591 | static bool in_paragraph(const LocalOptions *options, const StringView *line, size_t indent_width) | ||
| 592 | { | ||
| 593 | if (get_indent_width(options, line) != indent_width) { | ||
| 594 | return false; | ||
| 595 | } | ||
| 596 | return !is_paragraph_separator(line); | ||
| 597 | } | ||
| 598 | |||
| 599 | static size_t paragraph_size(View *view) | ||
| 600 | { | ||
| 601 | const LocalOptions *options = &view->buffer->options; | ||
| 602 | BlockIter bi = view->cursor; | ||
| 603 | StringView line; | ||
| 604 | block_iter_bol(&bi); | ||
| 605 | fill_line_ref(&bi, &line); | ||
| 606 | if (is_paragraph_separator(&line)) { | ||
| 607 | // Not in paragraph | ||
| 608 | return 0; | ||
| 609 | } | ||
| 610 | size_t indent_width = get_indent_width(options, &line); | ||
| 611 | |||
| 612 | // Go to beginning of paragraph | ||
| 613 | while (block_iter_prev_line(&bi)) { | ||
| 614 | fill_line_ref(&bi, &line); | ||
| 615 | if (!in_paragraph(options, &line, indent_width)) { | ||
| 616 | block_iter_eat_line(&bi); | ||
| 617 | break; | ||
| 618 | } | ||
| 619 | } | ||
| 620 | view->cursor = bi; | ||
| 621 | |||
| 622 | // Get size of paragraph | ||
| 623 | size_t size = 0; | ||
| 624 | do { | ||
| 625 | size_t bytes = block_iter_eat_line(&bi); | ||
| 626 | if (!bytes) { | ||
| 627 | break; | ||
| 628 | } | ||
| 629 | size += bytes; | ||
| 630 | fill_line_ref(&bi, &line); | ||
| 631 | } while (in_paragraph(options, &line, indent_width)); | ||
| 632 | return size; | ||
| 633 | } | ||
| 634 | |||
| 635 | void format_paragraph(View *view, size_t text_width) | ||
| 636 | { | ||
| 637 | size_t len; | ||
| 638 | if (view->selection) { | ||
| 639 | view->selection = SELECT_LINES; | ||
| 640 | len = prepare_selection(view); | ||
| 641 | } else { | ||
| 642 | len = paragraph_size(view); | ||
| 643 | } | ||
| 644 | if (!len) { | ||
| 645 | return; | ||
| 646 | } | ||
| 647 | |||
| 648 | const LocalOptions *options = &view->buffer->options; | ||
| 649 | char *sel = block_iter_get_bytes(&view->cursor, len); | ||
| 650 | StringView sv = string_view(sel, len); | ||
| 651 | size_t indent_width = get_indent_width(options, &sv); | ||
| 652 | char *indent = make_indent(options, indent_width); | ||
| 653 | |||
| 654 | ParagraphFormatter pf = { | ||
| 655 | .buf = STRING_INIT, | ||
| 656 | .indent = indent, | ||
| 657 | .indent_len = indent ? strlen(indent) : 0, | ||
| 658 | .indent_width = indent_width, | ||
| 659 | .cur_width = 0, | ||
| 660 | .text_width = text_width | ||
| 661 | }; | ||
| 662 | |||
| 663 | for (size_t i = 0; true; ) { | ||
| 664 | while (i < len) { | ||
| 665 | size_t tmp = i; | ||
| 666 | if (!u_is_breakable_whitespace(u_get_char(sel, len, &tmp))) { | ||
| 667 | break; | ||
| 668 | } | ||
| 669 | i = tmp; | ||
| 670 | } | ||
| 671 | if (i == len) { | ||
| 672 | break; | ||
| 673 | } | ||
| 674 | |||
| 675 | size_t start = i; | ||
| 676 | while (i < len) { | ||
| 677 | size_t tmp = i; | ||
| 678 | if (u_is_breakable_whitespace(u_get_char(sel, len, &tmp))) { | ||
| 679 | break; | ||
| 680 | } | ||
| 681 | i = tmp; | ||
| 682 | } | ||
| 683 | |||
| 684 | add_word(&pf, sel + start, i - start); | ||
| 685 | } | ||
| 686 | |||
| 687 | if (pf.buf.len) { | ||
| 688 | string_append_byte(&pf.buf, '\n'); | ||
| 689 | } | ||
| 690 | buffer_replace_bytes(view, len, pf.buf.buffer, pf.buf.len); | ||
| 691 | if (pf.buf.len) { | ||
| 692 | block_iter_skip_bytes(&view->cursor, pf.buf.len - 1); | ||
| 693 | } | ||
| 694 | string_free(&pf.buf); | ||
| 695 | free(pf.indent); | ||
| 696 | free(sel); | ||
| 697 | |||
| 698 | unselect(view); | ||
| 699 | } | ||
| 700 | |||
| 701 | void change_case(View *view, char mode) | ||
| 702 | { | ||
| 703 | bool was_selecting = false; | ||
| 704 | bool move = true; | ||
| 705 | size_t text_len; | ||
| 706 | if (view->selection) { | ||
| 707 | SelectionInfo info; | ||
| 708 | init_selection(view, &info); | ||
| 709 | view->cursor = info.si; | ||
| 710 | text_len = info.eo - info.so; | ||
| 711 | unselect(view); | ||
| 712 | was_selecting = true; | ||
| 713 | move = !info.swapped; | ||
| 714 | } else { | ||
| 715 | CodePoint u; | ||
| 716 | if (!block_iter_get_char(&view->cursor, &u)) { | ||
| 717 | return; | ||
| 718 | } | ||
| 719 | text_len = u_char_size(u); | ||
| 720 | } | ||
| 721 | |||
| 722 | String dst = string_new(text_len); | ||
| 723 | char *src = block_iter_get_bytes(&view->cursor, text_len); | ||
| 724 | size_t i = 0; | ||
| 725 | switch (mode) { | ||
| 726 | case 'l': | ||
| 727 | while (i < text_len) { | ||
| 728 | CodePoint u = u_to_lower(u_get_char(src, text_len, &i)); | ||
| 729 | string_append_codepoint(&dst, u); | ||
| 730 | } | ||
| 731 | break; | ||
| 732 | case 'u': | ||
| 733 | while (i < text_len) { | ||
| 734 | CodePoint u = u_to_upper(u_get_char(src, text_len, &i)); | ||
| 735 | string_append_codepoint(&dst, u); | ||
| 736 | } | ||
| 737 | break; | ||
| 738 | case 't': | ||
| 739 | while (i < text_len) { | ||
| 740 | CodePoint u = u_get_char(src, text_len, &i); | ||
| 741 | u = u_is_upper(u) ? u_to_lower(u) : u_to_upper(u); | ||
| 742 | string_append_codepoint(&dst, u); | ||
| 743 | } | ||
| 744 | break; | ||
| 745 | default: | ||
| 746 | BUG("unhandled case mode"); | ||
| 747 | } | ||
| 748 | |||
| 749 | buffer_replace_bytes(view, text_len, dst.buffer, dst.len); | ||
| 750 | free(src); | ||
| 751 | |||
| 752 | if (move && dst.len > 0) { | ||
| 753 | if (was_selecting) { | ||
| 754 | // Move cursor back to where it was | ||
| 755 | size_t idx = dst.len; | ||
| 756 | u_prev_char(dst.buffer, &idx); | ||
| 757 | block_iter_skip_bytes(&view->cursor, idx); | ||
| 758 | } else { | ||
| 759 | block_iter_skip_bytes(&view->cursor, dst.len); | ||
| 760 | } | ||
| 761 | } | ||
| 762 | |||
| 763 | string_free(&dst); | ||
| 764 | } | ||
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 @@ | |||
| 1 | #ifndef MISC_H | ||
| 2 | #define MISC_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "util/unicode.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | void select_block(View *view); | ||
| 10 | void unselect(View *view); | ||
| 11 | void insert_text(View *view, const char *text, size_t size, bool move_after); | ||
| 12 | void delete_ch(View *view); | ||
| 13 | void erase(View *view); | ||
| 14 | void insert_ch(View *view, CodePoint ch); | ||
| 15 | void join_lines(View *view); | ||
| 16 | void clear_lines(View *view, bool auto_indent); | ||
| 17 | void delete_lines(View *view); | ||
| 18 | void new_line(View *view, bool above); | ||
| 19 | void format_paragraph(View *view, size_t text_width); | ||
| 20 | void change_case(View *view, char mode); | ||
| 21 | |||
| 22 | #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 @@ | |||
| 1 | #include "mode.h" | ||
| 2 | #include "bind.h" | ||
| 3 | #include "change.h" | ||
| 4 | #include "cmdline.h" | ||
| 5 | #include "command/macro.h" | ||
| 6 | #include "completion.h" | ||
| 7 | #include "misc.h" | ||
| 8 | #include "shift.h" | ||
| 9 | #include "terminal/input.h" | ||
| 10 | #include "util/debug.h" | ||
| 11 | #include "util/unicode.h" | ||
| 12 | #include "view.h" | ||
| 13 | |||
| 14 | static bool normal_mode_keypress(EditorState *e, KeyCode key) | ||
| 15 | { | ||
| 16 | View *view = e->view; | ||
| 17 | KeyCode shift = key & MOD_SHIFT; | ||
| 18 | if ((key & ~shift) == KEY_TAB && view->selection == SELECT_LINES) { | ||
| 19 | // In line selections, Tab/S-Tab behave like `shift -- 1/-1` | ||
| 20 | shift_lines(view, shift ? -1 : 1); | ||
| 21 | return true; | ||
| 22 | } | ||
| 23 | |||
| 24 | if (u_is_unicode(key)) { | ||
| 25 | insert_ch(view, key); | ||
| 26 | macro_insert_char_hook(&e->macro, key); | ||
| 27 | return true; | ||
| 28 | } | ||
| 29 | |||
| 30 | return handle_binding(e, INPUT_NORMAL, key); | ||
| 31 | } | ||
| 32 | |||
| 33 | static bool insert_paste(EditorState *e, bool bracketed) | ||
| 34 | { | ||
| 35 | String str = term_read_paste(&e->terminal.ibuf, bracketed); | ||
| 36 | if (e->input_mode == INPUT_NORMAL) { | ||
| 37 | begin_change(CHANGE_MERGE_NONE); | ||
| 38 | insert_text(e->view, str.buffer, str.len, true); | ||
| 39 | end_change(); | ||
| 40 | macro_insert_text_hook(&e->macro, str.buffer, str.len); | ||
| 41 | } else { | ||
| 42 | CommandLine *c = &e->cmdline; | ||
| 43 | string_replace_byte(&str, '\n', ' '); | ||
| 44 | string_insert_buf(&c->buf, c->pos, str.buffer, str.len); | ||
| 45 | c->pos += str.len; | ||
| 46 | c->search_pos = NULL; | ||
| 47 | } | ||
| 48 | string_free(&str); | ||
| 49 | return true; | ||
| 50 | } | ||
| 51 | |||
| 52 | bool handle_input(EditorState *e, KeyCode key) | ||
| 53 | { | ||
| 54 | if (key == KEY_DETECTED_PASTE || key == KEY_BRACKETED_PASTE) { | ||
| 55 | return insert_paste(e, key == KEY_BRACKETED_PASTE); | ||
| 56 | } | ||
| 57 | |||
| 58 | InputMode mode = e->input_mode; | ||
| 59 | if (mode == INPUT_NORMAL) { | ||
| 60 | return normal_mode_keypress(e, key); | ||
| 61 | } | ||
| 62 | |||
| 63 | BUG_ON(!(mode == INPUT_COMMAND || mode == INPUT_SEARCH)); | ||
| 64 | if (!u_is_unicode(key) || key == KEY_TAB || key == KEY_ENTER) { | ||
| 65 | return handle_binding(e, mode, key); | ||
| 66 | } | ||
| 67 | |||
| 68 | CommandLine *c = &e->cmdline; | ||
| 69 | c->pos += string_insert_codepoint(&c->buf, c->pos, key); | ||
| 70 | if (mode == INPUT_COMMAND) { | ||
| 71 | reset_completion(c); | ||
| 72 | } | ||
| 73 | return true; | ||
| 74 | } | ||
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 @@ | |||
| 1 | #ifndef MODE_H | ||
| 2 | #define MODE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "editor.h" | ||
| 6 | #include "terminal/key.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | |||
| 9 | bool handle_input(EditorState *e, KeyCode key) NONNULL_ARGS; | ||
| 10 | |||
| 11 | #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 @@ | |||
| 1 | #include "move.h" | ||
| 2 | #include "buffer.h" | ||
| 3 | #include "indent.h" | ||
| 4 | #include "util/ascii.h" | ||
| 5 | #include "util/debug.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/utf8.h" | ||
| 8 | |||
| 9 | typedef enum { | ||
| 10 | CT_SPACE, | ||
| 11 | CT_NEWLINE, | ||
| 12 | CT_WORD, | ||
| 13 | CT_OTHER, | ||
| 14 | } CharTypeEnum; | ||
| 15 | |||
| 16 | void move_to_preferred_x(View *view, long preferred_x) | ||
| 17 | { | ||
| 18 | const LocalOptions *options = &view->buffer->options; | ||
| 19 | StringView line; | ||
| 20 | view->preferred_x = preferred_x; | ||
| 21 | block_iter_bol(&view->cursor); | ||
| 22 | fill_line_ref(&view->cursor, &line); | ||
| 23 | |||
| 24 | if (options->emulate_tab && view->preferred_x < line.length) { | ||
| 25 | const size_t iw = options->indent_width; | ||
| 26 | const size_t ilevel = indent_level(view->preferred_x, iw); | ||
| 27 | for (size_t i = 0; i < line.length && line.data[i] == ' '; i++) { | ||
| 28 | if (i + 1 == (ilevel + 1) * iw) { | ||
| 29 | // Force cursor to beginning of the indentation level | ||
| 30 | view->cursor.offset += ilevel * iw; | ||
| 31 | return; | ||
| 32 | } | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | const unsigned int tw = options->tab_width; | ||
| 37 | unsigned long x = 0; | ||
| 38 | size_t i = 0; | ||
| 39 | while (x < view->preferred_x && i < line.length) { | ||
| 40 | CodePoint u = line.data[i++]; | ||
| 41 | if (likely(u < 0x80)) { | ||
| 42 | if (likely(!ascii_iscntrl(u))) { | ||
| 43 | x++; | ||
| 44 | } else if (u == '\t') { | ||
| 45 | x = next_indent_width(x, tw); | ||
| 46 | } else if (u == '\n') { | ||
| 47 | break; | ||
| 48 | } else { | ||
| 49 | x += 2; | ||
| 50 | } | ||
| 51 | } else { | ||
| 52 | const size_t next = i; | ||
| 53 | i--; | ||
| 54 | u = u_get_nonascii(line.data, line.length, &i); | ||
| 55 | x += u_char_width(u); | ||
| 56 | if (x > view->preferred_x) { | ||
| 57 | i = next; | ||
| 58 | break; | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | if (x > view->preferred_x) { | ||
| 63 | i--; | ||
| 64 | } | ||
| 65 | view->cursor.offset += i; | ||
| 66 | |||
| 67 | // If cursor stopped on a zero-width char, move to the next spacing char | ||
| 68 | CodePoint u; | ||
| 69 | if (block_iter_get_char(&view->cursor, &u) && u_is_zero_width(u)) { | ||
| 70 | block_iter_next_column(&view->cursor); | ||
| 71 | } | ||
| 72 | } | ||
| 73 | |||
| 74 | void move_cursor_left(View *view) | ||
| 75 | { | ||
| 76 | const LocalOptions *options = &view->buffer->options; | ||
| 77 | if (options->emulate_tab) { | ||
| 78 | size_t size = get_indent_level_bytes_left(options, &view->cursor); | ||
| 79 | if (size) { | ||
| 80 | block_iter_back_bytes(&view->cursor, size); | ||
| 81 | view_reset_preferred_x(view); | ||
| 82 | return; | ||
| 83 | } | ||
| 84 | } | ||
| 85 | block_iter_prev_column(&view->cursor); | ||
| 86 | view_reset_preferred_x(view); | ||
| 87 | } | ||
| 88 | |||
| 89 | void move_cursor_right(View *view) | ||
| 90 | { | ||
| 91 | const LocalOptions *options = &view->buffer->options; | ||
| 92 | if (options->emulate_tab) { | ||
| 93 | size_t size = get_indent_level_bytes_right(options, &view->cursor); | ||
| 94 | if (size) { | ||
| 95 | block_iter_skip_bytes(&view->cursor, size); | ||
| 96 | view_reset_preferred_x(view); | ||
| 97 | return; | ||
| 98 | } | ||
| 99 | } | ||
| 100 | block_iter_next_column(&view->cursor); | ||
| 101 | view_reset_preferred_x(view); | ||
| 102 | } | ||
| 103 | |||
| 104 | void move_bol(View *view) | ||
| 105 | { | ||
| 106 | block_iter_bol(&view->cursor); | ||
| 107 | view_reset_preferred_x(view); | ||
| 108 | } | ||
| 109 | |||
| 110 | void move_bol_smart(View *view, SmartBolFlags flags) | ||
| 111 | { | ||
| 112 | if (flags == 0) { | ||
| 113 | move_bol(view); | ||
| 114 | return; | ||
| 115 | } | ||
| 116 | |||
| 117 | BUG_ON(!(flags & BOL_SMART)); | ||
| 118 | bool fwd = false; | ||
| 119 | StringView line; | ||
| 120 | size_t cursor_offset = fetch_this_line(&view->cursor, &line); | ||
| 121 | |||
| 122 | if (cursor_offset == 0) { | ||
| 123 | // Already at bol | ||
| 124 | if (!(flags & BOL_SMART_TOGGLE)) { | ||
| 125 | goto out; | ||
| 126 | } | ||
| 127 | fwd = true; | ||
| 128 | } | ||
| 129 | |||
| 130 | size_t indent = ascii_blank_prefix_length(line.data, line.length); | ||
| 131 | if (fwd) { | ||
| 132 | block_iter_skip_bytes(&view->cursor, indent); | ||
| 133 | } else { | ||
| 134 | size_t co = cursor_offset; | ||
| 135 | size_t move = (co > indent) ? co - indent : co; | ||
| 136 | block_iter_back_bytes(&view->cursor, move); | ||
| 137 | } | ||
| 138 | |||
| 139 | out: | ||
| 140 | view_reset_preferred_x(view); | ||
| 141 | } | ||
| 142 | |||
| 143 | void move_eol(View *view) | ||
| 144 | { | ||
| 145 | block_iter_eol(&view->cursor); | ||
| 146 | view_reset_preferred_x(view); | ||
| 147 | } | ||
| 148 | |||
| 149 | void move_up(View *view, long count) | ||
| 150 | { | ||
| 151 | const long x = view_get_preferred_x(view); | ||
| 152 | while (count > 0) { | ||
| 153 | if (!block_iter_prev_line(&view->cursor)) { | ||
| 154 | break; | ||
| 155 | } | ||
| 156 | count--; | ||
| 157 | } | ||
| 158 | move_to_preferred_x(view, x); | ||
| 159 | } | ||
| 160 | |||
| 161 | void move_down(View *view, long count) | ||
| 162 | { | ||
| 163 | const long x = view_get_preferred_x(view); | ||
| 164 | while (count > 0) { | ||
| 165 | if (!block_iter_eat_line(&view->cursor)) { | ||
| 166 | break; | ||
| 167 | } | ||
| 168 | count--; | ||
| 169 | } | ||
| 170 | move_to_preferred_x(view, x); | ||
| 171 | } | ||
| 172 | |||
| 173 | void move_bof(View *view) | ||
| 174 | { | ||
| 175 | block_iter_bof(&view->cursor); | ||
| 176 | view_reset_preferred_x(view); | ||
| 177 | } | ||
| 178 | |||
| 179 | void move_eof(View *view) | ||
| 180 | { | ||
| 181 | block_iter_eof(&view->cursor); | ||
| 182 | view_reset_preferred_x(view); | ||
| 183 | } | ||
| 184 | |||
| 185 | void move_to_line(View *view, size_t line) | ||
| 186 | { | ||
| 187 | BUG_ON(line == 0); | ||
| 188 | view->center_on_scroll = true; | ||
| 189 | block_iter_goto_line(&view->cursor, line - 1); | ||
| 190 | } | ||
| 191 | |||
| 192 | void move_to_column(View *view, size_t column) | ||
| 193 | { | ||
| 194 | BUG_ON(column == 0); | ||
| 195 | block_iter_bol(&view->cursor); | ||
| 196 | while (column-- > 1) { | ||
| 197 | CodePoint u; | ||
| 198 | if (!block_iter_next_char(&view->cursor, &u)) { | ||
| 199 | break; | ||
| 200 | } | ||
| 201 | if (u == '\n') { | ||
| 202 | block_iter_prev_char(&view->cursor, &u); | ||
| 203 | break; | ||
| 204 | } | ||
| 205 | } | ||
| 206 | view_reset_preferred_x(view); | ||
| 207 | } | ||
| 208 | |||
| 209 | void move_to_filepos(View *view, size_t line, size_t column) | ||
| 210 | { | ||
| 211 | move_to_line(view, line); | ||
| 212 | BUG_ON(!block_iter_is_bol(&view->cursor)); | ||
| 213 | if (column != 1) { | ||
| 214 | move_to_column(view, column); | ||
| 215 | } | ||
| 216 | view_reset_preferred_x(view); | ||
| 217 | } | ||
| 218 | |||
| 219 | static CharTypeEnum get_char_type(CodePoint u) | ||
| 220 | { | ||
| 221 | if (u == '\n') { | ||
| 222 | return CT_NEWLINE; | ||
| 223 | } | ||
| 224 | if (u_is_breakable_whitespace(u)) { | ||
| 225 | return CT_SPACE; | ||
| 226 | } | ||
| 227 | if (u_is_word_char(u)) { | ||
| 228 | return CT_WORD; | ||
| 229 | } | ||
| 230 | return CT_OTHER; | ||
| 231 | } | ||
| 232 | |||
| 233 | static bool get_current_char_type(BlockIter *bi, CharTypeEnum *type) | ||
| 234 | { | ||
| 235 | CodePoint u; | ||
| 236 | if (!block_iter_get_char(bi, &u)) { | ||
| 237 | return false; | ||
| 238 | } | ||
| 239 | |||
| 240 | *type = get_char_type(u); | ||
| 241 | return true; | ||
| 242 | } | ||
| 243 | |||
| 244 | static size_t skip_fwd_char_type(BlockIter *bi, CharTypeEnum type) | ||
| 245 | { | ||
| 246 | size_t count = 0; | ||
| 247 | CodePoint u; | ||
| 248 | while (block_iter_next_char(bi, &u)) { | ||
| 249 | if (get_char_type(u) != type) { | ||
| 250 | block_iter_prev_char(bi, &u); | ||
| 251 | break; | ||
| 252 | } | ||
| 253 | count += u_char_size(u); | ||
| 254 | } | ||
| 255 | return count; | ||
| 256 | } | ||
| 257 | |||
| 258 | static size_t skip_bwd_char_type(BlockIter *bi, CharTypeEnum type) | ||
| 259 | { | ||
| 260 | size_t count = 0; | ||
| 261 | CodePoint u; | ||
| 262 | while (block_iter_prev_char(bi, &u)) { | ||
| 263 | if (get_char_type(u) != type) { | ||
| 264 | block_iter_next_char(bi, &u); | ||
| 265 | break; | ||
| 266 | } | ||
| 267 | count += u_char_size(u); | ||
| 268 | } | ||
| 269 | return count; | ||
| 270 | } | ||
| 271 | |||
| 272 | size_t word_fwd(BlockIter *bi, bool skip_non_word) | ||
| 273 | { | ||
| 274 | size_t count = 0; | ||
| 275 | CharTypeEnum type; | ||
| 276 | |||
| 277 | while (1) { | ||
| 278 | count += skip_fwd_char_type(bi, CT_SPACE); | ||
| 279 | if (!get_current_char_type(bi, &type)) { | ||
| 280 | return count; | ||
| 281 | } | ||
| 282 | |||
| 283 | if ( | ||
| 284 | count | ||
| 285 | && (!skip_non_word || (type == CT_WORD || type == CT_NEWLINE)) | ||
| 286 | ) { | ||
| 287 | return count; | ||
| 288 | } | ||
| 289 | |||
| 290 | count += skip_fwd_char_type(bi, type); | ||
| 291 | } | ||
| 292 | } | ||
| 293 | |||
| 294 | size_t word_bwd(BlockIter *bi, bool skip_non_word) | ||
| 295 | { | ||
| 296 | size_t count = 0; | ||
| 297 | CharTypeEnum type; | ||
| 298 | CodePoint u; | ||
| 299 | |||
| 300 | do { | ||
| 301 | count += skip_bwd_char_type(bi, CT_SPACE); | ||
| 302 | if (!block_iter_prev_char(bi, &u)) { | ||
| 303 | return count; | ||
| 304 | } | ||
| 305 | |||
| 306 | type = get_char_type(u); | ||
| 307 | count += u_char_size(u); | ||
| 308 | count += skip_bwd_char_type(bi, type); | ||
| 309 | } while (skip_non_word && type != CT_WORD && type != CT_NEWLINE); | ||
| 310 | return count; | ||
| 311 | } | ||
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 @@ | |||
| 1 | #ifndef MOVE_H | ||
| 2 | #define MOVE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "block-iter.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | typedef enum { | ||
| 10 | BOL_SMART = 0x1, // Move to end of indent, before moving to bol (left moves only) | ||
| 11 | BOL_SMART_TOGGLE = 0x2, // Move to end of indent, if at bol (can move right) | ||
| 12 | } SmartBolFlags; | ||
| 13 | |||
| 14 | void move_to_preferred_x(View *view, long preferred_x); | ||
| 15 | void move_cursor_left(View *view); | ||
| 16 | void move_cursor_right(View *view); | ||
| 17 | void move_bol(View *view); | ||
| 18 | void move_bol_smart(View *view, SmartBolFlags flags); | ||
| 19 | void move_eol(View *view); | ||
| 20 | void move_up(View *view, long count); | ||
| 21 | void move_down(View *view, long count); | ||
| 22 | void move_bof(View *view); | ||
| 23 | void move_eof(View *view); | ||
| 24 | void move_to_line(View *view, size_t line); | ||
| 25 | void move_to_column(View *view, size_t column); | ||
| 26 | void move_to_filepos(View *view, size_t line, size_t column); | ||
| 27 | |||
| 28 | size_t word_fwd(BlockIter *bi, bool skip_non_word); | ||
| 29 | size_t word_bwd(BlockIter *bi, bool skip_non_word); | ||
| 30 | |||
| 31 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <unistd.h> | ||
| 4 | #include "msg.h" | ||
| 5 | #include "editor.h" | ||
| 6 | #include "error.h" | ||
| 7 | #include "util/debug.h" | ||
| 8 | #include "util/numtostr.h" | ||
| 9 | #include "util/path.h" | ||
| 10 | #include "util/xmalloc.h" | ||
| 11 | |||
| 12 | static void free_message(Message *m) | ||
| 13 | { | ||
| 14 | if (m->loc) { | ||
| 15 | file_location_free(m->loc); | ||
| 16 | } | ||
| 17 | free(m); | ||
| 18 | } | ||
| 19 | |||
| 20 | Message *new_message(const char *msg, size_t len) | ||
| 21 | { | ||
| 22 | Message *m = xmalloc(sizeof(*m) + len + 1); | ||
| 23 | m->loc = NULL; | ||
| 24 | if (len) { | ||
| 25 | memcpy(m->msg, msg, len); | ||
| 26 | } | ||
| 27 | m->msg[len] = '\0'; | ||
| 28 | return m; | ||
| 29 | } | ||
| 30 | |||
| 31 | void add_message(MessageArray *msgs, Message *m) | ||
| 32 | { | ||
| 33 | ptr_array_append(&msgs->array, m); | ||
| 34 | } | ||
| 35 | |||
| 36 | bool activate_current_message(EditorState *e) | ||
| 37 | { | ||
| 38 | const MessageArray *msgs = &e->messages; | ||
| 39 | size_t count = msgs->array.count; | ||
| 40 | if (count == 0) { | ||
| 41 | return true; | ||
| 42 | } | ||
| 43 | |||
| 44 | size_t pos = msgs->pos; | ||
| 45 | BUG_ON(pos >= count); | ||
| 46 | const Message *m = msgs->array.ptrs[pos]; | ||
| 47 | const FileLocation *loc = m->loc; | ||
| 48 | if (loc && loc->filename && !file_location_go(e->window, loc)) { | ||
| 49 | // Failed to jump to location; error message is visible | ||
| 50 | return false; | ||
| 51 | } | ||
| 52 | |||
| 53 | if (count == 1) { | ||
| 54 | info_msg("%s", m->msg); | ||
| 55 | } else { | ||
| 56 | info_msg("[%zu/%zu] %s", pos + 1, count, m->msg); | ||
| 57 | } | ||
| 58 | |||
| 59 | return true; | ||
| 60 | } | ||
| 61 | |||
| 62 | bool activate_current_message_save(EditorState *e) | ||
| 63 | { | ||
| 64 | const View *view = e->view; | ||
| 65 | const BlockIter save = view->cursor; | ||
| 66 | FileLocation *loc = get_current_file_location(view); | ||
| 67 | bool ok = activate_current_message(e); | ||
| 68 | |||
| 69 | // Save position if file changed or cursor moved | ||
| 70 | view = e->view; | ||
| 71 | if (view->cursor.blk != save.blk || view->cursor.offset != save.offset) { | ||
| 72 | bookmark_push(&e->bookmarks, loc); | ||
| 73 | } else { | ||
| 74 | file_location_free(loc); | ||
| 75 | } | ||
| 76 | |||
| 77 | return ok; | ||
| 78 | } | ||
| 79 | |||
| 80 | void clear_messages(MessageArray *msgs) | ||
| 81 | { | ||
| 82 | msgs->pos = 0; | ||
| 83 | ptr_array_free_cb(&msgs->array, FREE_FUNC(free_message)); | ||
| 84 | } | ||
| 85 | |||
| 86 | String dump_messages(const MessageArray *messages) | ||
| 87 | { | ||
| 88 | String buf = string_new(4096); | ||
| 89 | char cwd[8192]; | ||
| 90 | if (unlikely(!getcwd(cwd, sizeof cwd))) { | ||
| 91 | return buf; | ||
| 92 | } | ||
| 93 | |||
| 94 | for (size_t i = 0, n = messages->array.count; i < n; i++) { | ||
| 95 | char *ptr = string_reserve_space(&buf, DECIMAL_STR_MAX(i)); | ||
| 96 | buf.len += buf_umax_to_str(i + 1, ptr); | ||
| 97 | string_append_literal(&buf, ": "); | ||
| 98 | |||
| 99 | const Message *m = messages->array.ptrs[i]; | ||
| 100 | const FileLocation *loc = m->loc; | ||
| 101 | if (!loc || !loc->filename) { | ||
| 102 | goto append_msg; | ||
| 103 | } | ||
| 104 | |||
| 105 | if (path_is_absolute(loc->filename)) { | ||
| 106 | char *rel = path_relative(loc->filename, cwd); | ||
| 107 | string_append_cstring(&buf, rel); | ||
| 108 | free(rel); | ||
| 109 | } else { | ||
| 110 | string_append_cstring(&buf, loc->filename); | ||
| 111 | } | ||
| 112 | |||
| 113 | string_append_byte(&buf, ':'); | ||
| 114 | |||
| 115 | if (loc->pattern) { | ||
| 116 | string_append_literal(&buf, " /"); | ||
| 117 | string_append_cstring(&buf, loc->pattern); | ||
| 118 | string_append_literal(&buf, "/\n"); | ||
| 119 | continue; | ||
| 120 | } | ||
| 121 | |||
| 122 | if (loc->line != 0) { | ||
| 123 | string_append_cstring(&buf, ulong_to_str(loc->line)); | ||
| 124 | string_append_byte(&buf, ':'); | ||
| 125 | if (loc->column != 0) { | ||
| 126 | string_append_cstring(&buf, ulong_to_str(loc->column)); | ||
| 127 | string_append_byte(&buf, ':'); | ||
| 128 | } | ||
| 129 | } | ||
| 130 | |||
| 131 | string_append_literal(&buf, " "); | ||
| 132 | |||
| 133 | append_msg: | ||
| 134 | string_append_cstring(&buf, m->msg); | ||
| 135 | string_append_byte(&buf, '\n'); | ||
| 136 | } | ||
| 137 | |||
| 138 | return buf; | ||
| 139 | } | ||
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 @@ | |||
| 1 | #ifndef MSG_H | ||
| 2 | #define MSG_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "bookmark.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | #include "util/ptr-array.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | |||
| 11 | typedef struct { | ||
| 12 | FileLocation *loc; | ||
| 13 | char msg[]; | ||
| 14 | } Message; | ||
| 15 | |||
| 16 | typedef struct { | ||
| 17 | PointerArray array; | ||
| 18 | size_t pos; | ||
| 19 | } MessageArray; | ||
| 20 | |||
| 21 | struct EditorState; | ||
| 22 | |||
| 23 | Message *new_message(const char *msg, size_t len) RETURNS_NONNULL; | ||
| 24 | void add_message(MessageArray *msgs, Message *m) NONNULL_ARGS; | ||
| 25 | bool activate_current_message(struct EditorState *e) NONNULL_ARGS; | ||
| 26 | bool activate_current_message_save(struct EditorState *e) NONNULL_ARGS; | ||
| 27 | void clear_messages(MessageArray *msgs) NONNULL_ARGS; | ||
| 28 | String dump_messages(const MessageArray *messages) NONNULL_ARGS; | ||
| 29 | |||
| 30 | #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 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "options.h" | ||
| 5 | #include "buffer.h" | ||
| 6 | #include "command/serialize.h" | ||
| 7 | #include "editor.h" | ||
| 8 | #include "error.h" | ||
| 9 | #include "file-option.h" | ||
| 10 | #include "filetype.h" | ||
| 11 | #include "screen.h" | ||
| 12 | #include "status.h" | ||
| 13 | #include "terminal/output.h" | ||
| 14 | #include "util/bsearch.h" | ||
| 15 | #include "util/debug.h" | ||
| 16 | #include "util/intern.h" | ||
| 17 | #include "util/numtostr.h" | ||
| 18 | #include "util/str-util.h" | ||
| 19 | #include "util/string-view.h" | ||
| 20 | #include "util/strtonum.h" | ||
| 21 | #include "util/xmalloc.h" | ||
| 22 | |||
| 23 | typedef enum { | ||
| 24 | OPT_STR, | ||
| 25 | OPT_UINT, | ||
| 26 | OPT_ENUM, | ||
| 27 | OPT_BOOL, | ||
| 28 | OPT_FLAG, | ||
| 29 | OPT_REGEX, | ||
| 30 | } OptionType; | ||
| 31 | |||
| 32 | typedef union { | ||
| 33 | const char *str_val; // OPT_STR, OPT_REGEX | ||
| 34 | unsigned int uint_val; // OPT_UINT, OPT_ENUM, OPT_FLAG | ||
| 35 | bool bool_val; // OPT_BOOL | ||
| 36 | } OptionValue; | ||
| 37 | |||
| 38 | typedef union { | ||
| 39 | struct {bool (*validate)(const char *value);} str_opt; // OPT_STR (optional) | ||
| 40 | struct {unsigned int min, max;} uint_opt; // OPT_UINT | ||
| 41 | struct {const char *const *values;} enum_opt; // OPT_ENUM, OPT_FLAG, OPT_BOOL | ||
| 42 | } OptionConstraint; | ||
| 43 | |||
| 44 | typedef struct { | ||
| 45 | const char name[22]; | ||
| 46 | bool local; | ||
| 47 | bool global; | ||
| 48 | unsigned int offset; | ||
| 49 | OptionType type; | ||
| 50 | OptionConstraint u; | ||
| 51 | void (*on_change)(EditorState *e, bool global); // Optional | ||
| 52 | } OptionDesc; | ||
| 53 | |||
| 54 | #define STR_OPT(_name, OLG, _validate, _on_change) { \ | ||
| 55 | OLG \ | ||
| 56 | .name = _name, \ | ||
| 57 | .type = OPT_STR, \ | ||
| 58 | .u = {.str_opt = {.validate = _validate}}, \ | ||
| 59 | .on_change = _on_change, \ | ||
| 60 | } | ||
| 61 | |||
| 62 | #define UINT_OPT(_name, OLG, _min, _max, _on_change) { \ | ||
| 63 | OLG \ | ||
| 64 | .name = _name, \ | ||
| 65 | .type = OPT_UINT, \ | ||
| 66 | .u = {.uint_opt = { \ | ||
| 67 | .min = _min, \ | ||
| 68 | .max = _max, \ | ||
| 69 | }}, \ | ||
| 70 | .on_change = _on_change, \ | ||
| 71 | } | ||
| 72 | |||
| 73 | #define ENUM_OPT(_name, OLG, _values, _on_change) { \ | ||
| 74 | OLG \ | ||
| 75 | .name = _name, \ | ||
| 76 | .type = OPT_ENUM, \ | ||
| 77 | .u = {.enum_opt = {.values = _values}}, \ | ||
| 78 | .on_change = _on_change, \ | ||
| 79 | } | ||
| 80 | |||
| 81 | #define FLAG_OPT(_name, OLG, _values, _on_change) { \ | ||
| 82 | OLG \ | ||
| 83 | .name = _name, \ | ||
| 84 | .type = OPT_FLAG, \ | ||
| 85 | .u = {.enum_opt = {.values = _values}}, \ | ||
| 86 | .on_change = _on_change, \ | ||
| 87 | } | ||
| 88 | |||
| 89 | #define BOOL_OPT(_name, OLG, _on_change) { \ | ||
| 90 | OLG \ | ||
| 91 | .name = _name, \ | ||
| 92 | .type = OPT_BOOL, \ | ||
| 93 | .u = {.enum_opt = {.values = bool_enum}}, \ | ||
| 94 | .on_change = _on_change, \ | ||
| 95 | } | ||
| 96 | |||
| 97 | #define REGEX_OPT(_name, OLG, _on_change) { \ | ||
| 98 | OLG \ | ||
| 99 | .name = _name, \ | ||
| 100 | .type = OPT_REGEX, \ | ||
| 101 | .on_change = _on_change, \ | ||
| 102 | } | ||
| 103 | |||
| 104 | #define OLG(_offset, _local, _global) \ | ||
| 105 | .offset = _offset, \ | ||
| 106 | .local = _local, \ | ||
| 107 | .global = _global, \ | ||
| 108 | |||
| 109 | #define L(member) OLG(offsetof(LocalOptions, member), true, false) | ||
| 110 | #define G(member) OLG(offsetof(GlobalOptions, member), false, true) | ||
| 111 | #define C(member) OLG(offsetof(CommonOptions, member), true, true) | ||
| 112 | |||
| 113 | static void filetype_changed(EditorState *e, bool global) | ||
| 114 | { | ||
| 115 | BUG_ON(!e->buffer); | ||
| 116 | BUG_ON(global); | ||
| 117 | set_file_options(e, e->buffer); | ||
| 118 | buffer_update_syntax(e, e->buffer); | ||
| 119 | } | ||
| 120 | |||
| 121 | static void set_window_title_changed(EditorState *e, bool global) | ||
| 122 | { | ||
| 123 | BUG_ON(!global); | ||
| 124 | Terminal *term = &e->terminal; | ||
| 125 | if (e->options.set_window_title) { | ||
| 126 | if (e->status == EDITOR_RUNNING) { | ||
| 127 | update_term_title(term, e->buffer, e->options.set_window_title); | ||
| 128 | } | ||
| 129 | } else { | ||
| 130 | term_restore_title(term); | ||
| 131 | term_save_title(term); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | static void syntax_changed(EditorState *e, bool global) | ||
| 136 | { | ||
| 137 | if (e->buffer && !global) { | ||
| 138 | buffer_update_syntax(e, e->buffer); | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | static void overwrite_changed(EditorState *e, bool global) | ||
| 143 | { | ||
| 144 | if (!global) { | ||
| 145 | e->cursor_style_changed = true; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | static void redraw_buffer(EditorState *e, bool global) | ||
| 150 | { | ||
| 151 | if (e->buffer && !global) { | ||
| 152 | mark_all_lines_changed(e->buffer); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | static void redraw_screen(EditorState *e, bool global) | ||
| 157 | { | ||
| 158 | BUG_ON(!global); | ||
| 159 | mark_everything_changed(e); | ||
| 160 | } | ||
| 161 | |||
| 162 | static bool validate_statusline_format(const char *value) | ||
| 163 | { | ||
| 164 | size_t errpos = statusline_format_find_error(value); | ||
| 165 | if (likely(errpos == 0)) { | ||
| 166 | return true; | ||
| 167 | } | ||
| 168 | char ch = value[errpos]; | ||
| 169 | if (ch == '\0') { | ||
| 170 | return error_msg("Format character expected after '%%'"); | ||
| 171 | } | ||
| 172 | return error_msg("Invalid format character '%c'", ch); | ||
| 173 | } | ||
| 174 | |||
| 175 | static bool validate_filetype(const char *value) | ||
| 176 | { | ||
| 177 | if (!is_valid_filetype_name(value)) { | ||
| 178 | return error_msg("Invalid filetype name '%s'", value); | ||
| 179 | } | ||
| 180 | return true; | ||
| 181 | } | ||
| 182 | |||
| 183 | static OptionValue str_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 184 | { | ||
| 185 | const char *const *strp = ptr; | ||
| 186 | return (OptionValue){.str_val = *strp}; | ||
| 187 | } | ||
| 188 | |||
| 189 | static void str_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 190 | { | ||
| 191 | const char **strp = ptr; | ||
| 192 | *strp = str_intern(v.str_val); | ||
| 193 | } | ||
| 194 | |||
| 195 | static bool str_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 196 | { | ||
| 197 | bool valid = !d->u.str_opt.validate || d->u.str_opt.validate(str); | ||
| 198 | v->str_val = valid ? str : NULL; | ||
| 199 | return valid; | ||
| 200 | } | ||
| 201 | |||
| 202 | static const char *str_string(const OptionDesc* UNUSED_ARG(d), OptionValue v) | ||
| 203 | { | ||
| 204 | const char *s = v.str_val; | ||
| 205 | return s ? s : ""; | ||
| 206 | } | ||
| 207 | |||
| 208 | static bool str_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 209 | { | ||
| 210 | const char **strp = ptr; | ||
| 211 | return xstreq(*strp, v.str_val); | ||
| 212 | } | ||
| 213 | |||
| 214 | static OptionValue re_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 215 | { | ||
| 216 | const InternedRegexp *const *irp = ptr; | ||
| 217 | return (OptionValue){.str_val = *irp ? (*irp)->str : NULL}; | ||
| 218 | } | ||
| 219 | |||
| 220 | static void re_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 221 | { | ||
| 222 | const InternedRegexp **irp = ptr; | ||
| 223 | *irp = v.str_val ? regexp_intern(v.str_val) : NULL; | ||
| 224 | } | ||
| 225 | |||
| 226 | static bool re_parse(const OptionDesc* UNUSED_ARG(d), const char *str, OptionValue *v) | ||
| 227 | { | ||
| 228 | if (str[0] == '\0') { | ||
| 229 | v->str_val = NULL; | ||
| 230 | return true; | ||
| 231 | } | ||
| 232 | |||
| 233 | bool valid = regexp_is_interned(str) || regexp_is_valid(str, REG_NEWLINE); | ||
| 234 | v->str_val = valid ? str : NULL; | ||
| 235 | return valid; | ||
| 236 | } | ||
| 237 | |||
| 238 | static bool re_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 239 | { | ||
| 240 | const InternedRegexp **irp = ptr; | ||
| 241 | return *irp ? xstreq((*irp)->str, v.str_val) : !v.str_val; | ||
| 242 | } | ||
| 243 | |||
| 244 | static OptionValue uint_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 245 | { | ||
| 246 | const unsigned int *valp = ptr; | ||
| 247 | return (OptionValue){.uint_val = *valp}; | ||
| 248 | } | ||
| 249 | |||
| 250 | static void uint_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 251 | { | ||
| 252 | unsigned int *valp = ptr; | ||
| 253 | *valp = v.uint_val; | ||
| 254 | } | ||
| 255 | |||
| 256 | static bool uint_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 257 | { | ||
| 258 | unsigned int val; | ||
| 259 | if (!str_to_uint(str, &val)) { | ||
| 260 | return error_msg("Integer value for %s expected", d->name); | ||
| 261 | } | ||
| 262 | |||
| 263 | const unsigned int min = d->u.uint_opt.min; | ||
| 264 | const unsigned int max = d->u.uint_opt.max; | ||
| 265 | if (val < min || val > max) { | ||
| 266 | return error_msg("Value for %s must be in %u-%u range", d->name, min, max); | ||
| 267 | } | ||
| 268 | |||
| 269 | v->uint_val = val; | ||
| 270 | return true; | ||
| 271 | } | ||
| 272 | |||
| 273 | static const char *uint_string(const OptionDesc* UNUSED_ARG(desc), OptionValue value) | ||
| 274 | { | ||
| 275 | return uint_to_str(value.uint_val); | ||
| 276 | } | ||
| 277 | |||
| 278 | static bool uint_equals(const OptionDesc* UNUSED_ARG(desc), void *ptr, OptionValue value) | ||
| 279 | { | ||
| 280 | const unsigned int *valp = ptr; | ||
| 281 | return *valp == value.uint_val; | ||
| 282 | } | ||
| 283 | |||
| 284 | static OptionValue bool_get(const OptionDesc* UNUSED_ARG(d), void *ptr) | ||
| 285 | { | ||
| 286 | const bool *valp = ptr; | ||
| 287 | return (OptionValue){.bool_val = *valp}; | ||
| 288 | } | ||
| 289 | |||
| 290 | static void bool_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 291 | { | ||
| 292 | bool *valp = ptr; | ||
| 293 | *valp = v.bool_val; | ||
| 294 | } | ||
| 295 | |||
| 296 | static bool bool_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 297 | { | ||
| 298 | if (streq(str, "true")) { | ||
| 299 | v->bool_val = true; | ||
| 300 | return true; | ||
| 301 | } else if (streq(str, "false")) { | ||
| 302 | v->bool_val = false; | ||
| 303 | return true; | ||
| 304 | } | ||
| 305 | return error_msg("Invalid value for %s", d->name); | ||
| 306 | } | ||
| 307 | |||
| 308 | static const char *bool_string(const OptionDesc* UNUSED_ARG(d), OptionValue v) | ||
| 309 | { | ||
| 310 | return v.bool_val ? "true" : "false"; | ||
| 311 | } | ||
| 312 | |||
| 313 | static bool bool_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 314 | { | ||
| 315 | const bool *valp = ptr; | ||
| 316 | return *valp == v.bool_val; | ||
| 317 | } | ||
| 318 | |||
| 319 | static bool enum_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 320 | { | ||
| 321 | const char *const *values = d->u.enum_opt.values; | ||
| 322 | unsigned int i; | ||
| 323 | for (i = 0; values[i]; i++) { | ||
| 324 | if (streq(values[i], str)) { | ||
| 325 | v->uint_val = i; | ||
| 326 | return true; | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | unsigned int val; | ||
| 331 | if (!str_to_uint(str, &val) || val >= i) { | ||
| 332 | return error_msg("Invalid value for %s", d->name); | ||
| 333 | } | ||
| 334 | |||
| 335 | v->uint_val = val; | ||
| 336 | return true; | ||
| 337 | } | ||
| 338 | |||
| 339 | static const char *enum_string(const OptionDesc *desc, OptionValue value) | ||
| 340 | { | ||
| 341 | return desc->u.enum_opt.values[value.uint_val]; | ||
| 342 | } | ||
| 343 | |||
| 344 | static bool flag_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 345 | { | ||
| 346 | // "0" is allowed for compatibility and is the same as "" | ||
| 347 | if (str[0] == '0' && str[1] == '\0') { | ||
| 348 | v->uint_val = 0; | ||
| 349 | return true; | ||
| 350 | } | ||
| 351 | |||
| 352 | const char *const *values = d->u.enum_opt.values; | ||
| 353 | unsigned int flags = 0; | ||
| 354 | |||
| 355 | for (size_t pos = 0, len = strlen(str); pos < len; ) { | ||
| 356 | const StringView flag = get_delim(str, &pos, len, ','); | ||
| 357 | size_t i; | ||
| 358 | for (i = 0; values[i]; i++) { | ||
| 359 | if (strview_equal_cstring(&flag, values[i])) { | ||
| 360 | flags |= 1u << i; | ||
| 361 | break; | ||
| 362 | } | ||
| 363 | } | ||
| 364 | if (unlikely(!values[i])) { | ||
| 365 | int flen = (int)flag.length; | ||
| 366 | return error_msg("Invalid flag '%.*s' for %s", flen, flag.data, d->name); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | v->uint_val = flags; | ||
| 371 | return true; | ||
| 372 | } | ||
| 373 | |||
| 374 | static const char *flag_string(const OptionDesc *desc, OptionValue value) | ||
| 375 | { | ||
| 376 | static char buf[128]; | ||
| 377 | unsigned int flags = value.uint_val; | ||
| 378 | if (!flags) { | ||
| 379 | buf[0] = '0'; | ||
| 380 | buf[1] = '\0'; | ||
| 381 | return buf; | ||
| 382 | } | ||
| 383 | |||
| 384 | char *ptr = buf; | ||
| 385 | const char *const *values = desc->u.enum_opt.values; | ||
| 386 | for (size_t i = 0, avail = sizeof(buf); values[i]; i++) { | ||
| 387 | if (flags & (1u << i)) { | ||
| 388 | char *next = memccpy(ptr, values[i], '\0', avail); | ||
| 389 | if (DEBUG >= 1) { | ||
| 390 | BUG_ON(!next); | ||
| 391 | avail -= (size_t)(next - ptr); | ||
| 392 | } | ||
| 393 | ptr = next; | ||
| 394 | ptr[-1] = ','; | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | BUG_ON(ptr == buf); | ||
| 399 | ptr[-1] = '\0'; | ||
| 400 | return buf; | ||
| 401 | } | ||
| 402 | |||
| 403 | static const struct { | ||
| 404 | OptionValue (*get)(const OptionDesc *desc, void *ptr); | ||
| 405 | void (*set)(const OptionDesc *desc, void *ptr, OptionValue value); | ||
| 406 | bool (*parse)(const OptionDesc *desc, const char *str, OptionValue *value); | ||
| 407 | const char *(*string)(const OptionDesc *desc, OptionValue value); | ||
| 408 | bool (*equals)(const OptionDesc *desc, void *ptr, OptionValue value); | ||
| 409 | } option_ops[] = { | ||
| 410 | [OPT_STR] = {str_get, str_set, str_parse, str_string, str_equals}, | ||
| 411 | [OPT_UINT] = {uint_get, uint_set, uint_parse, uint_string, uint_equals}, | ||
| 412 | [OPT_ENUM] = {uint_get, uint_set, enum_parse, enum_string, uint_equals}, | ||
| 413 | [OPT_BOOL] = {bool_get, bool_set, bool_parse, bool_string, bool_equals}, | ||
| 414 | [OPT_FLAG] = {uint_get, uint_set, flag_parse, flag_string, uint_equals}, | ||
| 415 | [OPT_REGEX] = {re_get, re_set, re_parse, str_string, re_equals}, | ||
| 416 | }; | ||
| 417 | |||
| 418 | static const char *const bool_enum[] = {"false", "true", NULL}; | ||
| 419 | static const char *const newline_enum[] = {"unix", "dos", NULL}; | ||
| 420 | static const char *const tristate_enum[] = {"false", "true", "auto", NULL}; | ||
| 421 | static const char *const save_unmodified_enum[] = {"none", "touch", "full", NULL}; | ||
| 422 | |||
| 423 | static const char *const detect_indent_values[] = { | ||
| 424 | "1", "2", "3", "4", "5", "6", "7", "8", | ||
| 425 | NULL | ||
| 426 | }; | ||
| 427 | |||
| 428 | // Note: this must be kept in sync with WhitespaceErrorFlags | ||
| 429 | static const char *const ws_error_values[] = { | ||
| 430 | "space-indent", | ||
| 431 | "space-align", | ||
| 432 | "tab-indent", | ||
| 433 | "tab-after-indent", | ||
| 434 | "special", | ||
| 435 | "auto-indent", | ||
| 436 | "trailing", | ||
| 437 | "all-trailing", | ||
| 438 | NULL | ||
| 439 | }; | ||
| 440 | |||
| 441 | static const OptionDesc option_desc[] = { | ||
| 442 | BOOL_OPT("auto-indent", C(auto_indent), NULL), | ||
| 443 | BOOL_OPT("brace-indent", L(brace_indent), NULL), | ||
| 444 | ENUM_OPT("case-sensitive-search", G(case_sensitive_search), tristate_enum, NULL), | ||
| 445 | FLAG_OPT("detect-indent", C(detect_indent), detect_indent_values, NULL), | ||
| 446 | BOOL_OPT("display-special", G(display_special), redraw_screen), | ||
| 447 | BOOL_OPT("editorconfig", C(editorconfig), NULL), | ||
| 448 | BOOL_OPT("emulate-tab", C(emulate_tab), NULL), | ||
| 449 | UINT_OPT("esc-timeout", G(esc_timeout), 0, 2000, NULL), | ||
| 450 | BOOL_OPT("expand-tab", C(expand_tab), redraw_buffer), | ||
| 451 | BOOL_OPT("file-history", C(file_history), NULL), | ||
| 452 | UINT_OPT("filesize-limit", G(filesize_limit), 0, 16000, NULL), | ||
| 453 | STR_OPT("filetype", L(filetype), validate_filetype, filetype_changed), | ||
| 454 | BOOL_OPT("fsync", C(fsync), NULL), | ||
| 455 | REGEX_OPT("indent-regex", L(indent_regex), NULL), | ||
| 456 | UINT_OPT("indent-width", C(indent_width), 1, INDENT_WIDTH_MAX, NULL), | ||
| 457 | BOOL_OPT("lock-files", G(lock_files), NULL), | ||
| 458 | ENUM_OPT("newline", G(crlf_newlines), newline_enum, NULL), | ||
| 459 | BOOL_OPT("optimize-true-color", G(optimize_true_color), redraw_screen), | ||
| 460 | BOOL_OPT("overwrite", C(overwrite), overwrite_changed), | ||
| 461 | ENUM_OPT("save-unmodified", C(save_unmodified), save_unmodified_enum, NULL), | ||
| 462 | UINT_OPT("scroll-margin", G(scroll_margin), 0, 100, redraw_screen), | ||
| 463 | BOOL_OPT("select-cursor-char", G(select_cursor_char), redraw_screen), | ||
| 464 | BOOL_OPT("set-window-title", G(set_window_title), set_window_title_changed), | ||
| 465 | BOOL_OPT("show-line-numbers", G(show_line_numbers), redraw_screen), | ||
| 466 | STR_OPT("statusline-left", G(statusline_left), validate_statusline_format, NULL), | ||
| 467 | STR_OPT("statusline-right", G(statusline_right), validate_statusline_format, NULL), | ||
| 468 | BOOL_OPT("syntax", C(syntax), syntax_changed), | ||
| 469 | BOOL_OPT("tab-bar", G(tab_bar), redraw_screen), | ||
| 470 | UINT_OPT("tab-width", C(tab_width), 1, TAB_WIDTH_MAX, redraw_buffer), | ||
| 471 | UINT_OPT("text-width", C(text_width), 1, TEXT_WIDTH_MAX, NULL), | ||
| 472 | BOOL_OPT("utf8-bom", G(utf8_bom), NULL), | ||
| 473 | FLAG_OPT("ws-error", C(ws_error), ws_error_values, redraw_buffer), | ||
| 474 | }; | ||
| 475 | |||
| 476 | static char *local_ptr(const OptionDesc *desc, LocalOptions *opt) | ||
| 477 | { | ||
| 478 | BUG_ON(!desc->local); | ||
| 479 | return (char*)opt + desc->offset; | ||
| 480 | } | ||
| 481 | |||
| 482 | static char *global_ptr(const OptionDesc *desc, GlobalOptions *opt) | ||
| 483 | { | ||
| 484 | BUG_ON(!desc->global); | ||
| 485 | return (char*)opt + desc->offset; | ||
| 486 | } | ||
| 487 | |||
| 488 | static char *get_option_ptr(EditorState *e, const OptionDesc *d, bool global) | ||
| 489 | { | ||
| 490 | return global ? global_ptr(d, &e->options) : local_ptr(d, &e->buffer->options); | ||
| 491 | } | ||
| 492 | |||
| 493 | static inline size_t count_enum_values(const OptionDesc *desc) | ||
| 494 | { | ||
| 495 | OptionType type = desc->type; | ||
| 496 | BUG_ON(type != OPT_ENUM && type != OPT_FLAG && type != OPT_BOOL); | ||
| 497 | const char *const *values = desc->u.enum_opt.values; | ||
| 498 | BUG_ON(!values); | ||
| 499 | return string_array_length((char**)values); | ||
| 500 | } | ||
| 501 | |||
| 502 | UNITTEST { | ||
| 503 | static const struct { | ||
| 504 | size_t alignment; | ||
| 505 | size_t size; | ||
| 506 | } map[] = { | ||
| 507 | [OPT_STR] = {ALIGNOF(const char*), sizeof(const char*)}, | ||
| 508 | [OPT_UINT] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 509 | [OPT_ENUM] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 510 | [OPT_BOOL] = {ALIGNOF(bool), sizeof(bool)}, | ||
| 511 | [OPT_FLAG] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 512 | [OPT_REGEX] = {ALIGNOF(const InternedRegexp*), sizeof(const InternedRegexp*)}, | ||
| 513 | }; | ||
| 514 | |||
| 515 | GlobalOptions gopts = {.tab_bar = true}; | ||
| 516 | LocalOptions lopts = {.filetype = NULL}; | ||
| 517 | |||
| 518 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 519 | const OptionDesc *desc = &option_desc[i]; | ||
| 520 | const OptionType type = desc->type; | ||
| 521 | BUG_ON(type >= ARRAYLEN(map)); | ||
| 522 | size_t alignment = map[type].alignment; | ||
| 523 | size_t end = desc->offset + map[type].size; | ||
| 524 | if (desc->global) { | ||
| 525 | uintptr_t ptr_val = (uintptr_t)global_ptr(desc, &gopts); | ||
| 526 | BUG_ON(ptr_val % alignment != 0); | ||
| 527 | BUG_ON(end > sizeof(GlobalOptions)); | ||
| 528 | } | ||
| 529 | if (desc->local) { | ||
| 530 | uintptr_t ptr_val = (uintptr_t)local_ptr(desc, &lopts); | ||
| 531 | BUG_ON(ptr_val % alignment != 0); | ||
| 532 | BUG_ON(end > sizeof(LocalOptions)); | ||
| 533 | } | ||
| 534 | if (desc->global && desc->local) { | ||
| 535 | BUG_ON(end > sizeof(CommonOptions)); | ||
| 536 | } | ||
| 537 | if (type == OPT_UINT) { | ||
| 538 | BUG_ON(desc->u.uint_opt.max <= desc->u.uint_opt.min); | ||
| 539 | } else if (type == OPT_BOOL) { | ||
| 540 | BUG_ON(desc->u.enum_opt.values != bool_enum); | ||
| 541 | } else if (type == OPT_ENUM) { | ||
| 542 | BUG_ON(count_enum_values(desc) < 2); | ||
| 543 | } else if (type == OPT_FLAG) { | ||
| 544 | size_t nvals = count_enum_values(desc); | ||
| 545 | OptionValue val = {.uint_val = -1}; | ||
| 546 | BUG_ON(nvals < 2); | ||
| 547 | BUG_ON(nvals >= BITSIZE(val.uint_val)); | ||
| 548 | const char *str = flag_string(desc, val); | ||
| 549 | BUG_ON(!str); | ||
| 550 | BUG_ON(str[0] == '\0'); | ||
| 551 | if (!flag_parse(desc, str, &val)) { | ||
| 552 | BUG("flag_parse() failed for string: %s", str); | ||
| 553 | } | ||
| 554 | unsigned int mask = (1u << nvals) - 1; | ||
| 555 | if (val.uint_val != mask) { | ||
| 556 | BUG("values not equal: %u, %u", val.uint_val, mask); | ||
| 557 | } | ||
| 558 | } | ||
| 559 | } | ||
| 560 | |||
| 561 | // Ensure option_desc[] is properly sorted | ||
| 562 | CHECK_BSEARCH_ARRAY(option_desc, name, strcmp); | ||
| 563 | } | ||
| 564 | |||
| 565 | static OptionValue desc_get(const OptionDesc *desc, void *ptr) | ||
| 566 | { | ||
| 567 | return option_ops[desc->type].get(desc, ptr); | ||
| 568 | } | ||
| 569 | |||
| 570 | static void desc_set(EditorState *e, const OptionDesc *desc, void *ptr, bool global, OptionValue value) | ||
| 571 | { | ||
| 572 | option_ops[desc->type].set(desc, ptr, value); | ||
| 573 | if (desc->on_change) { | ||
| 574 | desc->on_change(e, global); | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | static bool desc_parse(const OptionDesc *desc, const char *str, OptionValue *value) | ||
| 579 | { | ||
| 580 | return option_ops[desc->type].parse(desc, str, value); | ||
| 581 | } | ||
| 582 | |||
| 583 | static const char *desc_string(const OptionDesc *desc, OptionValue value) | ||
| 584 | { | ||
| 585 | return option_ops[desc->type].string(desc, value); | ||
| 586 | } | ||
| 587 | |||
| 588 | static bool desc_equals(const OptionDesc *desc, void *ptr, OptionValue value) | ||
| 589 | { | ||
| 590 | return option_ops[desc->type].equals(desc, ptr, value); | ||
| 591 | } | ||
| 592 | |||
| 593 | static int option_cmp(const void *key, const void *elem) | ||
| 594 | { | ||
| 595 | const char *name = key; | ||
| 596 | const OptionDesc *desc = elem; | ||
| 597 | return strcmp(name, desc->name); | ||
| 598 | } | ||
| 599 | |||
| 600 | static const OptionDesc *find_option(const char *name) | ||
| 601 | { | ||
| 602 | return BSEARCH(name, option_desc, option_cmp); | ||
| 603 | } | ||
| 604 | |||
| 605 | static const OptionDesc *must_find_option(const char *name) | ||
| 606 | { | ||
| 607 | const OptionDesc *desc = find_option(name); | ||
| 608 | if (!desc) { | ||
| 609 | error_msg("No such option %s", name); | ||
| 610 | } | ||
| 611 | return desc; | ||
| 612 | } | ||
| 613 | |||
| 614 | static const OptionDesc *must_find_global_option(const char *name) | ||
| 615 | { | ||
| 616 | const OptionDesc *desc = must_find_option(name); | ||
| 617 | if (desc && !desc->global) { | ||
| 618 | error_msg("Option %s is not global", name); | ||
| 619 | return NULL; | ||
| 620 | } | ||
| 621 | return desc; | ||
| 622 | } | ||
| 623 | |||
| 624 | static bool do_set_option ( | ||
| 625 | EditorState *e, | ||
| 626 | const OptionDesc *desc, | ||
| 627 | const char *value, | ||
| 628 | bool local, | ||
| 629 | bool global | ||
| 630 | ) { | ||
| 631 | if (local && !desc->local) { | ||
| 632 | return error_msg("Option %s is not local", desc->name); | ||
| 633 | } | ||
| 634 | if (global && !desc->global) { | ||
| 635 | return error_msg("Option %s is not global", desc->name); | ||
| 636 | } | ||
| 637 | |||
| 638 | OptionValue val; | ||
| 639 | if (!desc_parse(desc, value, &val)) { | ||
| 640 | return false; | ||
| 641 | } | ||
| 642 | |||
| 643 | if (!local && !global) { | ||
| 644 | // Set both by default | ||
| 645 | local = desc->local; | ||
| 646 | global = desc->global; | ||
| 647 | } | ||
| 648 | |||
| 649 | if (local) { | ||
| 650 | desc_set(e, desc, local_ptr(desc, &e->buffer->options), false, val); | ||
| 651 | } | ||
| 652 | if (global) { | ||
| 653 | desc_set(e, desc, global_ptr(desc, &e->options), true, val); | ||
| 654 | } | ||
| 655 | |||
| 656 | return true; | ||
| 657 | } | ||
| 658 | |||
| 659 | bool set_option(EditorState *e, const char *name, const char *value, bool local, bool global) | ||
| 660 | { | ||
| 661 | const OptionDesc *desc = must_find_option(name); | ||
| 662 | if (!desc) { | ||
| 663 | return false; | ||
| 664 | } | ||
| 665 | return do_set_option(e, desc, value, local, global); | ||
| 666 | } | ||
| 667 | |||
| 668 | bool set_bool_option(EditorState *e, const char *name, bool local, bool global) | ||
| 669 | { | ||
| 670 | const OptionDesc *desc = must_find_option(name); | ||
| 671 | if (!desc) { | ||
| 672 | return false; | ||
| 673 | } | ||
| 674 | if (desc->type != OPT_BOOL) { | ||
| 675 | return error_msg("Option %s is not boolean", desc->name); | ||
| 676 | } | ||
| 677 | return do_set_option(e, desc, "true", local, global); | ||
| 678 | } | ||
| 679 | |||
| 680 | static const OptionDesc *find_toggle_option(const char *name, bool *global) | ||
| 681 | { | ||
| 682 | if (*global) { | ||
| 683 | return must_find_global_option(name); | ||
| 684 | } | ||
| 685 | |||
| 686 | // Toggle local value by default if option has both values | ||
| 687 | const OptionDesc *desc = must_find_option(name); | ||
| 688 | if (desc && !desc->local) { | ||
| 689 | *global = true; | ||
| 690 | } | ||
| 691 | return desc; | ||
| 692 | } | ||
| 693 | |||
| 694 | bool toggle_option(EditorState *e, const char *name, bool global, bool verbose) | ||
| 695 | { | ||
| 696 | const OptionDesc *desc = find_toggle_option(name, &global); | ||
| 697 | if (!desc) { | ||
| 698 | return false; | ||
| 699 | } | ||
| 700 | |||
| 701 | char *ptr = get_option_ptr(e, desc, global); | ||
| 702 | OptionValue value = desc_get(desc, ptr); | ||
| 703 | OptionType type = desc->type; | ||
| 704 | if (type == OPT_ENUM) { | ||
| 705 | if (desc->u.enum_opt.values[value.uint_val + 1]) { | ||
| 706 | value.uint_val++; | ||
| 707 | } else { | ||
| 708 | value.uint_val = 0; | ||
| 709 | } | ||
| 710 | } else if (type == OPT_BOOL) { | ||
| 711 | value.bool_val = !value.bool_val; | ||
| 712 | } else { | ||
| 713 | return error_msg("Toggling %s requires arguments", name); | ||
| 714 | } | ||
| 715 | |||
| 716 | desc_set(e, desc, ptr, global, value); | ||
| 717 | if (verbose) { | ||
| 718 | const char *prefix = (global && desc->local) ? "[global] " : ""; | ||
| 719 | const char *str = desc_string(desc, value); | ||
| 720 | info_msg("%s%s = %s", prefix, desc->name, str); | ||
| 721 | } | ||
| 722 | |||
| 723 | return true; | ||
| 724 | } | ||
| 725 | |||
| 726 | bool toggle_option_values ( | ||
| 727 | EditorState *e, | ||
| 728 | const char *name, | ||
| 729 | bool global, | ||
| 730 | bool verbose, | ||
| 731 | char **values, | ||
| 732 | size_t count | ||
| 733 | ) { | ||
| 734 | const OptionDesc *desc = find_toggle_option(name, &global); | ||
| 735 | if (!desc) { | ||
| 736 | return false; | ||
| 737 | } | ||
| 738 | |||
| 739 | BUG_ON(count == 0); | ||
| 740 | size_t current = 0; | ||
| 741 | bool error = false; | ||
| 742 | char *ptr = get_option_ptr(e, desc, global); | ||
| 743 | OptionValue *parsed_values = xnew(OptionValue, count); | ||
| 744 | |||
| 745 | for (size_t i = 0; i < count; i++) { | ||
| 746 | if (desc_parse(desc, values[i], &parsed_values[i])) { | ||
| 747 | if (desc_equals(desc, ptr, parsed_values[i])) { | ||
| 748 | current = i + 1; | ||
| 749 | } | ||
| 750 | } else { | ||
| 751 | error = true; | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | if (!error) { | ||
| 756 | size_t i = current % count; | ||
| 757 | desc_set(e, desc, ptr, global, parsed_values[i]); | ||
| 758 | if (verbose) { | ||
| 759 | const char *prefix = (global && desc->local) ? "[global] " : ""; | ||
| 760 | const char *str = desc_string(desc, parsed_values[i]); | ||
| 761 | info_msg("%s%s = %s", prefix, desc->name, str); | ||
| 762 | } | ||
| 763 | } | ||
| 764 | |||
| 765 | free(parsed_values); | ||
| 766 | return !error; | ||
| 767 | } | ||
| 768 | |||
| 769 | bool validate_local_options(char **strs) | ||
| 770 | { | ||
| 771 | size_t invalid = 0; | ||
| 772 | for (size_t i = 0; strs[i]; i += 2) { | ||
| 773 | const char *name = strs[i]; | ||
| 774 | const char *value = strs[i + 1]; | ||
| 775 | const OptionDesc *desc = must_find_option(name); | ||
| 776 | if (unlikely(!desc)) { | ||
| 777 | invalid++; | ||
| 778 | } else if (unlikely(!desc->local)) { | ||
| 779 | error_msg("%s is not local", name); | ||
| 780 | invalid++; | ||
| 781 | } else if (unlikely(desc->on_change == filetype_changed)) { | ||
| 782 | error_msg("filetype cannot be set via option command"); | ||
| 783 | invalid++; | ||
| 784 | } else { | ||
| 785 | OptionValue val; | ||
| 786 | if (unlikely(!desc_parse(desc, value, &val))) { | ||
| 787 | invalid++; | ||
| 788 | } | ||
| 789 | } | ||
| 790 | } | ||
| 791 | return !invalid; | ||
| 792 | } | ||
| 793 | |||
| 794 | #if DEBUG >= 1 | ||
| 795 | static void sanity_check_option_value(const OptionDesc *desc, OptionValue val) | ||
| 796 | { | ||
| 797 | switch (desc->type) { | ||
| 798 | case OPT_STR: | ||
| 799 | BUG_ON(!val.str_val); | ||
| 800 | BUG_ON(val.str_val != str_intern(val.str_val)); | ||
| 801 | if (desc->u.str_opt.validate) { | ||
| 802 | BUG_ON(!desc->u.str_opt.validate(val.str_val)); | ||
| 803 | } | ||
| 804 | return; | ||
| 805 | case OPT_UINT: | ||
| 806 | BUG_ON(val.uint_val < desc->u.uint_opt.min); | ||
| 807 | BUG_ON(val.uint_val > desc->u.uint_opt.max); | ||
| 808 | return; | ||
| 809 | case OPT_ENUM: | ||
| 810 | BUG_ON(val.uint_val >= count_enum_values(desc)); | ||
| 811 | return; | ||
| 812 | case OPT_FLAG: { | ||
| 813 | size_t nvals = count_enum_values(desc); | ||
| 814 | BUG_ON(nvals >= 32); | ||
| 815 | unsigned int mask = (1u << nvals) - 1; | ||
| 816 | unsigned int uint_val = val.uint_val; | ||
| 817 | BUG_ON((uint_val & mask) != uint_val); | ||
| 818 | } | ||
| 819 | return; | ||
| 820 | case OPT_REGEX: | ||
| 821 | BUG_ON(val.str_val && val.str_val[0] == '\0'); | ||
| 822 | BUG_ON(val.str_val && !regexp_is_interned(val.str_val)); | ||
| 823 | return; | ||
| 824 | case OPT_BOOL: | ||
| 825 | return; | ||
| 826 | } | ||
| 827 | |||
| 828 | BUG("unhandled option type"); | ||
| 829 | } | ||
| 830 | |||
| 831 | static void sanity_check_options(const void *opts, bool global) | ||
| 832 | { | ||
| 833 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 834 | const OptionDesc *desc = &option_desc[i]; | ||
| 835 | BUG_ON(desc->type >= ARRAYLEN(option_ops)); | ||
| 836 | if ((desc->global && desc->local) || global == desc->global) { | ||
| 837 | OptionValue val = desc_get(desc, (char*)opts + desc->offset); | ||
| 838 | sanity_check_option_value(desc, val); | ||
| 839 | } | ||
| 840 | } | ||
| 841 | } | ||
| 842 | |||
| 843 | void sanity_check_global_options(const GlobalOptions *gopts) | ||
| 844 | { | ||
| 845 | sanity_check_options(gopts, true); | ||
| 846 | } | ||
| 847 | |||
| 848 | void sanity_check_local_options(const LocalOptions *lopts) | ||
| 849 | { | ||
| 850 | sanity_check_options(lopts, false); | ||
| 851 | } | ||
| 852 | #endif | ||
| 853 | |||
| 854 | void collect_options(PointerArray *a, const char *prefix, bool local, bool global) | ||
| 855 | { | ||
| 856 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 857 | const OptionDesc *desc = &option_desc[i]; | ||
| 858 | if ((local && !desc->local) || (global && !desc->global)) { | ||
| 859 | continue; | ||
| 860 | } | ||
| 861 | if (str_has_prefix(desc->name, prefix)) { | ||
| 862 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 863 | } | ||
| 864 | } | ||
| 865 | } | ||
| 866 | |||
| 867 | // Collect options that can be set via the "option" command | ||
| 868 | void collect_auto_options(PointerArray *a, const char *prefix) | ||
| 869 | { | ||
| 870 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 871 | const OptionDesc *desc = &option_desc[i]; | ||
| 872 | if (!desc->local || desc->on_change == filetype_changed) { | ||
| 873 | continue; | ||
| 874 | } | ||
| 875 | if (str_has_prefix(desc->name, prefix)) { | ||
| 876 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 877 | } | ||
| 878 | } | ||
| 879 | } | ||
| 880 | |||
| 881 | void collect_toggleable_options(PointerArray *a, const char *prefix, bool global) | ||
| 882 | { | ||
| 883 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 884 | const OptionDesc *desc = &option_desc[i]; | ||
| 885 | if (global && !desc->global) { | ||
| 886 | continue; | ||
| 887 | } | ||
| 888 | OptionType type = desc->type; | ||
| 889 | bool toggleable = (type == OPT_ENUM || type == OPT_BOOL); | ||
| 890 | if (toggleable && str_has_prefix(desc->name, prefix)) { | ||
| 891 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 892 | } | ||
| 893 | } | ||
| 894 | } | ||
| 895 | |||
| 896 | void collect_option_values(EditorState *e, PointerArray *a, const char *option, const char *prefix) | ||
| 897 | { | ||
| 898 | const OptionDesc *desc = find_option(option); | ||
| 899 | if (!desc) { | ||
| 900 | return; | ||
| 901 | } | ||
| 902 | |||
| 903 | if (prefix[0] == '\0') { | ||
| 904 | char *ptr = get_option_ptr(e, desc, !desc->local); | ||
| 905 | OptionValue value = desc_get(desc, ptr); | ||
| 906 | ptr_array_append(a, xstrdup(desc_string(desc, value))); | ||
| 907 | return; | ||
| 908 | } | ||
| 909 | |||
| 910 | OptionType type = desc->type; | ||
| 911 | if (type == OPT_STR || type == OPT_UINT || type == OPT_REGEX) { | ||
| 912 | return; | ||
| 913 | } | ||
| 914 | |||
| 915 | const char *const *values = desc->u.enum_opt.values; | ||
| 916 | if (type == OPT_ENUM || type == OPT_BOOL) { | ||
| 917 | for (size_t i = 0; values[i]; i++) { | ||
| 918 | if (str_has_prefix(values[i], prefix)) { | ||
| 919 | ptr_array_append(a, xstrdup(values[i])); | ||
| 920 | } | ||
| 921 | } | ||
| 922 | return; | ||
| 923 | } | ||
| 924 | |||
| 925 | BUG_ON(type != OPT_FLAG); | ||
| 926 | const char *comma = strrchr(prefix, ','); | ||
| 927 | size_t prefix_len = comma ? ++comma - prefix : 0; | ||
| 928 | for (size_t i = 0; values[i]; i++) { | ||
| 929 | const char *str = values[i]; | ||
| 930 | if (str_has_prefix(str, prefix + prefix_len)) { | ||
| 931 | size_t str_len = strlen(str); | ||
| 932 | char *completion = xmalloc(prefix_len + str_len + 1); | ||
| 933 | memcpy(completion, prefix, prefix_len); | ||
| 934 | memcpy(completion + prefix_len, str, str_len + 1); | ||
| 935 | ptr_array_append(a, completion); | ||
| 936 | } | ||
| 937 | } | ||
| 938 | } | ||
| 939 | |||
| 940 | static void append_option(String *s, const OptionDesc *desc, void *ptr) | ||
| 941 | { | ||
| 942 | const OptionValue value = desc_get(desc, ptr); | ||
| 943 | const char *value_str = desc_string(desc, value); | ||
| 944 | if (unlikely(value_str[0] == '-')) { | ||
| 945 | string_append_literal(s, "-- "); | ||
| 946 | } | ||
| 947 | string_append_cstring(s, desc->name); | ||
| 948 | string_append_byte(s, ' '); | ||
| 949 | string_append_escaped_arg(s, value_str, true); | ||
| 950 | string_append_byte(s, '\n'); | ||
| 951 | } | ||
| 952 | |||
| 953 | String dump_options(GlobalOptions *gopts, LocalOptions *lopts) | ||
| 954 | { | ||
| 955 | String buf = string_new(4096); | ||
| 956 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 957 | const OptionDesc *desc = &option_desc[i]; | ||
| 958 | void *local = desc->local ? local_ptr(desc, lopts) : NULL; | ||
| 959 | void *global = desc->global ? global_ptr(desc, gopts) : NULL; | ||
| 960 | if (local && global) { | ||
| 961 | const OptionValue global_value = desc_get(desc, global); | ||
| 962 | if (desc_equals(desc, local, global_value)) { | ||
| 963 | string_append_literal(&buf, "set "); | ||
| 964 | append_option(&buf, desc, local); | ||
| 965 | } else { | ||
| 966 | string_append_literal(&buf, "set -g "); | ||
| 967 | append_option(&buf, desc, global); | ||
| 968 | string_append_literal(&buf, "set -l "); | ||
| 969 | append_option(&buf, desc, local); | ||
| 970 | } | ||
| 971 | } else { | ||
| 972 | string_append_literal(&buf, "set "); | ||
| 973 | append_option(&buf, desc, local ? local : global); | ||
| 974 | } | ||
| 975 | } | ||
| 976 | return buf; | ||
| 977 | } | ||
| 978 | |||
| 979 | const char *get_option_value_string(EditorState *e, const char *name) | ||
| 980 | { | ||
| 981 | const OptionDesc *desc = find_option(name); | ||
| 982 | if (!desc) { | ||
| 983 | return NULL; | ||
| 984 | } | ||
| 985 | char *ptr = get_option_ptr(e, desc, !desc->local); | ||
| 986 | return desc_string(desc, desc_get(desc, ptr)); | ||
| 987 | } | ||
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 @@ | |||
| 1 | #ifndef OPTIONS_H | ||
| 2 | #define OPTIONS_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "regexp.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | #include "util/ptr-array.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | |||
| 11 | enum { | ||
| 12 | INDENT_WIDTH_MAX = 8, | ||
| 13 | TAB_WIDTH_MAX = 8, | ||
| 14 | TEXT_WIDTH_MAX = 1000, | ||
| 15 | }; | ||
| 16 | |||
| 17 | // Note: this must be kept in sync with ws_error_values[] | ||
| 18 | typedef enum { | ||
| 19 | WSE_SPACE_INDENT = 1 << 0, // Spaces in indent (except WSE_SPACE_ALIGN) | ||
| 20 | WSE_SPACE_ALIGN = 1 << 1, // Less than tab-width spaces at end of indent | ||
| 21 | WSE_TAB_INDENT = 1 << 2, // Tab in indent | ||
| 22 | WSE_TAB_AFTER_INDENT = 1 << 3, // Tab anywhere but indent | ||
| 23 | WSE_SPECIAL = 1 << 4, // Special whitespace characters | ||
| 24 | WSE_AUTO_INDENT = 1 << 5, // expand-tab ? WSE_TAB_AFTER_INDENT | WSE_TAB_INDENT : WSE_SPACE_INDENT | ||
| 25 | WSE_TRAILING = 1 << 6, // Trailing whitespace | ||
| 26 | WSE_ALL_TRAILING = 1 << 7, // Like WSE_TRAILING, but including around cursor | ||
| 27 | } WhitespaceErrorFlags; | ||
| 28 | |||
| 29 | // Note: this must be kept in sync with save_unmodified_enum[] | ||
| 30 | typedef enum { | ||
| 31 | SAVE_NONE, | ||
| 32 | SAVE_TOUCH, | ||
| 33 | SAVE_FULL, | ||
| 34 | } SaveUnmodifiedType; | ||
| 35 | |||
| 36 | #define COMMON_OPTIONS \ | ||
| 37 | unsigned int detect_indent; \ | ||
| 38 | unsigned int indent_width; \ | ||
| 39 | unsigned int save_unmodified; \ | ||
| 40 | unsigned int tab_width; \ | ||
| 41 | unsigned int text_width; \ | ||
| 42 | unsigned int ws_error; \ | ||
| 43 | bool auto_indent; \ | ||
| 44 | bool editorconfig; \ | ||
| 45 | bool emulate_tab; \ | ||
| 46 | bool expand_tab; \ | ||
| 47 | bool file_history; \ | ||
| 48 | bool fsync; \ | ||
| 49 | bool overwrite; \ | ||
| 50 | bool syntax | ||
| 51 | |||
| 52 | typedef struct { | ||
| 53 | COMMON_OPTIONS; | ||
| 54 | } CommonOptions; | ||
| 55 | |||
| 56 | // Note: all members should be initialized in buffer_new() | ||
| 57 | typedef struct { | ||
| 58 | COMMON_OPTIONS; | ||
| 59 | // Only local | ||
| 60 | bool brace_indent; | ||
| 61 | const char *filetype; | ||
| 62 | const InternedRegexp *indent_regex; | ||
| 63 | } LocalOptions; | ||
| 64 | |||
| 65 | typedef struct { | ||
| 66 | COMMON_OPTIONS; | ||
| 67 | // Only global | ||
| 68 | bool display_special; | ||
| 69 | bool lock_files; | ||
| 70 | bool optimize_true_color; | ||
| 71 | bool select_cursor_char; | ||
| 72 | bool set_window_title; | ||
| 73 | bool show_line_numbers; | ||
| 74 | bool tab_bar; | ||
| 75 | bool utf8_bom; // Default value for new files | ||
| 76 | unsigned int esc_timeout; | ||
| 77 | unsigned int filesize_limit; | ||
| 78 | unsigned int scroll_margin; | ||
| 79 | unsigned int crlf_newlines; // Default value for new files | ||
| 80 | unsigned int case_sensitive_search; // SearchCaseSensitivity | ||
| 81 | const char *statusline_left; | ||
| 82 | const char *statusline_right; | ||
| 83 | } GlobalOptions; | ||
| 84 | |||
| 85 | #undef COMMON_OPTIONS | ||
| 86 | |||
| 87 | static inline bool use_spaces_for_indent(const LocalOptions *opt) | ||
| 88 | { | ||
| 89 | return opt->expand_tab || opt->indent_width != opt->tab_width; | ||
| 90 | } | ||
| 91 | |||
| 92 | struct EditorState; | ||
| 93 | |||
| 94 | bool set_option(struct EditorState *e, const char *name, const char *value, bool local, bool global); | ||
| 95 | bool set_bool_option(struct EditorState *e, const char *name, bool local, bool global); | ||
| 96 | bool toggle_option(struct EditorState *e, const char *name, bool global, bool verbose); | ||
| 97 | bool toggle_option_values(struct EditorState *e, const char *name, bool global, bool verbose, char **values, size_t count); | ||
| 98 | bool validate_local_options(char **strs); | ||
| 99 | void collect_options(PointerArray *a, const char *prefix, bool local, bool global); | ||
| 100 | void collect_auto_options(PointerArray *a, const char *prefix); | ||
| 101 | void collect_toggleable_options(PointerArray *a, const char *prefix, bool global); | ||
| 102 | void collect_option_values(struct EditorState *e, PointerArray *a, const char *option, const char *prefix); | ||
| 103 | String dump_options(GlobalOptions *gopts, LocalOptions *lopts); | ||
| 104 | const char *get_option_value_string(struct EditorState *e, const char *name); | ||
| 105 | |||
| 106 | #if DEBUG >= 1 | ||
| 107 | void sanity_check_global_options(const GlobalOptions *opts); | ||
| 108 | void sanity_check_local_options(const LocalOptions *lopts); | ||
| 109 | #else | ||
| 110 | static inline void sanity_check_global_options(const GlobalOptions* UNUSED_ARG(gopts)) {} | ||
| 111 | static inline void sanity_check_local_options(const LocalOptions* UNUSED_ARG(lopts)) {} | ||
| 112 | #endif | ||
| 113 | |||
| 114 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include "regexp.h" | ||
| 4 | #include "error.h" | ||
| 5 | #include "util/debug.h" | ||
| 6 | #include "util/hashmap.h" | ||
| 7 | #include "util/str-util.h" | ||
| 8 | #include "util/xmalloc.h" | ||
| 9 | #include "util/xsnprintf.h" | ||
| 10 | |||
| 11 | static HashMap interned_regexps; | ||
| 12 | |||
| 13 | bool regexp_error_msg(const regex_t *re, const char *pattern, int err) | ||
| 14 | { | ||
| 15 | char msg[1024]; | ||
| 16 | regerror(err, re, msg, sizeof(msg)); | ||
| 17 | return error_msg("%s: %s", msg, pattern); | ||
| 18 | } | ||
| 19 | |||
| 20 | bool regexp_compile_internal(regex_t *re, const char *pattern, int flags) | ||
| 21 | { | ||
| 22 | int err = regcomp(re, pattern, flags); | ||
| 23 | if (err) { | ||
| 24 | return regexp_error_msg(re, pattern, err); | ||
| 25 | } | ||
| 26 | return true; | ||
| 27 | } | ||
| 28 | |||
| 29 | void regexp_compile_or_fatal_error(regex_t *re, const char *pattern, int flags) | ||
| 30 | { | ||
| 31 | // Note: DEFAULT_REGEX_FLAGS isn't used here because this function | ||
| 32 | // is only used for compiling built-in patterns, where we explicitly | ||
| 33 | // avoid using "enhanced" features | ||
| 34 | int err = regcomp(re, pattern, flags | REG_EXTENDED); | ||
| 35 | if (unlikely(err)) { | ||
| 36 | char msg[1024]; | ||
| 37 | regerror(err, re, msg, sizeof(msg)); | ||
| 38 | fatal_error(msg, EINVAL); | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | bool regexp_exec ( | ||
| 43 | const regex_t *re, | ||
| 44 | const char *buf, | ||
| 45 | size_t size, | ||
| 46 | size_t nmatch, | ||
| 47 | regmatch_t *pmatch, | ||
| 48 | int flags | ||
| 49 | ) { | ||
| 50 | // "If REG_STARTEND is specified, pmatch must point to at least one | ||
| 51 | // regmatch_t (even if nmatch is 0 or REG_NOSUB was specified), to | ||
| 52 | // hold the input offsets for REG_STARTEND." | ||
| 53 | // -- https://man.openbsd.org/regex.3 | ||
| 54 | BUG_ON(!pmatch); | ||
| 55 | |||
| 56 | // ASan's __interceptor_regexec() doesn't support REG_STARTEND | ||
| 57 | #if defined(REG_STARTEND) && !defined(ASAN_ENABLED) && !defined(MSAN_ENABLED) | ||
| 58 | pmatch[0].rm_so = 0; | ||
| 59 | pmatch[0].rm_eo = size; | ||
| 60 | return !regexec(re, buf, nmatch, pmatch, flags | REG_STARTEND); | ||
| 61 | #else | ||
| 62 | // Buffer must be null-terminated if REG_STARTEND isn't supported | ||
| 63 | char *tmp = xstrcut(buf, size); | ||
| 64 | int ret = !regexec(re, tmp, nmatch, pmatch, flags); | ||
| 65 | free(tmp); | ||
| 66 | return ret; | ||
| 67 | #endif | ||
| 68 | } | ||
| 69 | |||
| 70 | // Check which word boundary tokens are supported by regcomp(3) | ||
| 71 | // (if any) and initialize `rwbt` with them for later use | ||
| 72 | bool regexp_init_word_boundary_tokens(RegexpWordBoundaryTokens *rwbt) | ||
| 73 | { | ||
| 74 | static const char text[] = "SSfooEE SSfoo fooEE foo SSfooEE"; | ||
| 75 | const regoff_t match_start = 20, match_end = 23; | ||
| 76 | static const RegexpWordBoundaryTokens pairs[] = { | ||
| 77 | {"\\<", "\\>"}, | ||
| 78 | {"[[:<:]]", "[[:>:]]"}, | ||
| 79 | {"\\b", "\\b"}, | ||
| 80 | }; | ||
| 81 | |||
| 82 | BUG_ON(ARRAYLEN(text) <= match_end); | ||
| 83 | BUG_ON(!mem_equal(text + match_start - 1, " foo ", 5)); | ||
| 84 | |||
| 85 | for (size_t i = 0; i < ARRAYLEN(pairs); i++) { | ||
| 86 | const char *start = pairs[i].start; | ||
| 87 | const char *end = pairs[i].end; | ||
| 88 | char patt[32]; | ||
| 89 | xsnprintf(patt, sizeof(patt), "%s(foo)%s", start, end); | ||
| 90 | regex_t re; | ||
| 91 | if (regcomp(&re, patt, DEFAULT_REGEX_FLAGS) != 0) { | ||
| 92 | continue; | ||
| 93 | } | ||
| 94 | regmatch_t m[2]; | ||
| 95 | bool match = !regexec(&re, text, ARRAYLEN(m), m, 0); | ||
| 96 | regfree(&re); | ||
| 97 | if (match && m[0].rm_so == match_start && m[0].rm_eo == match_end) { | ||
| 98 | *rwbt = pairs[i]; | ||
| 99 | return true; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | |||
| 103 | return false; | ||
| 104 | } | ||
| 105 | |||
| 106 | void free_cached_regexp(CachedRegexp *cr) | ||
| 107 | { | ||
| 108 | regfree(&cr->re); | ||
| 109 | free(cr); | ||
| 110 | } | ||
| 111 | |||
| 112 | const InternedRegexp *regexp_intern(const char *pattern) | ||
| 113 | { | ||
| 114 | if (pattern[0] == '\0') { | ||
| 115 | return NULL; | ||
| 116 | } | ||
| 117 | |||
| 118 | InternedRegexp *ir = hashmap_get(&interned_regexps, pattern); | ||
| 119 | if (ir) { | ||
| 120 | return ir; | ||
| 121 | } | ||
| 122 | |||
| 123 | ir = xnew(InternedRegexp, 1); | ||
| 124 | int err = regcomp(&ir->re, pattern, DEFAULT_REGEX_FLAGS | REG_NEWLINE | REG_NOSUB); | ||
| 125 | if (unlikely(err)) { | ||
| 126 | regexp_error_msg(&ir->re, pattern, err); | ||
| 127 | free(ir); | ||
| 128 | return NULL; | ||
| 129 | } | ||
| 130 | |||
| 131 | ir->str = xstrdup(pattern); | ||
| 132 | return hashmap_insert(&interned_regexps, ir->str, ir); | ||
| 133 | } | ||
| 134 | |||
| 135 | bool regexp_is_interned(const char *pattern) | ||
| 136 | { | ||
| 137 | return !!hashmap_find(&interned_regexps, pattern); | ||
| 138 | } | ||
| 139 | |||
| 140 | // Note: this does NOT free InternedRegexp::str, because it points at the | ||
| 141 | // same string as HashMapEntry::key and is already freed by hashmap_free() | ||
| 142 | static void free_interned_regexp(InternedRegexp *ir) | ||
| 143 | { | ||
| 144 | regfree(&ir->re); | ||
| 145 | free(ir); | ||
| 146 | } | ||
| 147 | |||
| 148 | void free_interned_regexps(void) | ||
| 149 | { | ||
| 150 | hashmap_free(&interned_regexps, (FreeFunction)free_interned_regexp); | ||
| 151 | } | ||
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 @@ | |||
| 1 | #ifndef REGEXP_H | ||
| 2 | #define REGEXP_H | ||
| 3 | |||
| 4 | #include <regex.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <stddef.h> | ||
| 7 | #include "util/macros.h" | ||
| 8 | |||
| 9 | enum { | ||
| 10 | #ifdef REG_ENHANCED | ||
| 11 | // The REG_ENHANCED flag enables various extensions on macOS | ||
| 12 | // (see "enhanced features" in re_format(7)). Most of these | ||
| 13 | // extensions are enabled by default on Linux (in both glibc | ||
| 14 | // and musl) without the need for any extra flags. | ||
| 15 | DEFAULT_REGEX_FLAGS = REG_EXTENDED | REG_ENHANCED, | ||
| 16 | #else | ||
| 17 | // POSIX Extended Regular Expressions (ERE) are used almost | ||
| 18 | // everywhere in this codebase, except where Basic Regular | ||
| 19 | // Expressions (BRE) are explicitly called for (most notably | ||
| 20 | // in search_tag(), which is used for ctags patterns). | ||
| 21 | DEFAULT_REGEX_FLAGS = REG_EXTENDED, | ||
| 22 | #endif | ||
| 23 | }; | ||
| 24 | |||
| 25 | typedef struct { | ||
| 26 | regex_t re; | ||
| 27 | char str[]; | ||
| 28 | } CachedRegexp; | ||
| 29 | |||
| 30 | typedef struct { | ||
| 31 | char *str; | ||
| 32 | regex_t re; | ||
| 33 | } InternedRegexp; | ||
| 34 | |||
| 35 | // Platform-specific patterns for matching word boundaries, as detected | ||
| 36 | // and initialized by regexp_init_word_boundary_tokens() | ||
| 37 | typedef struct { | ||
| 38 | char start[8]; | ||
| 39 | char end[8]; | ||
| 40 | } RegexpWordBoundaryTokens; | ||
| 41 | |||
| 42 | bool regexp_compile_internal(regex_t *re, const char *pattern, int flags) WARN_UNUSED_RESULT; | ||
| 43 | |||
| 44 | WARN_UNUSED_RESULT | ||
| 45 | static inline bool regexp_compile(regex_t *re, const char *pattern, int flags) | ||
| 46 | { | ||
| 47 | return regexp_compile_internal(re, pattern, flags | DEFAULT_REGEX_FLAGS); | ||
| 48 | } | ||
| 49 | |||
| 50 | WARN_UNUSED_RESULT | ||
| 51 | static inline bool regexp_compile_basic(regex_t *re, const char *pattern, int flags) | ||
| 52 | { | ||
| 53 | return regexp_compile_internal(re, pattern, flags); | ||
| 54 | } | ||
| 55 | |||
| 56 | WARN_UNUSED_RESULT | ||
| 57 | static inline bool regexp_is_valid(const char *pattern, int flags) | ||
| 58 | { | ||
| 59 | regex_t re; | ||
| 60 | if (!regexp_compile(&re, pattern, flags | REG_NOSUB)) { | ||
| 61 | return false; | ||
| 62 | } | ||
| 63 | regfree(&re); | ||
| 64 | return true; | ||
| 65 | } | ||
| 66 | |||
| 67 | void regexp_compile_or_fatal_error(regex_t *re, const char *pattern, int flags); | ||
| 68 | bool regexp_init_word_boundary_tokens(RegexpWordBoundaryTokens *rwbt); | ||
| 69 | bool regexp_error_msg(const regex_t *re, const char *pattern, int err); | ||
| 70 | void free_cached_regexp(CachedRegexp *cr); | ||
| 71 | |||
| 72 | const InternedRegexp *regexp_intern(const char *pattern); | ||
| 73 | bool regexp_is_interned(const char *pattern); | ||
| 74 | void free_interned_regexps(void); | ||
| 75 | |||
| 76 | bool regexp_exec ( | ||
| 77 | const regex_t *re, | ||
| 78 | const char *buf, | ||
| 79 | size_t size, | ||
| 80 | size_t nmatch, | ||
| 81 | regmatch_t *pmatch, | ||
| 82 | int flags | ||
| 83 | ) WARN_UNUSED_RESULT; | ||
| 84 | |||
| 85 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include "replace.h" | ||
| 3 | #include "buffer.h" | ||
| 4 | #include "change.h" | ||
| 5 | #include "editor.h" | ||
| 6 | #include "error.h" | ||
| 7 | #include "regexp.h" | ||
| 8 | #include "screen.h" | ||
| 9 | #include "selection.h" | ||
| 10 | #include "util/debug.h" | ||
| 11 | #include "util/string.h" | ||
| 12 | #include "util/xmalloc.h" | ||
| 13 | #include "view.h" | ||
| 14 | #include "window.h" | ||
| 15 | |||
| 16 | static void build_replacement ( | ||
| 17 | String *buf, | ||
| 18 | const char *line, | ||
| 19 | const char *format, | ||
| 20 | const regmatch_t *matches | ||
| 21 | ) { | ||
| 22 | for (size_t i = 0; format[i]; ) { | ||
| 23 | char ch = format[i++]; | ||
| 24 | size_t match_idx; | ||
| 25 | if (ch == '\\') { | ||
| 26 | if (unlikely(format[i] == '\0')) { | ||
| 27 | break; | ||
| 28 | } | ||
| 29 | ch = format[i++]; | ||
| 30 | if (ch < '1' || ch > '9') { | ||
| 31 | string_append_byte(buf, ch); | ||
| 32 | continue; | ||
| 33 | } | ||
| 34 | match_idx = ch - '0'; | ||
| 35 | } else if (ch == '&') { | ||
| 36 | match_idx = 0; | ||
| 37 | } else { | ||
| 38 | string_append_byte(buf, ch); | ||
| 39 | continue; | ||
| 40 | } | ||
| 41 | const regmatch_t *match = &matches[match_idx]; | ||
| 42 | regoff_t len = match->rm_eo - match->rm_so; | ||
| 43 | if (len > 0) { | ||
| 44 | string_append_buf(buf, line + match->rm_so, (size_t)len); | ||
| 45 | } | ||
| 46 | } | ||
| 47 | } | ||
| 48 | |||
| 49 | /* | ||
| 50 | * s/abc/x | ||
| 51 | * | ||
| 52 | * string to match against | ||
| 53 | * ------------------------------------------- | ||
| 54 | * "foo abc bar abc baz" "foo abc bar abc baz" | ||
| 55 | * "foo x bar abc baz" " bar abc baz" | ||
| 56 | */ | ||
| 57 | static unsigned int replace_on_line ( | ||
| 58 | View *view, | ||
| 59 | StringView *line, | ||
| 60 | regex_t *re, | ||
| 61 | const char *format, | ||
| 62 | BlockIter *bi, | ||
| 63 | ReplaceFlags *flagsp | ||
| 64 | ) { | ||
| 65 | const unsigned char *buf = line->data; | ||
| 66 | unsigned char *alloc = NULL; | ||
| 67 | EditorState *e = view->window->editor; | ||
| 68 | ReplaceFlags flags = *flagsp; | ||
| 69 | regmatch_t matches[32]; | ||
| 70 | size_t pos = 0; | ||
| 71 | int eflags = 0; | ||
| 72 | unsigned int nr = 0; | ||
| 73 | |||
| 74 | while (regexp_exec ( | ||
| 75 | re, | ||
| 76 | buf + pos, | ||
| 77 | line->length - pos, | ||
| 78 | ARRAYLEN(matches), | ||
| 79 | matches, | ||
| 80 | eflags | ||
| 81 | )) { | ||
| 82 | regoff_t match_len = matches[0].rm_eo - matches[0].rm_so; | ||
| 83 | bool skip = false; | ||
| 84 | |||
| 85 | // Move cursor to beginning of the text to replace | ||
| 86 | block_iter_skip_bytes(bi, matches[0].rm_so); | ||
| 87 | view->cursor = *bi; | ||
| 88 | |||
| 89 | if (flags & REPLACE_CONFIRM) { | ||
| 90 | switch (status_prompt(e, "Replace? [Y/n/a/q]", "ynaq")) { | ||
| 91 | case 'y': | ||
| 92 | break; | ||
| 93 | case 'n': | ||
| 94 | skip = true; | ||
| 95 | break; | ||
| 96 | case 'a': | ||
| 97 | flags &= ~REPLACE_CONFIRM; | ||
| 98 | *flagsp = flags; | ||
| 99 | |||
| 100 | // Record rest of the changes as one chain | ||
| 101 | begin_change_chain(); | ||
| 102 | break; | ||
| 103 | case 'q': | ||
| 104 | case 0: | ||
| 105 | *flagsp = flags | REPLACE_CANCEL; | ||
| 106 | goto out; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | if (skip) { | ||
| 111 | // Move cursor after the matched text | ||
| 112 | block_iter_skip_bytes(&view->cursor, match_len); | ||
| 113 | } else { | ||
| 114 | String b = STRING_INIT; | ||
| 115 | build_replacement(&b, buf + pos, format, matches); | ||
| 116 | |||
| 117 | // line ref is invalidated by modification | ||
| 118 | if (buf == line->data && line->length != 0) { | ||
| 119 | BUG_ON(alloc); | ||
| 120 | alloc = xmemdup(buf, line->length); | ||
| 121 | buf = alloc; | ||
| 122 | } | ||
| 123 | |||
| 124 | buffer_replace_bytes(view, match_len, b.buffer, b.len); | ||
| 125 | nr++; | ||
| 126 | |||
| 127 | // Update selection length | ||
| 128 | if (view->selection) { | ||
| 129 | view->sel_eo += b.len; | ||
| 130 | view->sel_eo -= match_len; | ||
| 131 | } | ||
| 132 | |||
| 133 | // Move cursor after the replaced text | ||
| 134 | block_iter_skip_bytes(&view->cursor, b.len); | ||
| 135 | string_free(&b); | ||
| 136 | } | ||
| 137 | *bi = view->cursor; | ||
| 138 | |||
| 139 | if (!match_len) { | ||
| 140 | break; | ||
| 141 | } | ||
| 142 | |||
| 143 | if (!(flags & REPLACE_GLOBAL)) { | ||
| 144 | break; | ||
| 145 | } | ||
| 146 | |||
| 147 | pos += matches[0].rm_so + match_len; | ||
| 148 | |||
| 149 | // Don't match beginning of line again | ||
| 150 | eflags = REG_NOTBOL; | ||
| 151 | } | ||
| 152 | |||
| 153 | out: | ||
| 154 | free(alloc); | ||
| 155 | return nr; | ||
| 156 | } | ||
| 157 | |||
| 158 | bool reg_replace(View *view, const char *pattern, const char *format, ReplaceFlags flags) | ||
| 159 | { | ||
| 160 | if (unlikely(pattern[0] == '\0')) { | ||
| 161 | return error_msg("Search pattern must contain at least 1 character"); | ||
| 162 | } | ||
| 163 | |||
| 164 | int re_flags = REG_NEWLINE; | ||
| 165 | re_flags |= (flags & REPLACE_IGNORE_CASE) ? REG_ICASE : 0; | ||
| 166 | re_flags |= (flags & REPLACE_BASIC) ? 0 : DEFAULT_REGEX_FLAGS; | ||
| 167 | |||
| 168 | regex_t re; | ||
| 169 | if (unlikely(!regexp_compile_internal(&re, pattern, re_flags))) { | ||
| 170 | return false; | ||
| 171 | } | ||
| 172 | |||
| 173 | BlockIter bi = block_iter(view->buffer); | ||
| 174 | size_t nr_bytes; | ||
| 175 | bool swapped = false; | ||
| 176 | if (view->selection) { | ||
| 177 | SelectionInfo info; | ||
| 178 | init_selection(view, &info); | ||
| 179 | view->cursor = info.si; | ||
| 180 | view->sel_so = info.so; | ||
| 181 | view->sel_eo = info.eo; | ||
| 182 | swapped = info.swapped; | ||
| 183 | bi = view->cursor; | ||
| 184 | nr_bytes = info.eo - info.so; | ||
| 185 | } else { | ||
| 186 | BlockIter eof = bi; | ||
| 187 | block_iter_eof(&eof); | ||
| 188 | nr_bytes = block_iter_get_offset(&eof); | ||
| 189 | } | ||
| 190 | |||
| 191 | // Record multiple changes as one chain only when replacing all | ||
| 192 | if (!(flags & REPLACE_CONFIRM)) { | ||
| 193 | begin_change_chain(); | ||
| 194 | } | ||
| 195 | |||
| 196 | unsigned int nr_substitutions = 0; | ||
| 197 | size_t nr_lines = 0; | ||
| 198 | while (1) { | ||
| 199 | StringView line; | ||
| 200 | fill_line_ref(&bi, &line); | ||
| 201 | |||
| 202 | // Number of bytes to process | ||
| 203 | size_t count = line.length; | ||
| 204 | if (line.length > nr_bytes) { | ||
| 205 | // End of selection is not full line | ||
| 206 | line.length = nr_bytes; | ||
| 207 | } | ||
| 208 | |||
| 209 | unsigned int nr = replace_on_line(view, &line, &re, format, &bi, &flags); | ||
| 210 | if (nr) { | ||
| 211 | nr_substitutions += nr; | ||
| 212 | nr_lines++; | ||
| 213 | } | ||
| 214 | |||
| 215 | if (flags & REPLACE_CANCEL || count + 1 >= nr_bytes) { | ||
| 216 | break; | ||
| 217 | } | ||
| 218 | |||
| 219 | nr_bytes -= count + 1; | ||
| 220 | block_iter_next_line(&bi); | ||
| 221 | } | ||
| 222 | |||
| 223 | if (!(flags & REPLACE_CONFIRM)) { | ||
| 224 | end_change_chain(view); | ||
| 225 | } | ||
| 226 | |||
| 227 | regfree(&re); | ||
| 228 | |||
| 229 | if (nr_substitutions) { | ||
| 230 | info_msg ( | ||
| 231 | "%u substitution%s on %zu line%s", | ||
| 232 | nr_substitutions, | ||
| 233 | (nr_substitutions > 1) ? "s" : "", | ||
| 234 | nr_lines, | ||
| 235 | (nr_lines > 1) ? "s" : "" | ||
| 236 | ); | ||
| 237 | } else if (!(flags & REPLACE_CANCEL)) { | ||
| 238 | error_msg("Pattern '%s' not found", pattern); | ||
| 239 | } | ||
| 240 | |||
| 241 | if (view->selection) { | ||
| 242 | // Undo what init_selection() did | ||
| 243 | if (view->sel_eo) { | ||
| 244 | view->sel_eo--; | ||
| 245 | } | ||
| 246 | if (swapped) { | ||
| 247 | ssize_t tmp = view->sel_so; | ||
| 248 | view->sel_so = view->sel_eo; | ||
| 249 | view->sel_eo = tmp; | ||
| 250 | } | ||
| 251 | block_iter_goto_offset(&view->cursor, view->sel_eo); | ||
| 252 | view->sel_eo = SEL_EO_RECALC; | ||
| 253 | } | ||
| 254 | |||
| 255 | return (nr_substitutions > 0); | ||
| 256 | } | ||
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 @@ | |||
| 1 | #ifndef REPLACE_H | ||
| 2 | #define REPLACE_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "util/macros.h" | ||
| 6 | #include "view.h" | ||
| 7 | |||
| 8 | typedef enum { | ||
| 9 | REPLACE_CONFIRM = 1 << 0, | ||
| 10 | REPLACE_GLOBAL = 1 << 1, | ||
| 11 | REPLACE_IGNORE_CASE = 1 << 2, | ||
| 12 | REPLACE_BASIC = 1 << 3, | ||
| 13 | REPLACE_CANCEL = 1 << 4, | ||
| 14 | } ReplaceFlags; | ||
| 15 | |||
| 16 | bool reg_replace(View *view, const char *pattern, const char *format, ReplaceFlags flags) NONNULL_ARGS; | ||
| 17 | |||
| 18 | #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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | #include "error.h" | ||
| 3 | #include "search.h" | ||
| 4 | |||
| 5 | static void print_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error) | ||
| 6 | { | ||
| 7 | BuiltinColorEnum c = BC_COMMANDLINE; | ||
| 8 | if (msg[0]) { | ||
| 9 | c = is_error ? BC_ERRORMSG : BC_INFOMSG; | ||
| 10 | } | ||
| 11 | |||
| 12 | TermOutputBuffer *obuf = &term->obuf; | ||
| 13 | set_builtin_color(term, colors, c); | ||
| 14 | |||
| 15 | for (size_t i = 0; msg[i]; ) { | ||
| 16 | CodePoint u = u_get_char(msg, i + 4, &i); | ||
| 17 | if (!term_put_char(obuf, u)) { | ||
| 18 | break; | ||
| 19 | } | ||
| 20 | } | ||
| 21 | } | ||
| 22 | |||
| 23 | void show_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error) | ||
| 24 | { | ||
| 25 | term_output_reset(term, 0, term->width, 0); | ||
| 26 | term_move_cursor(&term->obuf, 0, term->height - 1); | ||
| 27 | print_message(term, colors, msg, is_error); | ||
| 28 | term_clear_eol(term); | ||
| 29 | } | ||
| 30 | |||
| 31 | static size_t print_command(Terminal *term, const ColorScheme *colors, const CommandLine *cmdline, char prefix) | ||
| 32 | { | ||
| 33 | const String *buf = &cmdline->buf; | ||
| 34 | TermOutputBuffer *obuf = &term->obuf; | ||
| 35 | |||
| 36 | // Width of characters up to and including cursor position | ||
| 37 | size_t w = 1; // ":" (prefix) | ||
| 38 | |||
| 39 | for (size_t i = 0; i <= cmdline->pos && i < buf->len; ) { | ||
| 40 | CodePoint u = u_get_char(buf->buffer, buf->len, &i); | ||
| 41 | w += u_char_width(u); | ||
| 42 | } | ||
| 43 | if (cmdline->pos == buf->len) { | ||
| 44 | w++; | ||
| 45 | } | ||
| 46 | if (w > term->width) { | ||
| 47 | obuf->scroll_x = w - term->width; | ||
| 48 | } | ||
| 49 | |||
| 50 | set_builtin_color(term, colors, BC_COMMANDLINE); | ||
| 51 | term_put_char(obuf, prefix); | ||
| 52 | |||
| 53 | size_t x = obuf->x - obuf->scroll_x; | ||
| 54 | for (size_t i = 0; i < buf->len; ) { | ||
| 55 | BUG_ON(obuf->x > obuf->scroll_x + obuf->width); | ||
| 56 | CodePoint u = u_get_char(buf->buffer, buf->len, &i); | ||
| 57 | if (!term_put_char(obuf, u)) { | ||
| 58 | break; | ||
| 59 | } | ||
| 60 | if (i <= cmdline->pos) { | ||
| 61 | x = obuf->x - obuf->scroll_x; | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | return x; | ||
| 66 | } | ||
| 67 | |||
| 68 | void update_command_line(EditorState *e) | ||
| 69 | { | ||
| 70 | Terminal *term = &e->terminal; | ||
| 71 | char prefix = ':'; | ||
| 72 | term_output_reset(term, 0, term->width, 0); | ||
| 73 | term_move_cursor(&term->obuf, 0, term->height - 1); | ||
| 74 | switch (e->input_mode) { | ||
| 75 | case INPUT_NORMAL: { | ||
| 76 | bool msg_is_error; | ||
| 77 | const char *msg = get_msg(&msg_is_error); | ||
| 78 | print_message(term, &e->colors, msg, msg_is_error); | ||
| 79 | break; | ||
| 80 | } | ||
| 81 | case INPUT_SEARCH: | ||
| 82 | prefix = e->search.reverse ? '?' : '/'; | ||
| 83 | // Fallthrough | ||
| 84 | case INPUT_COMMAND: | ||
| 85 | e->cmdline_x = print_command(term, &e->colors, &e->cmdline, prefix); | ||
| 86 | break; | ||
| 87 | default: | ||
| 88 | BUG("unhandled input mode"); | ||
| 89 | } | ||
| 90 | term_clear_eol(term); | ||
| 91 | } | ||
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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | #include "signals.h" | ||
| 3 | #include "terminal/input.h" | ||
| 4 | |||
| 5 | static char get_choice(Terminal *term, const char *choices, unsigned int esc_timeout) | ||
| 6 | { | ||
| 7 | KeyCode key = term_read_key(term, esc_timeout); | ||
| 8 | if (key == KEY_NONE) { | ||
| 9 | return 0; | ||
| 10 | } | ||
| 11 | |||
| 12 | switch (key) { | ||
| 13 | case KEY_BRACKETED_PASTE: | ||
| 14 | case KEY_DETECTED_PASTE: | ||
| 15 | term_discard_paste(&term->ibuf, key == KEY_BRACKETED_PASTE); | ||
| 16 | return 0; | ||
| 17 | case MOD_CTRL | 'c': | ||
| 18 | case MOD_CTRL | 'g': | ||
| 19 | case MOD_CTRL | '[': | ||
| 20 | return 0x18; // Cancel | ||
| 21 | case KEY_ENTER: | ||
| 22 | return choices[0]; // Default | ||
| 23 | } | ||
| 24 | |||
| 25 | if (key < 128) { | ||
| 26 | char ch = ascii_tolower(key); | ||
| 27 | if (strchr(choices, ch)) { | ||
| 28 | return ch; | ||
| 29 | } | ||
| 30 | } | ||
| 31 | return 0; | ||
| 32 | } | ||
| 33 | |||
| 34 | static void show_dialog ( | ||
| 35 | EditorState *e, | ||
| 36 | const TermColor *text_color, | ||
| 37 | const char *question | ||
| 38 | ) { | ||
| 39 | Terminal *term = &e->terminal; | ||
| 40 | unsigned int question_width = u_str_width(question); | ||
| 41 | unsigned int min_width = question_width + 2; | ||
| 42 | if (term->height < 12 || term->width < min_width) { | ||
| 43 | return; | ||
| 44 | } | ||
| 45 | |||
| 46 | unsigned int height = term->height / 4; | ||
| 47 | unsigned int mid = term->height / 2; | ||
| 48 | unsigned int top = mid - (height / 2); | ||
| 49 | unsigned int bot = top + height; | ||
| 50 | unsigned int width = MAX(term->width / 2, min_width); | ||
| 51 | unsigned int x = (term->width - width) / 2; | ||
| 52 | |||
| 53 | // The "underline" and "strikethrough" attributes should only apply | ||
| 54 | // to the text, not the whole dialog background: | ||
| 55 | TermColor dialog_color = *text_color; | ||
| 56 | TermOutputBuffer *obuf = &term->obuf; | ||
| 57 | dialog_color.attr &= ~(ATTR_UNDERLINE | ATTR_STRIKETHROUGH); | ||
| 58 | set_color(term, &e->colors, &dialog_color); | ||
| 59 | |||
| 60 | for (unsigned int y = top; y < bot; y++) { | ||
| 61 | term_output_reset(term, x, width, 0); | ||
| 62 | term_move_cursor(obuf, x, y); | ||
| 63 | if (y == mid) { | ||
| 64 | term_set_bytes(term, ' ', (width - question_width) / 2); | ||
| 65 | set_color(term, &e->colors, text_color); | ||
| 66 | term_add_str(obuf, question); | ||
| 67 | set_color(term, &e->colors, &dialog_color); | ||
| 68 | } | ||
| 69 | term_clear_eol(term); | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | char dialog_prompt(EditorState *e, const char *question, const char *choices) | ||
| 74 | { | ||
| 75 | const TermColor *color = &e->colors.builtin[BC_DIALOG]; | ||
| 76 | Terminal *term = &e->terminal; | ||
| 77 | TermOutputBuffer *obuf = &term->obuf; | ||
| 78 | |||
| 79 | normal_update(e); | ||
| 80 | term_hide_cursor(term); | ||
| 81 | show_dialog(e, color, question); | ||
| 82 | show_message(term, &e->colors, question, false); | ||
| 83 | term_output_flush(obuf); | ||
| 84 | |||
| 85 | unsigned int esc_timeout = e->options.esc_timeout; | ||
| 86 | char choice; | ||
| 87 | while ((choice = get_choice(term, choices, esc_timeout)) == 0) { | ||
| 88 | if (!resized) { | ||
| 89 | continue; | ||
| 90 | } | ||
| 91 | ui_resize(e); | ||
| 92 | term_hide_cursor(term); | ||
| 93 | show_dialog(e, color, question); | ||
| 94 | show_message(term, &e->colors, question, false); | ||
| 95 | term_output_flush(obuf); | ||
| 96 | } | ||
| 97 | |||
| 98 | mark_everything_changed(e); | ||
| 99 | return (choice >= 'a') ? choice : 0; | ||
| 100 | } | ||
| 101 | |||
| 102 | char status_prompt(EditorState *e, const char *question, const char *choices) | ||
| 103 | { | ||
| 104 | // update_buffer_windows() assumes these have been called for current view | ||
| 105 | view_update_cursor_x(e->view); | ||
| 106 | view_update_cursor_y(e->view); | ||
| 107 | view_update(e->view, e->options.scroll_margin); | ||
| 108 | |||
| 109 | // Set changed_line_min and changed_line_max before calling update_range() | ||
| 110 | mark_all_lines_changed(e->buffer); | ||
| 111 | |||
| 112 | Terminal *term = &e->terminal; | ||
| 113 | start_update(term); | ||
| 114 | update_term_title(term, e->buffer, e->options.set_window_title); | ||
| 115 | update_buffer_windows(e, e->buffer); | ||
| 116 | show_message(term, &e->colors, question, false); | ||
| 117 | end_update(e); | ||
| 118 | |||
| 119 | unsigned int esc_timeout = e->options.esc_timeout; | ||
| 120 | char choice; | ||
| 121 | while ((choice = get_choice(term, choices, esc_timeout)) == 0) { | ||
| 122 | if (!resized) { | ||
| 123 | continue; | ||
| 124 | } | ||
| 125 | ui_resize(e); | ||
| 126 | term_hide_cursor(term); | ||
| 127 | show_message(term, &e->colors, question, false); | ||
| 128 | restore_cursor(e); | ||
| 129 | term_show_cursor(term); | ||
| 130 | term_output_flush(&term->obuf); | ||
| 131 | } | ||
| 132 | |||
| 133 | return (choice >= 'a') ? choice : 0; | ||
| 134 | } | ||
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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | #include "status.h" | ||
| 3 | |||
| 4 | void update_status_line(const Window *window) | ||
| 5 | { | ||
| 6 | EditorState *e = window->editor; | ||
| 7 | const GlobalOptions *opts = &e->options; | ||
| 8 | InputMode mode = e->input_mode; | ||
| 9 | char lbuf[512], rbuf[512]; | ||
| 10 | sf_format(window, opts, mode, lbuf, sizeof lbuf, opts->statusline_left); | ||
| 11 | sf_format(window, opts, mode, rbuf, sizeof rbuf, opts->statusline_right); | ||
| 12 | |||
| 13 | const ColorScheme *colors = &e->colors; | ||
| 14 | Terminal *term = &e->terminal; | ||
| 15 | TermOutputBuffer *obuf = &term->obuf; | ||
| 16 | size_t lw = u_str_width(lbuf); | ||
| 17 | size_t rw = u_str_width(rbuf); | ||
| 18 | int w = window->w; | ||
| 19 | static_assert_compatible_types(w, window->w); | ||
| 20 | term_output_reset(term, window->x, w, 0); | ||
| 21 | term_move_cursor(obuf, window->x, window->y + window->h - 1); | ||
| 22 | set_builtin_color(term, colors, BC_STATUSLINE); | ||
| 23 | |||
| 24 | if (lw + rw <= w) { | ||
| 25 | // Both fit | ||
| 26 | term_add_str(obuf, lbuf); | ||
| 27 | term_set_bytes(term, ' ', w - lw - rw); | ||
| 28 | term_add_str(obuf, rbuf); | ||
| 29 | } else if (lw <= w && rw <= w) { | ||
| 30 | // Both would fit separately, draw overlapping | ||
| 31 | term_add_str(obuf, lbuf); | ||
| 32 | obuf->x = w - rw; | ||
| 33 | term_move_cursor(obuf, window->x + w - rw, window->y + window->h - 1); | ||
| 34 | term_add_str(obuf, rbuf); | ||
| 35 | } else if (lw <= w) { | ||
| 36 | // Left fits | ||
| 37 | term_add_str(obuf, lbuf); | ||
| 38 | term_clear_eol(term); | ||
| 39 | } else if (rw <= w) { | ||
| 40 | // Right fits | ||
| 41 | term_set_bytes(term, ' ', w - rw); | ||
| 42 | term_add_str(obuf, rbuf); | ||
| 43 | } else { | ||
| 44 | term_clear_eol(term); | ||
| 45 | } | ||
| 46 | } | ||
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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | #include "util/numtostr.h" | ||
| 3 | #include "util/strtonum.h" | ||
| 4 | |||
| 5 | static size_t tab_title_width(size_t tab_number, const char *filename) | ||
| 6 | { | ||
| 7 | return 3 + size_str_width(tab_number) + u_str_width(filename); | ||
| 8 | } | ||
| 9 | |||
| 10 | static void update_tab_title_width(View *view, size_t tab_number) | ||
| 11 | { | ||
| 12 | size_t w = tab_title_width(tab_number, buffer_filename(view->buffer)); | ||
| 13 | view->tt_width = w; | ||
| 14 | view->tt_truncated_width = w; | ||
| 15 | } | ||
| 16 | |||
| 17 | static void update_first_tab_idx(Window *window) | ||
| 18 | { | ||
| 19 | size_t max_first_idx = window->views.count; | ||
| 20 | for (size_t w = 0; max_first_idx > 0; max_first_idx--) { | ||
| 21 | const View *view = window->views.ptrs[max_first_idx - 1]; | ||
| 22 | w += view->tt_truncated_width; | ||
| 23 | if (w > window->w) { | ||
| 24 | break; | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 28 | size_t min_first_idx = window->views.count; | ||
| 29 | for (size_t w = 0; min_first_idx > 0; min_first_idx--) { | ||
| 30 | const View *view = window->views.ptrs[min_first_idx - 1]; | ||
| 31 | if (w || view == window->view) { | ||
| 32 | w += view->tt_truncated_width; | ||
| 33 | } | ||
| 34 | if (w > window->w) { | ||
| 35 | break; | ||
| 36 | } | ||
| 37 | } | ||
| 38 | |||
| 39 | size_t idx = CLAMP(window->first_tab_idx, min_first_idx, max_first_idx); | ||
| 40 | window->first_tab_idx = idx; | ||
| 41 | } | ||
| 42 | |||
| 43 | static void calculate_tabbar(Window *window) | ||
| 44 | { | ||
| 45 | int total_w = 0; | ||
| 46 | for (size_t i = 0, n = window->views.count; i < n; i++) { | ||
| 47 | View *view = window->views.ptrs[i]; | ||
| 48 | if (view == window->view) { | ||
| 49 | // Make sure current tab is visible | ||
| 50 | window->first_tab_idx = MIN(i, window->first_tab_idx); | ||
| 51 | } | ||
| 52 | update_tab_title_width(view, i + 1); | ||
| 53 | total_w += view->tt_width; | ||
| 54 | } | ||
| 55 | |||
| 56 | if (total_w <= window->w) { | ||
| 57 | // All tabs fit without truncating | ||
| 58 | window->first_tab_idx = 0; | ||
| 59 | return; | ||
| 60 | } | ||
| 61 | |||
| 62 | // Truncate all wide tabs | ||
| 63 | total_w = 0; | ||
| 64 | int truncated_count = 0; | ||
| 65 | for (size_t i = 0, n = window->views.count; i < n; i++) { | ||
| 66 | View *view = window->views.ptrs[i]; | ||
| 67 | int truncated_w = 20; | ||
| 68 | if (view->tt_width > truncated_w) { | ||
| 69 | view->tt_truncated_width = truncated_w; | ||
| 70 | total_w += truncated_w; | ||
| 71 | truncated_count++; | ||
| 72 | } else { | ||
| 73 | total_w += view->tt_width; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | if (total_w > window->w) { | ||
| 78 | // Not all tabs fit even after truncating wide tabs | ||
| 79 | update_first_tab_idx(window); | ||
| 80 | return; | ||
| 81 | } | ||
| 82 | |||
| 83 | // All tabs fit after truncating wide tabs | ||
| 84 | int extra = window->w - total_w; | ||
| 85 | |||
| 86 | // Divide extra space between truncated tabs | ||
| 87 | while (extra > 0) { | ||
| 88 | BUG_ON(truncated_count == 0); | ||
| 89 | int extra_avg = extra / truncated_count; | ||
| 90 | int extra_mod = extra % truncated_count; | ||
| 91 | |||
| 92 | for (size_t i = 0, n = window->views.count; i < n; i++) { | ||
| 93 | View *view = window->views.ptrs[i]; | ||
| 94 | int add = view->tt_width - view->tt_truncated_width; | ||
| 95 | if (add == 0) { | ||
| 96 | continue; | ||
| 97 | } | ||
| 98 | |||
| 99 | int avail = extra_avg; | ||
| 100 | if (extra_mod) { | ||
| 101 | // This is needed for equal divide | ||
| 102 | if (extra_avg == 0) { | ||
| 103 | avail++; | ||
| 104 | extra_mod--; | ||
| 105 | } | ||
| 106 | } | ||
| 107 | if (add > avail) { | ||
| 108 | add = avail; | ||
| 109 | } else { | ||
| 110 | truncated_count--; | ||
| 111 | } | ||
| 112 | |||
| 113 | view->tt_truncated_width += add; | ||
| 114 | extra -= add; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | window->first_tab_idx = 0; | ||
| 119 | } | ||
| 120 | |||
| 121 | static void print_tab_title(Terminal *term, const ColorScheme *colors, const View *view, size_t idx) | ||
| 122 | { | ||
| 123 | const char *filename = buffer_filename(view->buffer); | ||
| 124 | int skip = view->tt_width - view->tt_truncated_width; | ||
| 125 | if (skip > 0) { | ||
| 126 | filename += u_skip_chars(filename, &skip); | ||
| 127 | } | ||
| 128 | |||
| 129 | const char *tab_number = uint_to_str((unsigned int)idx + 1); | ||
| 130 | TermOutputBuffer *obuf = &term->obuf; | ||
| 131 | bool is_active_tab = (view == view->window->view); | ||
| 132 | bool is_modified = buffer_modified(view->buffer); | ||
| 133 | bool left_overflow = (obuf->x == 0 && idx > 0); | ||
| 134 | |||
| 135 | set_builtin_color(term, colors, is_active_tab ? BC_ACTIVETAB : BC_INACTIVETAB); | ||
| 136 | term_put_char(obuf, left_overflow ? '<' : ' '); | ||
| 137 | term_add_str(obuf, tab_number); | ||
| 138 | term_put_char(obuf, is_modified ? '+' : ':'); | ||
| 139 | term_add_str(obuf, filename); | ||
| 140 | |||
| 141 | size_t ntabs = view->window->views.count; | ||
| 142 | bool right_overflow = (obuf->x == (obuf->width - 1) && idx < (ntabs - 1)); | ||
| 143 | term_put_char(obuf, right_overflow ? '>' : ' '); | ||
| 144 | } | ||
| 145 | |||
| 146 | void print_tabbar(Terminal *term, const ColorScheme *colors, Window *window) | ||
| 147 | { | ||
| 148 | TermOutputBuffer *obuf = &term->obuf; | ||
| 149 | term_output_reset(term, window->x, window->w, 0); | ||
| 150 | term_move_cursor(obuf, window->x, window->y); | ||
| 151 | calculate_tabbar(window); | ||
| 152 | |||
| 153 | size_t i = window->first_tab_idx; | ||
| 154 | size_t n = window->views.count; | ||
| 155 | for (; i < n; i++) { | ||
| 156 | const View *view = window->views.ptrs[i]; | ||
| 157 | if (obuf->x + view->tt_truncated_width > window->w) { | ||
| 158 | break; | ||
| 159 | } | ||
| 160 | print_tab_title(term, colors, view, i); | ||
| 161 | } | ||
| 162 | |||
| 163 | set_builtin_color(term, colors, BC_TABBAR); | ||
| 164 | |||
| 165 | if (i == n) { | ||
| 166 | term_clear_eol(term); | ||
| 167 | return; | ||
| 168 | } | ||
| 169 | |||
| 170 | while (obuf->x < obuf->width - 1) { | ||
| 171 | term_put_char(obuf, ' '); | ||
| 172 | } | ||
| 173 | if (obuf->x == obuf->width - 1) { | ||
| 174 | term_put_char(obuf, '>'); | ||
| 175 | } | ||
| 176 | } | ||
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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | #include "indent.h" | ||
| 3 | #include "selection.h" | ||
| 4 | #include "syntax/highlight.h" | ||
| 5 | #include "util/ascii.h" | ||
| 6 | #include "util/debug.h" | ||
| 7 | #include "util/str-util.h" | ||
| 8 | #include "util/utf8.h" | ||
| 9 | |||
| 10 | typedef struct { | ||
| 11 | const View *view; | ||
| 12 | size_t line_nr; | ||
| 13 | size_t offset; | ||
| 14 | ssize_t sel_so; | ||
| 15 | ssize_t sel_eo; | ||
| 16 | |||
| 17 | const unsigned char *line; | ||
| 18 | size_t size; | ||
| 19 | size_t pos; | ||
| 20 | size_t indent_size; | ||
| 21 | size_t trailing_ws_offset; | ||
| 22 | const TermColor **colors; | ||
| 23 | } LineInfo; | ||
| 24 | |||
| 25 | // Like mask_color() but can change bg color only if it has not been changed yet | ||
| 26 | static void mask_color2(const ColorScheme *colors, TermColor *color, const TermColor *over) | ||
| 27 | { | ||
| 28 | int32_t default_bg = colors->builtin[BC_DEFAULT].bg; | ||
| 29 | if (over->bg != COLOR_KEEP && (color->bg == default_bg || color->bg < 0)) { | ||
| 30 | color->bg = over->bg; | ||
| 31 | } | ||
| 32 | |||
| 33 | if (over->fg != COLOR_KEEP) { | ||
| 34 | color->fg = over->fg; | ||
| 35 | } | ||
| 36 | |||
| 37 | if (!(over->attr & ATTR_KEEP)) { | ||
| 38 | color->attr = over->attr; | ||
| 39 | } | ||
| 40 | } | ||
| 41 | |||
| 42 | static void mask_selection_and_current_line ( | ||
| 43 | const ColorScheme *colors, | ||
| 44 | const LineInfo *info, | ||
| 45 | TermColor *color | ||
| 46 | ) { | ||
| 47 | if (info->offset >= info->sel_so && info->offset < info->sel_eo) { | ||
| 48 | mask_color(color, &colors->builtin[BC_SELECTION]); | ||
| 49 | } else if (info->line_nr == info->view->cy) { | ||
| 50 | mask_color2(colors, color, &colors->builtin[BC_CURRENTLINE]); | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | static bool is_non_text(CodePoint u, bool display_special) | ||
| 55 | { | ||
| 56 | if (u == '\t') { | ||
| 57 | return display_special; | ||
| 58 | } | ||
| 59 | return u < 0x20 || u == 0x7F || u_is_unprintable(u); | ||
| 60 | } | ||
| 61 | |||
| 62 | static WhitespaceErrorFlags get_ws_error(const LocalOptions *opts) | ||
| 63 | { | ||
| 64 | WhitespaceErrorFlags taberrs = WSE_TAB_INDENT | WSE_TAB_AFTER_INDENT; | ||
| 65 | WhitespaceErrorFlags extra = opts->expand_tab ? taberrs : WSE_SPACE_INDENT; | ||
| 66 | return opts->ws_error | ((opts->ws_error & WSE_AUTO_INDENT) ? extra : 0); | ||
| 67 | } | ||
| 68 | |||
| 69 | static bool whitespace_error(const LineInfo *info, CodePoint u, size_t i) | ||
| 70 | { | ||
| 71 | const View *view = info->view; | ||
| 72 | WhitespaceErrorFlags flags = get_ws_error(&view->buffer->options); | ||
| 73 | WhitespaceErrorFlags trailing = flags & (WSE_TRAILING | WSE_ALL_TRAILING); | ||
| 74 | if (i >= info->trailing_ws_offset && trailing) { | ||
| 75 | // Trailing whitespace | ||
| 76 | if ( | ||
| 77 | // Cursor is not on this line | ||
| 78 | info->line_nr != view->cy | ||
| 79 | // or is positioned before any trailing whitespace | ||
| 80 | || view->cx < info->trailing_ws_offset | ||
| 81 | // or user explicitly wants trailing space under cursor highlighted | ||
| 82 | || flags & WSE_ALL_TRAILING | ||
| 83 | ) { | ||
| 84 | return true; | ||
| 85 | } | ||
| 86 | } | ||
| 87 | |||
| 88 | bool in_indent = (i < info->indent_size); | ||
| 89 | if (u == '\t') { | ||
| 90 | WhitespaceErrorFlags mask = in_indent ? WSE_TAB_INDENT : WSE_TAB_AFTER_INDENT; | ||
| 91 | return (flags & mask) != 0; | ||
| 92 | } | ||
| 93 | if (!in_indent) { | ||
| 94 | // All checks below here only apply to indentation | ||
| 95 | return false; | ||
| 96 | } | ||
| 97 | |||
| 98 | const char *line = info->line; | ||
| 99 | size_t pos = i; | ||
| 100 | size_t count = 0; | ||
| 101 | while (pos > 0 && line[pos - 1] == ' ') { | ||
| 102 | pos--; | ||
| 103 | } | ||
| 104 | while (pos < info->size && line[pos] == ' ') { | ||
| 105 | pos++; | ||
| 106 | count++; | ||
| 107 | } | ||
| 108 | |||
| 109 | WhitespaceErrorFlags mask; | ||
| 110 | if (count >= view->buffer->options.tab_width) { | ||
| 111 | // Spaces used instead of tab | ||
| 112 | mask = WSE_SPACE_INDENT; | ||
| 113 | } else if (pos < info->size && line[pos] == '\t') { | ||
| 114 | // Space before tab | ||
| 115 | mask = WSE_SPACE_INDENT; | ||
| 116 | } else { | ||
| 117 | // Less than tab width spaces at end of indentation | ||
| 118 | mask = WSE_SPACE_ALIGN; | ||
| 119 | } | ||
| 120 | return (flags & mask) != 0; | ||
| 121 | } | ||
| 122 | |||
| 123 | static CodePoint screen_next_char(EditorState *e, LineInfo *info) | ||
| 124 | { | ||
| 125 | size_t count, pos = info->pos; | ||
| 126 | CodePoint u = info->line[pos]; | ||
| 127 | TermColor color; | ||
| 128 | bool ws_error = false; | ||
| 129 | |||
| 130 | if (likely(u < 0x80)) { | ||
| 131 | info->pos++; | ||
| 132 | count = 1; | ||
| 133 | if (u == '\t' || u == ' ') { | ||
| 134 | ws_error = whitespace_error(info, u, pos); | ||
| 135 | } | ||
| 136 | } else { | ||
| 137 | u = u_get_nonascii(info->line, info->size, &info->pos); | ||
| 138 | count = info->pos - pos; | ||
| 139 | |||
| 140 | if ( | ||
| 141 | u_is_special_whitespace(u) // Highly annoying no-break space etc. | ||
| 142 | && (info->view->buffer->options.ws_error & WSE_SPECIAL) | ||
| 143 | ) { | ||
| 144 | ws_error = true; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | if (info->colors && info->colors[pos]) { | ||
| 149 | color = *info->colors[pos]; | ||
| 150 | } else { | ||
| 151 | color = e->colors.builtin[BC_DEFAULT]; | ||
| 152 | } | ||
| 153 | if (is_non_text(u, e->options.display_special)) { | ||
| 154 | mask_color(&color, &e->colors.builtin[BC_NONTEXT]); | ||
| 155 | } | ||
| 156 | if (ws_error) { | ||
| 157 | mask_color(&color, &e->colors.builtin[BC_WSERROR]); | ||
| 158 | } | ||
| 159 | mask_selection_and_current_line(&e->colors, info, &color); | ||
| 160 | set_color(&e->terminal, &e->colors, &color); | ||
| 161 | |||
| 162 | info->offset += count; | ||
| 163 | return u; | ||
| 164 | } | ||
| 165 | |||
| 166 | static void screen_skip_char(TermOutputBuffer *obuf, LineInfo *info) | ||
| 167 | { | ||
| 168 | CodePoint u = info->line[info->pos++]; | ||
| 169 | info->offset++; | ||
| 170 | if (likely(u < 0x80)) { | ||
| 171 | if (likely(!ascii_iscntrl(u))) { | ||
| 172 | obuf->x++; | ||
| 173 | } else if (u == '\t' && obuf->tab_mode != TAB_CONTROL) { | ||
| 174 | obuf->x = next_indent_width(obuf->x, obuf->tab_width); | ||
| 175 | } else { | ||
| 176 | // Control | ||
| 177 | obuf->x += 2; | ||
| 178 | } | ||
| 179 | } else { | ||
| 180 | size_t pos = info->pos; | ||
| 181 | info->pos--; | ||
| 182 | u = u_get_nonascii(info->line, info->size, &info->pos); | ||
| 183 | obuf->x += u_char_width(u); | ||
| 184 | info->offset += info->pos - pos; | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | static bool is_notice(const char *word, size_t len) | ||
| 189 | { | ||
| 190 | switch (len) { | ||
| 191 | case 3: return mem_equal(word, "XXX", 3); | ||
| 192 | case 4: return mem_equal(word, "TODO", 4); | ||
| 193 | case 5: return mem_equal(word, "FIXME", 5); | ||
| 194 | } | ||
| 195 | return false; | ||
| 196 | } | ||
| 197 | |||
| 198 | // Highlight certain words inside comments | ||
| 199 | static void hl_words(Terminal *term, const ColorScheme *colors, const LineInfo *info) | ||
| 200 | { | ||
| 201 | const TermColor *cc = find_color(colors, "comment"); | ||
| 202 | const TermColor *nc = find_color(colors, "notice"); | ||
| 203 | |||
| 204 | if (!info->colors || !cc || !nc) { | ||
| 205 | return; | ||
| 206 | } | ||
| 207 | |||
| 208 | size_t i = info->pos; | ||
| 209 | if (i >= info->size) { | ||
| 210 | return; | ||
| 211 | } | ||
| 212 | |||
| 213 | // Go to beginning of partially visible word inside comment | ||
| 214 | while (i > 0 && info->colors[i] == cc && is_word_byte(info->line[i])) { | ||
| 215 | i--; | ||
| 216 | } | ||
| 217 | |||
| 218 | // This should be more than enough. I'm too lazy to iterate characters | ||
| 219 | // instead of bytes and calculate text width. | ||
| 220 | const size_t max = info->pos + term->width * 4 + 8; | ||
| 221 | |||
| 222 | size_t si; | ||
| 223 | while (i < info->size) { | ||
| 224 | if (info->colors[i] != cc || !is_word_byte(info->line[i])) { | ||
| 225 | if (i > max) { | ||
| 226 | break; | ||
| 227 | } | ||
| 228 | i++; | ||
| 229 | } else { | ||
| 230 | // Beginning of a word inside comment | ||
| 231 | si = i++; | ||
| 232 | while ( | ||
| 233 | i < info->size && info->colors[i] == cc | ||
| 234 | && is_word_byte(info->line[i]) | ||
| 235 | ) { | ||
| 236 | i++; | ||
| 237 | } | ||
| 238 | if (is_notice(info->line + si, i - si)) { | ||
| 239 | for (size_t j = si; j < i; j++) { | ||
| 240 | info->colors[j] = nc; | ||
| 241 | } | ||
| 242 | } | ||
| 243 | } | ||
| 244 | } | ||
| 245 | } | ||
| 246 | |||
| 247 | static void line_info_init ( | ||
| 248 | LineInfo *info, | ||
| 249 | const View *view, | ||
| 250 | const BlockIter *bi, | ||
| 251 | size_t line_nr | ||
| 252 | ) { | ||
| 253 | *info = (LineInfo) { | ||
| 254 | .view = view, | ||
| 255 | .line_nr = line_nr, | ||
| 256 | .offset = block_iter_get_offset(bi), | ||
| 257 | }; | ||
| 258 | |||
| 259 | if (!view->selection) { | ||
| 260 | info->sel_so = -1; | ||
| 261 | info->sel_eo = -1; | ||
| 262 | } else if (view->sel_eo != SEL_EO_RECALC) { | ||
| 263 | // Already calculated | ||
| 264 | info->sel_so = view->sel_so; | ||
| 265 | info->sel_eo = view->sel_eo; | ||
| 266 | BUG_ON(info->sel_so > info->sel_eo); | ||
| 267 | } else { | ||
| 268 | SelectionInfo sel; | ||
| 269 | init_selection(view, &sel); | ||
| 270 | info->sel_so = sel.so; | ||
| 271 | info->sel_eo = sel.eo; | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | static void line_info_set_line ( | ||
| 276 | LineInfo *info, | ||
| 277 | const StringView *line, | ||
| 278 | const TermColor **colors | ||
| 279 | ) { | ||
| 280 | BUG_ON(line->length == 0); | ||
| 281 | BUG_ON(line->data[line->length - 1] != '\n'); | ||
| 282 | |||
| 283 | info->line = line->data; | ||
| 284 | info->size = line->length - 1; | ||
| 285 | info->pos = 0; | ||
| 286 | info->colors = colors; | ||
| 287 | |||
| 288 | { | ||
| 289 | size_t i, n; | ||
| 290 | for (i = 0, n = info->size; i < n; i++) { | ||
| 291 | char ch = info->line[i]; | ||
| 292 | if (ch != '\t' && ch != ' ') { | ||
| 293 | break; | ||
| 294 | } | ||
| 295 | } | ||
| 296 | info->indent_size = i; | ||
| 297 | } | ||
| 298 | |||
| 299 | static_assert_compatible_types(info->trailing_ws_offset, size_t); | ||
| 300 | info->trailing_ws_offset = SIZE_MAX; | ||
| 301 | for (ssize_t i = info->size - 1; i >= 0; i--) { | ||
| 302 | char ch = info->line[i]; | ||
| 303 | if (ch != '\t' && ch != ' ') { | ||
| 304 | break; | ||
| 305 | } | ||
| 306 | info->trailing_ws_offset = i; | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | static void print_line(EditorState *e, LineInfo *info) | ||
| 311 | { | ||
| 312 | // Screen might be scrolled horizontally. Skip most invisible | ||
| 313 | // characters using screen_skip_char(), which is much faster than | ||
| 314 | // buf_skip(screen_next_char(info)). | ||
| 315 | // | ||
| 316 | // There can be a wide character (tab, control code etc.) that is | ||
| 317 | // partially visible and can't be skipped using screen_skip_char(). | ||
| 318 | Terminal *term = &e->terminal; | ||
| 319 | TermOutputBuffer *obuf = &term->obuf; | ||
| 320 | while (obuf->x + 8 < obuf->scroll_x && info->pos < info->size) { | ||
| 321 | screen_skip_char(obuf, info); | ||
| 322 | } | ||
| 323 | |||
| 324 | const ColorScheme *colors = &e->colors; | ||
| 325 | hl_words(term, colors, info); | ||
| 326 | |||
| 327 | while (info->pos < info->size) { | ||
| 328 | BUG_ON(obuf->x > obuf->scroll_x + obuf->width); | ||
| 329 | CodePoint u = screen_next_char(e, info); | ||
| 330 | if (!term_put_char(obuf, u)) { | ||
| 331 | // +1 for newline | ||
| 332 | info->offset += info->size - info->pos + 1; | ||
| 333 | return; | ||
| 334 | } | ||
| 335 | } | ||
| 336 | |||
| 337 | TermColor color; | ||
| 338 | if (e->options.display_special && obuf->x >= obuf->scroll_x) { | ||
| 339 | // Syntax highlighter highlights \n but use default color anyway | ||
| 340 | color = colors->builtin[BC_DEFAULT]; | ||
| 341 | mask_color(&color, &colors->builtin[BC_NONTEXT]); | ||
| 342 | mask_selection_and_current_line(colors, info, &color); | ||
| 343 | set_color(term, colors, &color); | ||
| 344 | term_put_char(obuf, '$'); | ||
| 345 | } | ||
| 346 | |||
| 347 | color = colors->builtin[BC_DEFAULT]; | ||
| 348 | mask_selection_and_current_line(colors, info, &color); | ||
| 349 | set_color(term, colors, &color); | ||
| 350 | info->offset++; | ||
| 351 | term_clear_eol(term); | ||
| 352 | } | ||
| 353 | |||
| 354 | void update_range(EditorState *e, const View *view, long y1, long y2) | ||
| 355 | { | ||
| 356 | const int edit_x = view->window->edit_x; | ||
| 357 | const int edit_y = view->window->edit_y; | ||
| 358 | const int edit_w = view->window->edit_w; | ||
| 359 | const int edit_h = view->window->edit_h; | ||
| 360 | |||
| 361 | Terminal *term = &e->terminal; | ||
| 362 | TermOutputBuffer *obuf = &term->obuf; | ||
| 363 | term_output_reset(term, edit_x, edit_w, view->vx); | ||
| 364 | obuf->tab_width = view->buffer->options.tab_width; | ||
| 365 | obuf->tab_mode = e->options.display_special ? TAB_SPECIAL : TAB_NORMAL; | ||
| 366 | |||
| 367 | BlockIter bi = view->cursor; | ||
| 368 | for (long i = 0, n = view->cy - y1; i < n; i++) { | ||
| 369 | block_iter_prev_line(&bi); | ||
| 370 | } | ||
| 371 | for (long i = 0, n = y1 - view->cy; i < n; i++) { | ||
| 372 | block_iter_eat_line(&bi); | ||
| 373 | } | ||
| 374 | block_iter_bol(&bi); | ||
| 375 | |||
| 376 | LineInfo info; | ||
| 377 | line_info_init(&info, view, &bi, y1); | ||
| 378 | |||
| 379 | y1 -= view->vy; | ||
| 380 | y2 -= view->vy; | ||
| 381 | |||
| 382 | bool got_line = !block_iter_is_eof(&bi); | ||
| 383 | hl_fill_start_states(view->buffer, &e->colors, info.line_nr); | ||
| 384 | long i; | ||
| 385 | for (i = y1; got_line && i < y2; i++) { | ||
| 386 | obuf->x = 0; | ||
| 387 | term_move_cursor(obuf, edit_x, edit_y + i); | ||
| 388 | |||
| 389 | StringView line; | ||
| 390 | fill_line_nl_ref(&bi, &line); | ||
| 391 | bool next_changed; | ||
| 392 | const TermColor **colors = hl_line(view->buffer, &e->colors, &line, info.line_nr, &next_changed); | ||
| 393 | line_info_set_line(&info, &line, colors); | ||
| 394 | print_line(e, &info); | ||
| 395 | |||
| 396 | got_line = !!block_iter_next_line(&bi); | ||
| 397 | info.line_nr++; | ||
| 398 | |||
| 399 | if (next_changed && i + 1 == y2 && y2 < edit_h) { | ||
| 400 | // More lines need to be updated not because their contents have | ||
| 401 | // changed but because their highlight state has | ||
| 402 | y2++; | ||
| 403 | } | ||
| 404 | } | ||
| 405 | |||
| 406 | if (i < y2 && info.line_nr == view->cy) { | ||
| 407 | // Dummy empty line is shown only if cursor is on it | ||
| 408 | TermColor color = e->colors.builtin[BC_DEFAULT]; | ||
| 409 | |||
| 410 | obuf->x = 0; | ||
| 411 | mask_color2(&e->colors, &color, &e->colors.builtin[BC_CURRENTLINE]); | ||
| 412 | set_color(term, &e->colors, &color); | ||
| 413 | |||
| 414 | term_move_cursor(obuf, edit_x, edit_y + i++); | ||
| 415 | term_clear_eol(term); | ||
| 416 | } | ||
| 417 | |||
| 418 | if (i < y2) { | ||
| 419 | set_builtin_color(term, &e->colors, BC_NOLINE); | ||
| 420 | } | ||
| 421 | for (; i < y2; i++) { | ||
| 422 | obuf->x = 0; | ||
| 423 | term_move_cursor(obuf, edit_x, edit_y + i); | ||
| 424 | term_put_char(obuf, '~'); | ||
| 425 | term_clear_eol(term); | ||
| 426 | } | ||
| 427 | } | ||
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 @@ | |||
| 1 | #include "screen.h" | ||
| 2 | |||
| 3 | static void print_separator(Window *window, void *ud) | ||
| 4 | { | ||
| 5 | Terminal *term = ud; | ||
| 6 | TermOutputBuffer *obuf = &term->obuf; | ||
| 7 | if (window->x + window->w == term->width) { | ||
| 8 | return; | ||
| 9 | } | ||
| 10 | for (int y = 0, h = window->h; y < h; y++) { | ||
| 11 | term_move_cursor(obuf, window->x + window->w, window->y + y); | ||
| 12 | term_add_byte(obuf, '|'); | ||
| 13 | } | ||
| 14 | } | ||
| 15 | |||
| 16 | static void update_separators(Terminal *term, const ColorScheme *colors, const Frame *frame) | ||
| 17 | { | ||
| 18 | set_builtin_color(term, colors, BC_STATUSLINE); | ||
| 19 | frame_for_each_window(frame, print_separator, term); | ||
| 20 | } | ||
| 21 | |||
| 22 | static void update_line_numbers(Terminal *term, const ColorScheme *colors, Window *window, bool force) | ||
| 23 | { | ||
| 24 | const View *view = window->view; | ||
| 25 | size_t lines = view->buffer->nl; | ||
| 26 | int x = window->x; | ||
| 27 | |||
| 28 | calculate_line_numbers(window); | ||
| 29 | long first = view->vy + 1; | ||
| 30 | long last = MIN(view->vy + window->edit_h, lines); | ||
| 31 | |||
| 32 | if ( | ||
| 33 | !force | ||
| 34 | && window->line_numbers.first == first | ||
| 35 | && window->line_numbers.last == last | ||
| 36 | ) { | ||
| 37 | return; | ||
| 38 | } | ||
| 39 | |||
| 40 | window->line_numbers.first = first; | ||
| 41 | window->line_numbers.last = last; | ||
| 42 | |||
| 43 | TermOutputBuffer *obuf = &term->obuf; | ||
| 44 | char buf[DECIMAL_STR_MAX(unsigned long) + 1]; | ||
| 45 | size_t width = window->line_numbers.width; | ||
| 46 | BUG_ON(width > sizeof(buf)); | ||
| 47 | BUG_ON(width < LINE_NUMBERS_MIN_WIDTH); | ||
| 48 | term_output_reset(term, window->x, window->w, 0); | ||
| 49 | set_builtin_color(term, colors, BC_LINENUMBER); | ||
| 50 | |||
| 51 | for (int y = 0, h = window->edit_h, edit_y = window->edit_y; y < h; y++) { | ||
| 52 | unsigned long line = view->vy + y + 1; | ||
| 53 | memset(buf, ' ', width); | ||
| 54 | if (line <= lines) { | ||
| 55 | size_t i = width - 2; | ||
| 56 | do { | ||
| 57 | buf[i--] = (line % 10) + '0'; | ||
| 58 | } while (line /= 10); | ||
| 59 | } | ||
| 60 | term_move_cursor(obuf, x, edit_y + y); | ||
| 61 | term_add_bytes(obuf, buf, width); | ||
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | static void update_window_full(Window *window, void* UNUSED_ARG(data)) | ||
| 66 | { | ||
| 67 | EditorState *e = window->editor; | ||
| 68 | View *view = window->view; | ||
| 69 | view_update_cursor_x(view); | ||
| 70 | view_update_cursor_y(view); | ||
| 71 | view_update(view, e->options.scroll_margin); | ||
| 72 | if (e->options.tab_bar) { | ||
| 73 | print_tabbar(&e->terminal, &e->colors, window); | ||
| 74 | } | ||
| 75 | if (e->options.show_line_numbers) { | ||
| 76 | update_line_numbers(&e->terminal, &e->colors, window, true); | ||
| 77 | } | ||
| 78 | update_range(e, view, view->vy, view->vy + window->edit_h); | ||
| 79 | update_status_line(window); | ||
| 80 | } | ||
| 81 | |||
| 82 | void update_all_windows(EditorState *e) | ||
| 83 | { | ||
| 84 | update_window_sizes(&e->terminal, e->root_frame); | ||
| 85 | frame_for_each_window(e->root_frame, update_window_full, NULL); | ||
| 86 | update_separators(&e->terminal, &e->colors, e->root_frame); | ||
| 87 | } | ||
| 88 | |||
| 89 | static void update_window(EditorState *e, Window *window) | ||
| 90 | { | ||
| 91 | if (e->options.tab_bar && window->update_tabbar) { | ||
| 92 | print_tabbar(&e->terminal, &e->colors, window); | ||
| 93 | } | ||
| 94 | |||
| 95 | const View *view = window->view; | ||
| 96 | if (e->options.show_line_numbers) { | ||
| 97 | // Force updating lines numbers if all lines changed | ||
| 98 | bool force = (view->buffer->changed_line_max == LONG_MAX); | ||
| 99 | update_line_numbers(&e->terminal, &e->colors, window, force); | ||
| 100 | } | ||
| 101 | |||
| 102 | long y1 = MAX(view->buffer->changed_line_min, view->vy); | ||
| 103 | long y2 = MIN(view->buffer->changed_line_max, view->vy + window->edit_h - 1); | ||
| 104 | update_range(e, view, y1, y2 + 1); | ||
| 105 | update_status_line(window); | ||
| 106 | } | ||
| 107 | |||
| 108 | // Update all visible views containing this buffer | ||
| 109 | void update_buffer_windows(EditorState *e, const Buffer *buffer) | ||
| 110 | { | ||
| 111 | const View *current_view = e->view; | ||
| 112 | for (size_t i = 0, n = buffer->views.count; i < n; i++) { | ||
| 113 | View *view = buffer->views.ptrs[i]; | ||
| 114 | if (view != view->window->view) { | ||
| 115 | // Not visible | ||
| 116 | continue; | ||
| 117 | } | ||
| 118 | if (view != current_view) { | ||
| 119 | // Restore cursor | ||
| 120 | view->cursor.blk = BLOCK(view->buffer->blocks.next); | ||
| 121 | block_iter_goto_offset(&view->cursor, view->saved_cursor_offset); | ||
| 122 | |||
| 123 | // These have already been updated for current view | ||
| 124 | view_update_cursor_x(view); | ||
| 125 | view_update_cursor_y(view); | ||
| 126 | view_update(view, e->options.scroll_margin); | ||
| 127 | } | ||
| 128 | update_window(e, view->window); | ||
| 129 | } | ||
| 130 | } | ||
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 @@ | |||
| 1 | #include <string.h> | ||
| 2 | #include "screen.h" | ||
| 3 | #include "frame.h" | ||
| 4 | #include "terminal/cursor.h" | ||
| 5 | #include "terminal/ioctl.h" | ||
| 6 | #include "util/log.h" | ||
| 7 | |||
| 8 | void set_color(Terminal *term, const ColorScheme *colors, const TermColor *color) | ||
| 9 | { | ||
| 10 | TermColor tmp = *color; | ||
| 11 | // NOTE: -2 (keep) is treated as -1 (default) | ||
| 12 | if (tmp.fg < 0) { | ||
| 13 | tmp.fg = colors->builtin[BC_DEFAULT].fg; | ||
| 14 | } | ||
| 15 | if (tmp.bg < 0) { | ||
| 16 | tmp.bg = colors->builtin[BC_DEFAULT].bg; | ||
| 17 | } | ||
| 18 | if (same_color(&tmp, &term->obuf.color)) { | ||
| 19 | return; | ||
| 20 | } | ||
| 21 | term_set_color(term, &tmp); | ||
| 22 | } | ||
| 23 | |||
| 24 | void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c) | ||
| 25 | { | ||
| 26 | set_color(term, colors, &colors->builtin[c]); | ||
| 27 | } | ||
| 28 | |||
| 29 | static void update_cursor_style(EditorState *e) | ||
| 30 | { | ||
| 31 | CursorInputMode mode; | ||
| 32 | switch (e->input_mode) { | ||
| 33 | case INPUT_NORMAL: | ||
| 34 | if (e->buffer->options.overwrite) { | ||
| 35 | mode = CURSOR_MODE_OVERWRITE; | ||
| 36 | } else { | ||
| 37 | mode = CURSOR_MODE_INSERT; | ||
| 38 | } | ||
| 39 | break; | ||
| 40 | case INPUT_COMMAND: | ||
| 41 | case INPUT_SEARCH: | ||
| 42 | mode = CURSOR_MODE_CMDLINE; | ||
| 43 | break; | ||
| 44 | default: | ||
| 45 | BUG("unhandled input mode"); | ||
| 46 | return; | ||
| 47 | } | ||
| 48 | |||
| 49 | TermCursorStyle style = e->cursor_styles[mode]; | ||
| 50 | TermCursorStyle def = e->cursor_styles[CURSOR_MODE_DEFAULT]; | ||
| 51 | if (style.type == CURSOR_KEEP) { | ||
| 52 | style.type = def.type; | ||
| 53 | } | ||
| 54 | if (style.color == COLOR_KEEP) { | ||
| 55 | style.color = def.color; | ||
| 56 | } | ||
| 57 | |||
| 58 | e->cursor_style_changed = false; | ||
| 59 | if (!same_cursor(&style, &e->terminal.obuf.cursor_style)) { | ||
| 60 | term_set_cursor_style(&e->terminal, style); | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 64 | void update_term_title(Terminal *term, const Buffer *buffer, bool set_window_title) | ||
| 65 | { | ||
| 66 | if (!set_window_title || !(term->features & TFLAG_SET_WINDOW_TITLE)) { | ||
| 67 | return; | ||
| 68 | } | ||
| 69 | |||
| 70 | // FIXME: title must not contain control characters | ||
| 71 | TermOutputBuffer *obuf = &term->obuf; | ||
| 72 | const char *filename = buffer_filename(buffer); | ||
| 73 | term_add_literal(obuf, "\033]2;"); | ||
| 74 | term_add_bytes(obuf, filename, strlen(filename)); | ||
| 75 | term_add_byte(obuf, ' '); | ||
| 76 | term_add_byte(obuf, buffer_modified(buffer) ? '+' : '-'); | ||
| 77 | term_add_literal(obuf, " dte\033\\"); | ||
| 78 | } | ||
| 79 | |||
| 80 | void mask_color(TermColor *color, const TermColor *over) | ||
| 81 | { | ||
| 82 | if (over->fg != COLOR_KEEP) { | ||
| 83 | color->fg = over->fg; | ||
| 84 | } | ||
| 85 | if (over->bg != COLOR_KEEP) { | ||
| 86 | color->bg = over->bg; | ||
| 87 | } | ||
| 88 | if (!(over->attr & ATTR_KEEP)) { | ||
| 89 | color->attr = over->attr; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | void restore_cursor(EditorState *e) | ||
| 94 | { | ||
| 95 | unsigned int x, y; | ||
| 96 | switch (e->input_mode) { | ||
| 97 | case INPUT_NORMAL: | ||
| 98 | x = e->window->edit_x + e->view->cx_display - e->view->vx; | ||
| 99 | y = e->window->edit_y + e->view->cy - e->view->vy; | ||
| 100 | break; | ||
| 101 | case INPUT_COMMAND: | ||
| 102 | case INPUT_SEARCH: | ||
| 103 | x = e->cmdline_x; | ||
| 104 | y = e->terminal.height - 1; | ||
| 105 | break; | ||
| 106 | default: | ||
| 107 | BUG("unhandled input mode"); | ||
| 108 | } | ||
| 109 | term_move_cursor(&e->terminal.obuf, x, y); | ||
| 110 | } | ||
| 111 | |||
| 112 | static void clear_update_tabbar(Window *window, void* UNUSED_ARG(data)) | ||
| 113 | { | ||
| 114 | window->update_tabbar = false; | ||
| 115 | } | ||
| 116 | |||
| 117 | void end_update(EditorState *e) | ||
| 118 | { | ||
| 119 | Terminal *term = &e->terminal; | ||
| 120 | restore_cursor(e); | ||
| 121 | term_show_cursor(term); | ||
| 122 | term_end_sync_update(term); | ||
| 123 | term_output_flush(&term->obuf); | ||
| 124 | |||
| 125 | e->buffer->changed_line_min = LONG_MAX; | ||
| 126 | e->buffer->changed_line_max = -1; | ||
| 127 | frame_for_each_window(e->root_frame, clear_update_tabbar, NULL); | ||
| 128 | } | ||
| 129 | |||
| 130 | void start_update(Terminal *term) | ||
| 131 | { | ||
| 132 | term_begin_sync_update(term); | ||
| 133 | term_hide_cursor(term); | ||
| 134 | } | ||
| 135 | |||
| 136 | void update_window_sizes(Terminal *term, Frame *frame) | ||
| 137 | { | ||
| 138 | set_frame_size(frame, term->width, term->height - 1); | ||
| 139 | update_window_coordinates(frame); | ||
| 140 | } | ||
| 141 | |||
| 142 | void update_screen_size(Terminal *term, Frame *root_frame) | ||
| 143 | { | ||
| 144 | unsigned int width, height; | ||
| 145 | if (!term_get_size(&width, &height)) { | ||
| 146 | return; | ||
| 147 | } | ||
| 148 | |||
| 149 | // TODO: remove minimum width/height and instead make update_screen() | ||
| 150 | // do something sensible when the terminal dimensions are tiny | ||
| 151 | term->width = MAX(width, 3); | ||
| 152 | term->height = MAX(height, 3); | ||
| 153 | |||
| 154 | update_window_sizes(term, root_frame); | ||
| 155 | LOG_INFO("terminal size: %ux%u", width, height); | ||
| 156 | } | ||
| 157 | |||
| 158 | NOINLINE | ||
| 159 | void normal_update(EditorState *e) | ||
| 160 | { | ||
| 161 | Terminal *term = &e->terminal; | ||
| 162 | start_update(term); | ||
| 163 | update_term_title(term, e->buffer, e->options.set_window_title); | ||
| 164 | update_all_windows(e); | ||
| 165 | update_command_line(e); | ||
| 166 | update_cursor_style(e); | ||
| 167 | end_update(e); | ||
| 168 | } | ||
| 169 | |||
| 170 | void update_screen(EditorState *e, const ScreenState *s) | ||
| 171 | { | ||
| 172 | if (e->everything_changed) { | ||
| 173 | e->everything_changed = false; | ||
| 174 | normal_update(e); | ||
| 175 | return; | ||
| 176 | } | ||
| 177 | |||
| 178 | Buffer *buffer = e->buffer; | ||
| 179 | View *view = e->view; | ||
| 180 | view_update_cursor_x(view); | ||
| 181 | view_update_cursor_y(view); | ||
| 182 | view_update(view, e->options.scroll_margin); | ||
| 183 | |||
| 184 | if (s->id == buffer->id) { | ||
| 185 | if (s->vx != view->vx || s->vy != view->vy) { | ||
| 186 | mark_all_lines_changed(buffer); | ||
| 187 | } else { | ||
| 188 | // Because of trailing whitespace highlighting and highlighting | ||
| 189 | // current line in different color, the lines cy (old cursor y) and | ||
| 190 | // view->cy need to be updated. Always update at least current line. | ||
| 191 | buffer_mark_lines_changed(buffer, s->cy, view->cy); | ||
| 192 | } | ||
| 193 | if (s->is_modified != buffer_modified(buffer)) { | ||
| 194 | mark_buffer_tabbars_changed(buffer); | ||
| 195 | } | ||
| 196 | } else { | ||
| 197 | e->window->update_tabbar = true; | ||
| 198 | mark_all_lines_changed(buffer); | ||
| 199 | } | ||
| 200 | |||
| 201 | start_update(&e->terminal); | ||
| 202 | if (e->window->update_tabbar) { | ||
| 203 | update_term_title(&e->terminal, e->buffer, e->options.set_window_title); | ||
| 204 | } | ||
| 205 | update_buffer_windows(e, buffer); | ||
| 206 | update_command_line(e); | ||
| 207 | if (e->cursor_style_changed) { | ||
| 208 | update_cursor_style(e); | ||
| 209 | } | ||
| 210 | end_update(e); | ||
| 211 | } | ||
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 @@ | |||
| 1 | #ifndef SCREEN_H | ||
| 2 | #define SCREEN_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "buffer.h" | ||
| 7 | #include "editor.h" | ||
| 8 | #include "syntax/color.h" | ||
| 9 | #include "terminal/output.h" | ||
| 10 | #include "terminal/terminal.h" | ||
| 11 | #include "util/debug.h" | ||
| 12 | #include "util/macros.h" | ||
| 13 | #include "util/utf8.h" | ||
| 14 | #include "view.h" | ||
| 15 | #include "window.h" | ||
| 16 | |||
| 17 | typedef struct { | ||
| 18 | bool is_modified; | ||
| 19 | unsigned long id; | ||
| 20 | long cy; | ||
| 21 | long vx; | ||
| 22 | long vy; | ||
| 23 | } ScreenState; | ||
| 24 | |||
| 25 | // screen.c | ||
| 26 | void update_screen(EditorState *e, const ScreenState *s); | ||
| 27 | void update_term_title(Terminal *term, const Buffer *buffer, bool set_window_title); | ||
| 28 | void update_window_sizes(Terminal *term, Frame *frame); | ||
| 29 | void update_screen_size(Terminal *term, Frame *root_frame); | ||
| 30 | void set_color(Terminal *term, const ColorScheme *colors, const TermColor *color); | ||
| 31 | void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c); | ||
| 32 | void mask_color(TermColor *color, const TermColor *over); | ||
| 33 | void start_update(Terminal *term); | ||
| 34 | void end_update(EditorState *e); | ||
| 35 | void normal_update(EditorState *e); | ||
| 36 | void restore_cursor(EditorState *e); | ||
| 37 | |||
| 38 | // screen-cmdline.c | ||
| 39 | void update_command_line(EditorState *e); | ||
| 40 | void show_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error); | ||
| 41 | |||
| 42 | // screen-tabbar.c | ||
| 43 | void print_tabbar(Terminal *term, const ColorScheme *colors, Window *window); | ||
| 44 | |||
| 45 | // screen-status.c | ||
| 46 | void update_status_line(const Window *window); | ||
| 47 | |||
| 48 | // screen-view.c | ||
| 49 | void update_range(EditorState *e, const View *view, long y1, long y2); | ||
| 50 | |||
| 51 | // screen-window.c | ||
| 52 | void update_all_windows(EditorState *e); | ||
| 53 | void update_buffer_windows(EditorState *e, const Buffer *buffer); | ||
| 54 | |||
| 55 | // screen-prompt.c | ||
| 56 | char status_prompt(EditorState *e, const char *question, const char *choices) NONNULL_ARGS; | ||
| 57 | char dialog_prompt(EditorState *e, const char *question, const char *choices) NONNULL_ARGS; | ||
| 58 | |||
| 59 | #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 @@ | |||
| 1 | #include <stdlib.h> | ||
| 2 | #include "search.h" | ||
| 3 | #include "block-iter.h" | ||
| 4 | #include "buffer.h" | ||
| 5 | #include "error.h" | ||
| 6 | #include "regexp.h" | ||
| 7 | #include "util/ascii.h" | ||
| 8 | #include "util/xmalloc.h" | ||
| 9 | |||
| 10 | static bool do_search_fwd(View *view, regex_t *regex, BlockIter *bi, bool skip) | ||
| 11 | { | ||
| 12 | int flags = block_iter_is_bol(bi) ? 0 : REG_NOTBOL; | ||
| 13 | |||
| 14 | do { | ||
| 15 | if (block_iter_is_eof(bi)) { | ||
| 16 | return false; | ||
| 17 | } | ||
| 18 | |||
| 19 | regmatch_t match; | ||
| 20 | StringView line; | ||
| 21 | fill_line_ref(bi, &line); | ||
| 22 | |||
| 23 | // NOTE: If this is the first iteration then line.data contains | ||
| 24 | // partial line (text starting from the cursor position) and | ||
| 25 | // if match.rm_so is 0 then match is at beginning of the text | ||
| 26 | // which is same as the cursor position. | ||
| 27 | if (regexp_exec(regex, line.data, line.length, 1, &match, flags)) { | ||
| 28 | if (skip && match.rm_so == 0) { | ||
| 29 | // Ignore match at current cursor position | ||
| 30 | regoff_t count = match.rm_eo; | ||
| 31 | if (count == 0) { | ||
| 32 | // It is safe to skip one byte because every line | ||
| 33 | // has one extra byte (newline) that is not in line.data | ||
| 34 | count = 1; | ||
| 35 | } | ||
| 36 | block_iter_skip_bytes(bi, (size_t)count); | ||
| 37 | return do_search_fwd(view, regex, bi, false); | ||
| 38 | } | ||
| 39 | |||
| 40 | block_iter_skip_bytes(bi, match.rm_so); | ||
| 41 | view->cursor = *bi; | ||
| 42 | view->center_on_scroll = true; | ||
| 43 | view_reset_preferred_x(view); | ||
| 44 | return true; | ||
| 45 | } | ||
| 46 | |||
| 47 | skip = false; // Not at cursor position any more | ||
| 48 | flags = 0; | ||
| 49 | } while (block_iter_next_line(bi)); | ||
| 50 | |||
| 51 | return false; | ||
| 52 | } | ||
| 53 | |||
| 54 | static bool do_search_bwd(View *view, regex_t *regex, BlockIter *bi, ssize_t cx, bool skip) | ||
| 55 | { | ||
| 56 | if (block_iter_is_eof(bi)) { | ||
| 57 | goto next; | ||
| 58 | } | ||
| 59 | |||
| 60 | do { | ||
| 61 | regmatch_t match; | ||
| 62 | StringView line; | ||
| 63 | int flags = 0; | ||
| 64 | regoff_t offset = -1; | ||
| 65 | regoff_t pos = 0; | ||
| 66 | |||
| 67 | fill_line_ref(bi, &line); | ||
| 68 | while ( | ||
| 69 | pos <= line.length | ||
| 70 | && regexp_exec(regex, line.data + pos, line.length - pos, 1, &match, flags) | ||
| 71 | ) { | ||
| 72 | flags = REG_NOTBOL; | ||
| 73 | if (cx >= 0) { | ||
| 74 | if (pos + match.rm_so >= cx) { | ||
| 75 | // Ignore match at or after cursor | ||
| 76 | break; | ||
| 77 | } | ||
| 78 | if (skip && pos + match.rm_eo > cx) { | ||
| 79 | // Search -rw should not find word under cursor | ||
| 80 | break; | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | // This might be what we want (last match before cursor) | ||
| 85 | offset = pos + match.rm_so; | ||
| 86 | pos += match.rm_eo; | ||
| 87 | |||
| 88 | if (match.rm_so == match.rm_eo) { | ||
| 89 | // Zero length match | ||
| 90 | break; | ||
| 91 | } | ||
| 92 | } | ||
| 93 | |||
| 94 | if (offset >= 0) { | ||
| 95 | block_iter_skip_bytes(bi, offset); | ||
| 96 | view->cursor = *bi; | ||
| 97 | view->center_on_scroll = true; | ||
| 98 | view_reset_preferred_x(view); | ||
| 99 | return true; | ||
| 100 | } | ||
| 101 | |||
| 102 | next: | ||
| 103 | cx = -1; | ||
| 104 | } while (block_iter_prev_line(bi)); | ||
| 105 | |||
| 106 | return false; | ||
| 107 | } | ||
| 108 | |||
| 109 | bool search_tag(View *view, const char *pattern) | ||
| 110 | { | ||
| 111 | regex_t regex; | ||
| 112 | if (!regexp_compile_basic(®ex, pattern, REG_NEWLINE)) { | ||
| 113 | return false; | ||
| 114 | } | ||
| 115 | |||
| 116 | BlockIter bi = block_iter(view->buffer); | ||
| 117 | bool found = do_search_fwd(view, ®ex, &bi, false); | ||
| 118 | regfree(®ex); | ||
| 119 | |||
| 120 | if (!found) { | ||
| 121 | // Don't center view to cursor unnecessarily | ||
| 122 | view->force_center = false; | ||
| 123 | return error_msg("Tag not found"); | ||
| 124 | } | ||
| 125 | |||
| 126 | view->center_on_scroll = true; | ||
| 127 | return true; | ||
| 128 | } | ||
| 129 | |||
| 130 | static void free_regex(SearchState *search) | ||
| 131 | { | ||
| 132 | if (search->re_flags) { | ||
| 133 | regfree(&search->regex); | ||
| 134 | search->re_flags = 0; | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | static bool has_upper(const char *str) | ||
| 139 | { | ||
| 140 | for (size_t i = 0; str[i]; i++) { | ||
| 141 | if (ascii_isupper(str[i])) { | ||
| 142 | return true; | ||
| 143 | } | ||
| 144 | } | ||
| 145 | return false; | ||
| 146 | } | ||
| 147 | |||
| 148 | static bool update_regex(SearchState *search, SearchCaseSensitivity cs) | ||
| 149 | { | ||
| 150 | int re_flags = REG_NEWLINE; | ||
| 151 | switch (cs) { | ||
| 152 | case CSS_TRUE: | ||
| 153 | break; | ||
| 154 | case CSS_FALSE: | ||
| 155 | re_flags |= REG_ICASE; | ||
| 156 | break; | ||
| 157 | case CSS_AUTO: | ||
| 158 | if (!has_upper(search->pattern)) { | ||
| 159 | re_flags |= REG_ICASE; | ||
| 160 | } | ||
| 161 | break; | ||
| 162 | default: | ||
| 163 | BUG("unhandled case sensitivity value"); | ||
| 164 | } | ||
| 165 | |||
| 166 | if (re_flags == search->re_flags) { | ||
| 167 | return true; | ||
| 168 | } | ||
| 169 | |||
| 170 | free_regex(search); | ||
| 171 | |||
| 172 | search->re_flags = re_flags; | ||
| 173 | if (regexp_compile(&search->regex, search->pattern, search->re_flags)) { | ||
| 174 | return true; | ||
| 175 | } | ||
| 176 | |||
| 177 | free_regex(search); | ||
| 178 | return false; | ||
| 179 | } | ||
| 180 | |||
| 181 | void search_free_regexp(SearchState *search) | ||
| 182 | { | ||
| 183 | free_regex(search); | ||
| 184 | free(search->pattern); | ||
| 185 | } | ||
| 186 | |||
| 187 | void search_set_regexp(SearchState *search, const char *pattern) | ||
| 188 | { | ||
| 189 | search_free_regexp(search); | ||
| 190 | search->pattern = xstrdup(pattern); | ||
| 191 | } | ||
| 192 | |||
| 193 | static bool do_search_next(View *view, SearchState *search, SearchCaseSensitivity cs, bool skip) | ||
| 194 | { | ||
| 195 | if (!search->pattern) { | ||
| 196 | return error_msg("No previous search pattern"); | ||
| 197 | } | ||
| 198 | if (!update_regex(search, cs)) { | ||
| 199 | return false; | ||
| 200 | } | ||
| 201 | |||
| 202 | BlockIter bi = view->cursor; | ||
| 203 | regex_t *regex = &search->regex; | ||
| 204 | if (!search->reverse) { | ||
| 205 | if (do_search_fwd(view, regex, &bi, true)) { | ||
| 206 | return true; | ||
| 207 | } | ||
| 208 | block_iter_bof(&bi); | ||
| 209 | if (do_search_fwd(view, regex, &bi, false)) { | ||
| 210 | info_msg("Continuing at top"); | ||
| 211 | return true; | ||
| 212 | } | ||
| 213 | } else { | ||
| 214 | size_t cursor_x = block_iter_bol(&bi); | ||
| 215 | if (do_search_bwd(view, regex, &bi, cursor_x, skip)) { | ||
| 216 | return true; | ||
| 217 | } | ||
| 218 | block_iter_eof(&bi); | ||
| 219 | if (do_search_bwd(view, regex, &bi, -1, false)) { | ||
| 220 | info_msg("Continuing at bottom"); | ||
| 221 | return true; | ||
| 222 | } | ||
| 223 | } | ||
| 224 | |||
| 225 | return error_msg("Pattern '%s' not found", search->pattern); | ||
| 226 | } | ||
| 227 | |||
| 228 | bool search_prev(View *view, SearchState *search, SearchCaseSensitivity cs) | ||
| 229 | { | ||
| 230 | toggle_search_direction(search); | ||
| 231 | bool r = search_next(view, search, cs); | ||
| 232 | toggle_search_direction(search); | ||
| 233 | return r; | ||
| 234 | } | ||
| 235 | |||
| 236 | bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs) | ||
| 237 | { | ||
| 238 | return do_search_next(view, search, cs, false); | ||
| 239 | } | ||
| 240 | |||
| 241 | bool search_next_word(View *view, SearchState *search, SearchCaseSensitivity cs) | ||
| 242 | { | ||
| 243 | return do_search_next(view, search, cs, true); | ||
| 244 | } | ||
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 @@ | |||
| 1 | #ifndef SEARCH_H | ||
| 2 | #define SEARCH_H | ||
| 3 | |||
| 4 | #include <regex.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | typedef enum { | ||
| 10 | CSS_FALSE, | ||
| 11 | CSS_TRUE, | ||
| 12 | CSS_AUTO, | ||
| 13 | } SearchCaseSensitivity; | ||
| 14 | |||
| 15 | typedef struct { | ||
| 16 | regex_t regex; | ||
| 17 | char *pattern; | ||
| 18 | int re_flags; // If zero, regex hasn't been compiled | ||
| 19 | bool reverse; | ||
| 20 | } SearchState; | ||
| 21 | |||
| 22 | static inline void toggle_search_direction(SearchState *search) | ||
| 23 | { | ||
| 24 | search->reverse ^= 1; | ||
| 25 | } | ||
| 26 | |||
| 27 | bool search_tag(View *view, const char *pattern) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 28 | void search_set_regexp(SearchState *search, const char *pattern) NONNULL_ARGS; | ||
| 29 | void search_free_regexp(SearchState *search) NONNULL_ARGS; | ||
| 30 | bool search_prev(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 31 | bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 32 | bool search_next_word(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 33 | |||
| 34 | #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 @@ | |||
| 1 | #include "selection.h" | ||
| 2 | #include "editor.h" | ||
| 3 | #include "util/unicode.h" | ||
| 4 | |||
| 5 | static bool include_cursor_char_in_selection(const View *view) | ||
| 6 | { | ||
| 7 | const EditorState *e = view->window->editor; | ||
| 8 | if (!e->options.select_cursor_char) { | ||
| 9 | return false; | ||
| 10 | } | ||
| 11 | |||
| 12 | bool overwrite = view->buffer->options.overwrite; | ||
| 13 | CursorInputMode mode = overwrite ? CURSOR_MODE_OVERWRITE : CURSOR_MODE_INSERT; | ||
| 14 | TermCursorType type = e->cursor_styles[mode].type; | ||
| 15 | if (type == CURSOR_KEEP) { | ||
| 16 | type = e->cursor_styles[CURSOR_MODE_DEFAULT].type; | ||
| 17 | } | ||
| 18 | |||
| 19 | // If "select-cursor-char" option is true, include character under cursor | ||
| 20 | // in selections for any cursor type except bars (where it makes no sense | ||
| 21 | // to do so) | ||
| 22 | return !(type == CURSOR_STEADY_BAR || type == CURSOR_BLINKING_BAR); | ||
| 23 | } | ||
| 24 | |||
| 25 | void init_selection(const View *view, SelectionInfo *info) | ||
| 26 | { | ||
| 27 | info->so = view->sel_so; | ||
| 28 | info->eo = block_iter_get_offset(&view->cursor); | ||
| 29 | info->si = view->cursor; | ||
| 30 | block_iter_goto_offset(&info->si, info->so); | ||
| 31 | info->swapped = false; | ||
| 32 | if (info->so > info->eo) { | ||
| 33 | size_t o = info->so; | ||
| 34 | info->so = info->eo; | ||
| 35 | info->eo = o; | ||
| 36 | info->si = view->cursor; | ||
| 37 | info->swapped = true; | ||
| 38 | } | ||
| 39 | |||
| 40 | BlockIter ei = info->si; | ||
| 41 | block_iter_skip_bytes(&ei, info->eo - info->so); | ||
| 42 | if (block_iter_is_eof(&ei)) { | ||
| 43 | if (info->so == info->eo) { | ||
| 44 | return; | ||
| 45 | } | ||
| 46 | CodePoint u; | ||
| 47 | info->eo -= block_iter_prev_char(&ei, &u); | ||
| 48 | } | ||
| 49 | |||
| 50 | if (view->selection == SELECT_LINES) { | ||
| 51 | info->so -= block_iter_bol(&info->si); | ||
| 52 | info->eo += block_iter_eat_line(&ei); | ||
| 53 | } else { | ||
| 54 | if (include_cursor_char_in_selection(view)) { | ||
| 55 | info->eo += block_iter_next_column(&ei); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | size_t prepare_selection(View *view) | ||
| 61 | { | ||
| 62 | SelectionInfo info; | ||
| 63 | init_selection(view, &info); | ||
| 64 | view->cursor = info.si; | ||
| 65 | return info.eo - info.so; | ||
| 66 | } | ||
| 67 | |||
| 68 | char *view_get_selection(View *view, size_t *size) | ||
| 69 | { | ||
| 70 | if (view->selection == SELECT_NONE) { | ||
| 71 | *size = 0; | ||
| 72 | return NULL; | ||
| 73 | } | ||
| 74 | |||
| 75 | BlockIter save = view->cursor; | ||
| 76 | *size = prepare_selection(view); | ||
| 77 | char *buf = block_iter_get_bytes(&view->cursor, *size); | ||
| 78 | view->cursor = save; | ||
| 79 | return buf; | ||
| 80 | } | ||
| 81 | |||
| 82 | size_t get_nr_selected_lines(const SelectionInfo *info) | ||
| 83 | { | ||
| 84 | BlockIter bi = info->si; | ||
| 85 | size_t pos = info->so; | ||
| 86 | CodePoint u = 0; | ||
| 87 | size_t nr_lines = 1; | ||
| 88 | |||
| 89 | while (pos < info->eo) { | ||
| 90 | if (u == '\n') { | ||
| 91 | nr_lines++; | ||
| 92 | } | ||
| 93 | pos += block_iter_next_char(&bi, &u); | ||
| 94 | } | ||
| 95 | return nr_lines; | ||
| 96 | } | ||
| 97 | |||
| 98 | size_t get_nr_selected_chars(const SelectionInfo *info) | ||
| 99 | { | ||
| 100 | BlockIter bi = info->si; | ||
| 101 | size_t pos = info->so; | ||
| 102 | CodePoint u; | ||
| 103 | size_t nr_chars = 0; | ||
| 104 | |||
| 105 | while (pos < info->eo) { | ||
| 106 | nr_chars++; | ||
| 107 | pos += block_iter_next_char(&bi, &u); | ||
| 108 | } | ||
| 109 | return nr_chars; | ||
| 110 | } | ||
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 @@ | |||
| 1 | #ifndef SELECTION_H | ||
| 2 | #define SELECTION_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "block-iter.h" | ||
| 7 | #include "view.h" | ||
| 8 | |||
| 9 | typedef struct { | ||
| 10 | BlockIter si; | ||
| 11 | size_t so; | ||
| 12 | size_t eo; | ||
| 13 | bool swapped; | ||
| 14 | } SelectionInfo; | ||
| 15 | |||
| 16 | void init_selection(const View *view, SelectionInfo *info); | ||
| 17 | size_t prepare_selection(View *view); | ||
| 18 | char *view_get_selection(View *view, size_t *size); | ||
| 19 | size_t get_nr_selected_lines(const SelectionInfo *info); | ||
| 20 | size_t get_nr_selected_chars(const SelectionInfo *info); | ||
| 21 | |||
| 22 | #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 @@ | |||
| 1 | #include <stddef.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "shift.h" | ||
| 5 | #include "block-iter.h" | ||
| 6 | #include "buffer.h" | ||
| 7 | #include "change.h" | ||
| 8 | #include "indent.h" | ||
| 9 | #include "move.h" | ||
| 10 | #include "options.h" | ||
| 11 | #include "selection.h" | ||
| 12 | #include "util/debug.h" | ||
| 13 | #include "util/macros.h" | ||
| 14 | #include "util/xmalloc.h" | ||
| 15 | |||
| 16 | static char *alloc_indent(const LocalOptions *options, size_t count, size_t *sizep) | ||
| 17 | { | ||
| 18 | bool use_spaces = use_spaces_for_indent(options); | ||
| 19 | size_t size = use_spaces ? count * options->indent_width : count; | ||
| 20 | *sizep = size; | ||
| 21 | return memset(xmalloc(size), use_spaces ? ' ' : '\t', size); | ||
| 22 | } | ||
| 23 | |||
| 24 | static void shift_right(View *view, size_t nr_lines, size_t count) | ||
| 25 | { | ||
| 26 | const LocalOptions *options = &view->buffer->options; | ||
| 27 | size_t indent_size; | ||
| 28 | char *indent = alloc_indent(options, count, &indent_size); | ||
| 29 | |||
| 30 | for (size_t i = 0; true; ) { | ||
| 31 | StringView line; | ||
| 32 | fetch_this_line(&view->cursor, &line); | ||
| 33 | IndentInfo info = get_indent_info(options, &line); | ||
| 34 | if (info.wsonly) { | ||
| 35 | if (info.bytes) { | ||
| 36 | // Remove indentation | ||
| 37 | buffer_delete_bytes(view, info.bytes); | ||
| 38 | } | ||
| 39 | } else if (info.sane) { | ||
| 40 | // Insert whitespace | ||
| 41 | buffer_insert_bytes(view, indent, indent_size); | ||
| 42 | } else { | ||
| 43 | // Replace whole indentation with sane one | ||
| 44 | size_t size; | ||
| 45 | char *buf = alloc_indent(options, info.level + count, &size); | ||
| 46 | buffer_replace_bytes(view, info.bytes, buf, size); | ||
| 47 | free(buf); | ||
| 48 | } | ||
| 49 | if (++i == nr_lines) { | ||
| 50 | break; | ||
| 51 | } | ||
| 52 | block_iter_eat_line(&view->cursor); | ||
| 53 | } | ||
| 54 | |||
| 55 | free(indent); | ||
| 56 | } | ||
| 57 | |||
| 58 | static void shift_left(View *view, size_t nr_lines, size_t count) | ||
| 59 | { | ||
| 60 | const LocalOptions *options = &view->buffer->options; | ||
| 61 | const size_t indent_width = options->indent_width; | ||
| 62 | const bool space_indent = use_spaces_for_indent(options); | ||
| 63 | |||
| 64 | for (size_t i = 0; true; ) { | ||
| 65 | StringView line; | ||
| 66 | fetch_this_line(&view->cursor, &line); | ||
| 67 | IndentInfo info = get_indent_info(options, &line); | ||
| 68 | if (info.wsonly) { | ||
| 69 | if (info.bytes) { | ||
| 70 | // Remove indentation | ||
| 71 | buffer_delete_bytes(view, info.bytes); | ||
| 72 | } | ||
| 73 | } else if (info.level && info.sane) { | ||
| 74 | size_t n = MIN(count, info.level); | ||
| 75 | if (space_indent) { | ||
| 76 | n *= indent_width; | ||
| 77 | } | ||
| 78 | buffer_delete_bytes(view, n); | ||
| 79 | } else if (info.bytes) { | ||
| 80 | // Replace whole indentation with sane one | ||
| 81 | if (info.level > count) { | ||
| 82 | size_t size; | ||
| 83 | char *buf = alloc_indent(options, info.level - count, &size); | ||
| 84 | buffer_replace_bytes(view, info.bytes, buf, size); | ||
| 85 | free(buf); | ||
| 86 | } else { | ||
| 87 | buffer_delete_bytes(view, info.bytes); | ||
| 88 | } | ||
| 89 | } | ||
| 90 | if (++i == nr_lines) { | ||
| 91 | break; | ||
| 92 | } | ||
| 93 | block_iter_eat_line(&view->cursor); | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | static void do_shift_lines(View *view, int count, size_t nr_lines) | ||
| 98 | { | ||
| 99 | begin_change_chain(); | ||
| 100 | block_iter_bol(&view->cursor); | ||
| 101 | if (count > 0) { | ||
| 102 | shift_right(view, nr_lines, count); | ||
| 103 | } else { | ||
| 104 | shift_left(view, nr_lines, -count); | ||
| 105 | } | ||
| 106 | end_change_chain(view); | ||
| 107 | } | ||
| 108 | |||
| 109 | void shift_lines(View *view, int count) | ||
| 110 | { | ||
| 111 | unsigned int width = view->buffer->options.indent_width; | ||
| 112 | BUG_ON(width > INDENT_WIDTH_MAX); | ||
| 113 | BUG_ON(count == 0); | ||
| 114 | |||
| 115 | long x = view_get_preferred_x(view) + (count * width); | ||
| 116 | x = MAX(x, 0); | ||
| 117 | |||
| 118 | if (view->selection == SELECT_NONE) { | ||
| 119 | do_shift_lines(view, count, 1); | ||
| 120 | goto out; | ||
| 121 | } | ||
| 122 | |||
| 123 | SelectionInfo info; | ||
| 124 | view->selection = SELECT_LINES; | ||
| 125 | init_selection(view, &info); | ||
| 126 | view->cursor = info.si; | ||
| 127 | size_t nr_lines = get_nr_selected_lines(&info); | ||
| 128 | do_shift_lines(view, count, nr_lines); | ||
| 129 | if (info.swapped) { | ||
| 130 | // Cursor should be at beginning of selection | ||
| 131 | block_iter_bol(&view->cursor); | ||
| 132 | view->sel_so = block_iter_get_offset(&view->cursor); | ||
| 133 | while (--nr_lines) { | ||
| 134 | block_iter_prev_line(&view->cursor); | ||
| 135 | } | ||
| 136 | } else { | ||
| 137 | BlockIter save = view->cursor; | ||
| 138 | while (--nr_lines) { | ||
| 139 | block_iter_prev_line(&view->cursor); | ||
| 140 | } | ||
| 141 | view->sel_so = block_iter_get_offset(&view->cursor); | ||
| 142 | view->cursor = save; | ||
| 143 | } | ||
| 144 | |||
| 145 | out: | ||
| 146 | move_to_preferred_x(view, x); | ||
| 147 | } | ||
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 @@ | |||
| 1 | #ifndef SHIFT_H | ||
| 2 | #define SHIFT_H | ||
| 3 | |||
| 4 | #include "view.h" | ||
| 5 | |||
| 6 | void shift_lines(View *view, int count); | ||
| 7 | |||
| 8 | #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 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "show.h" | ||
| 5 | #include "bind.h" | ||
| 6 | #include "buffer.h" | ||
| 7 | #include "change.h" | ||
| 8 | #include "cmdline.h" | ||
| 9 | #include "command/alias.h" | ||
| 10 | #include "command/macro.h" | ||
| 11 | #include "command/serialize.h" | ||
| 12 | #include "commands.h" | ||
| 13 | #include "compiler.h" | ||
| 14 | #include "completion.h" | ||
| 15 | #include "config.h" | ||
| 16 | #include "edit.h" | ||
| 17 | #include "encoding.h" | ||
| 18 | #include "error.h" | ||
| 19 | #include "file-option.h" | ||
| 20 | #include "filetype.h" | ||
| 21 | #include "frame.h" | ||
| 22 | #include "msg.h" | ||
| 23 | #include "options.h" | ||
| 24 | #include "syntax/color.h" | ||
| 25 | #include "terminal/cursor.h" | ||
| 26 | #include "terminal/key.h" | ||
| 27 | #include "terminal/style.h" | ||
| 28 | #include "util/array.h" | ||
| 29 | #include "util/bsearch.h" | ||
| 30 | #include "util/debug.h" | ||
| 31 | #include "util/intern.h" | ||
| 32 | #include "util/str-util.h" | ||
| 33 | #include "util/unicode.h" | ||
| 34 | #include "util/xmalloc.h" | ||
| 35 | #include "util/xsnprintf.h" | ||
| 36 | #include "view.h" | ||
| 37 | #include "window.h" | ||
| 38 | |||
| 39 | extern char **environ; | ||
| 40 | |||
| 41 | typedef enum { | ||
| 42 | DTERC = 0x1, // Use "dte" filetype (and syntax highlighter) | ||
| 43 | LASTLINE = 0x2, // Move cursor to last line (e.g. most recent history entry) | ||
| 44 | MSGLINE = 0x4, // Move cursor to line containing current message | ||
| 45 | } ShowHandlerFlags; | ||
| 46 | |||
| 47 | typedef struct { | ||
| 48 | const char name[11]; | ||
| 49 | uint8_t flags; // ShowHandlerFlags | ||
| 50 | String (*dump)(EditorState *e); | ||
| 51 | bool (*show)(EditorState *e, const char *name, bool cmdline); | ||
| 52 | void (*complete_arg)(EditorState *e, PointerArray *a, const char *prefix); | ||
| 53 | } ShowHandler; | ||
| 54 | |||
| 55 | static void open_temporary_buffer ( | ||
| 56 | EditorState *e, | ||
| 57 | const char *text, | ||
| 58 | size_t text_len, | ||
| 59 | const char *cmd, | ||
| 60 | const char *cmd_arg, | ||
| 61 | ShowHandlerFlags flags | ||
| 62 | ) { | ||
| 63 | View *view = window_open_new_file(e->window); | ||
| 64 | Buffer *buffer = view->buffer; | ||
| 65 | buffer->temporary = true; | ||
| 66 | do_insert(view, text, text_len); | ||
| 67 | set_display_filename(buffer, xasprintf("(%s %s)", cmd, cmd_arg)); | ||
| 68 | buffer_set_encoding(buffer, encoding_from_type(UTF8), e->options.utf8_bom); | ||
| 69 | |||
| 70 | if (flags & LASTLINE) { | ||
| 71 | block_iter_eof(&view->cursor); | ||
| 72 | block_iter_prev_line(&view->cursor); | ||
| 73 | } else if ((flags & MSGLINE) && e->messages.array.count > 0) { | ||
| 74 | block_iter_goto_line(&view->cursor, e->messages.pos); | ||
| 75 | } | ||
| 76 | |||
| 77 | if (flags & DTERC) { | ||
| 78 | buffer->options.filetype = str_intern("dte"); | ||
| 79 | set_file_options(e, buffer); | ||
| 80 | buffer_update_syntax(e, buffer); | ||
| 81 | } | ||
| 82 | } | ||
| 83 | |||
| 84 | static bool show_normal_alias(EditorState *e, const char *alias_name, bool cflag) | ||
| 85 | { | ||
| 86 | const char *cmd_str = find_alias(&e->aliases, alias_name); | ||
| 87 | if (!cmd_str) { | ||
| 88 | if (find_normal_command(alias_name)) { | ||
| 89 | info_msg("%s is a built-in command, not an alias", alias_name); | ||
| 90 | } else { | ||
| 91 | info_msg("%s is not a known alias", alias_name); | ||
| 92 | } | ||
| 93 | return true; | ||
| 94 | } | ||
| 95 | |||
| 96 | if (cflag) { | ||
| 97 | set_input_mode(e, INPUT_COMMAND); | ||
| 98 | cmdline_set_text(&e->cmdline, cmd_str); | ||
| 99 | } else { | ||
| 100 | info_msg("%s is aliased to: %s", alias_name, cmd_str); | ||
| 101 | } | ||
| 102 | |||
| 103 | return true; | ||
| 104 | } | ||
| 105 | |||
| 106 | static bool show_binding(EditorState *e, const char *keystr, bool cflag) | ||
| 107 | { | ||
| 108 | KeyCode key; | ||
| 109 | if (!parse_key_string(&key, keystr)) { | ||
| 110 | return error_msg("invalid key string: %s", keystr); | ||
| 111 | } | ||
| 112 | |||
| 113 | // Use canonical key string in printed messages | ||
| 114 | char buf[KEYCODE_STR_MAX]; | ||
| 115 | size_t len = keycode_to_string(key, buf); | ||
| 116 | BUG_ON(len == 0); | ||
| 117 | keystr = buf; | ||
| 118 | |||
| 119 | if (u_is_unicode(key)) { | ||
| 120 | return error_msg("%s is not a bindable key", keystr); | ||
| 121 | } | ||
| 122 | |||
| 123 | const CachedCommand *b = lookup_binding(&e->modes[INPUT_NORMAL].key_bindings, key); | ||
| 124 | if (!b) { | ||
| 125 | info_msg("%s is not bound to a command", keystr); | ||
| 126 | return true; | ||
| 127 | } | ||
| 128 | |||
| 129 | if (cflag) { | ||
| 130 | set_input_mode(e, INPUT_COMMAND); | ||
| 131 | cmdline_set_text(&e->cmdline, b->cmd_str); | ||
| 132 | } else { | ||
| 133 | info_msg("%s is bound to: %s", keystr, b->cmd_str); | ||
| 134 | } | ||
| 135 | |||
| 136 | return true; | ||
| 137 | } | ||
| 138 | |||
| 139 | static bool show_color(EditorState *e, const char *color_name, bool cflag) | ||
| 140 | { | ||
| 141 | const TermColor *hl = find_color(&e->colors, color_name); | ||
| 142 | if (!hl) { | ||
| 143 | info_msg("no color entry with name '%s'", color_name); | ||
| 144 | return true; | ||
| 145 | } | ||
| 146 | |||
| 147 | if (cflag) { | ||
| 148 | CommandLine *c = &e->cmdline; | ||
| 149 | set_input_mode(e, INPUT_COMMAND); | ||
| 150 | cmdline_clear(c); | ||
| 151 | string_append_hl_color(&c->buf, color_name, hl); | ||
| 152 | c->pos = c->buf.len; | ||
| 153 | } else { | ||
| 154 | const char *color_str = term_color_to_string(hl); | ||
| 155 | info_msg("color '%s' is set to: %s", color_name, color_str); | ||
| 156 | } | ||
| 157 | |||
| 158 | return true; | ||
| 159 | } | ||
| 160 | |||
| 161 | static bool show_cursor(EditorState *e, const char *mode_str, bool cflag) | ||
| 162 | { | ||
| 163 | CursorInputMode mode = cursor_mode_from_str(mode_str); | ||
| 164 | if (mode >= NR_CURSOR_MODES) { | ||
| 165 | return error_msg("no cursor entry for '%s'", mode_str); | ||
| 166 | } | ||
| 167 | |||
| 168 | TermCursorStyle style = e->cursor_styles[mode]; | ||
| 169 | const char *type = cursor_type_to_str(style.type); | ||
| 170 | const char *color = cursor_color_to_str(style.color); | ||
| 171 | if (cflag) { | ||
| 172 | char buf[64]; | ||
| 173 | xsnprintf(buf, sizeof buf, "cursor %s %s %s", mode_str, type, color); | ||
| 174 | set_input_mode(e, INPUT_COMMAND); | ||
| 175 | cmdline_set_text(&e->cmdline, buf); | ||
| 176 | } else { | ||
| 177 | info_msg("cursor '%s' is set to: %s %s", mode_str, type, color); | ||
| 178 | } | ||
| 179 | |||
| 180 | return true; | ||
| 181 | } | ||
| 182 | |||
| 183 | static bool show_env(EditorState *e, const char *name, bool cflag) | ||
| 184 | { | ||
| 185 | const char *value = getenv(name); | ||
| 186 | if (!value) { | ||
| 187 | info_msg("no environment variable with name '%s'", name); | ||
| 188 | return true; | ||
| 189 | } | ||
| 190 | |||
| 191 | if (cflag) { | ||
| 192 | set_input_mode(e, INPUT_COMMAND); | ||
| 193 | cmdline_set_text(&e->cmdline, value); | ||
| 194 | } else { | ||
| 195 | info_msg("$%s is set to: %s", name, value); | ||
| 196 | } | ||
| 197 | |||
| 198 | return true; | ||
| 199 | } | ||
| 200 | |||
| 201 | static String dump_env(EditorState* UNUSED_ARG(e)) | ||
| 202 | { | ||
| 203 | String buf = string_new(4096); | ||
| 204 | for (size_t i = 0; environ[i]; i++) { | ||
| 205 | string_append_cstring(&buf, environ[i]); | ||
| 206 | string_append_byte(&buf, '\n'); | ||
| 207 | } | ||
| 208 | return buf; | ||
| 209 | } | ||
| 210 | |||
| 211 | static String dump_setenv(EditorState* UNUSED_ARG(e)) | ||
| 212 | { | ||
| 213 | String buf = string_new(4096); | ||
| 214 | for (size_t i = 0; environ[i]; i++) { | ||
| 215 | const char *str = environ[i]; | ||
| 216 | const char *delim = strchr(str, '='); | ||
| 217 | if (unlikely(!delim || delim == str)) { | ||
| 218 | continue; | ||
| 219 | } | ||
| 220 | string_append_literal(&buf, "setenv "); | ||
| 221 | if (unlikely(str[0] == '-' || delim[1] == '-')) { | ||
| 222 | string_append_literal(&buf, "-- "); | ||
| 223 | } | ||
| 224 | const StringView name = string_view(str, delim - str); | ||
| 225 | string_append_escaped_arg_sv(&buf, name, true); | ||
| 226 | string_append_byte(&buf, ' '); | ||
| 227 | string_append_escaped_arg(&buf, delim + 1, true); | ||
| 228 | string_append_byte(&buf, '\n'); | ||
| 229 | } | ||
| 230 | return buf; | ||
| 231 | } | ||
| 232 | |||
| 233 | static bool show_builtin(EditorState *e, const char *name, bool cflag) | ||
| 234 | { | ||
| 235 | const BuiltinConfig *cfg = get_builtin_config(name); | ||
| 236 | if (!cfg) { | ||
| 237 | return error_msg("no built-in config with name '%s'", name); | ||
| 238 | } | ||
| 239 | |||
| 240 | const StringView sv = cfg->text; | ||
| 241 | if (cflag) { | ||
| 242 | buffer_insert_bytes(e->view, sv.data, sv.length); | ||
| 243 | } else { | ||
| 244 | open_temporary_buffer(e, sv.data, sv.length, "builtin", name, DTERC); | ||
| 245 | } | ||
| 246 | |||
| 247 | return true; | ||
| 248 | } | ||
| 249 | |||
| 250 | static bool show_compiler(EditorState *e, const char *name, bool cflag) | ||
| 251 | { | ||
| 252 | const Compiler *compiler = find_compiler(&e->compilers, name); | ||
| 253 | if (!compiler) { | ||
| 254 | info_msg("no errorfmt entry found for '%s'", name); | ||
| 255 | return true; | ||
| 256 | } | ||
| 257 | |||
| 258 | String str = string_new(512); | ||
| 259 | dump_compiler(compiler, name, &str); | ||
| 260 | if (cflag) { | ||
| 261 | buffer_insert_bytes(e->view, str.buffer, str.len); | ||
| 262 | } else { | ||
| 263 | open_temporary_buffer(e, str.buffer, str.len, "errorfmt", name, DTERC); | ||
| 264 | } | ||
| 265 | |||
| 266 | string_free(&str); | ||
| 267 | return true; | ||
| 268 | } | ||
| 269 | |||
| 270 | static bool show_option(EditorState *e, const char *name, bool cflag) | ||
| 271 | { | ||
| 272 | const char *value = get_option_value_string(e, name); | ||
| 273 | if (!value) { | ||
| 274 | return error_msg("invalid option name: %s", name); | ||
| 275 | } | ||
| 276 | |||
| 277 | if (cflag) { | ||
| 278 | set_input_mode(e, INPUT_COMMAND); | ||
| 279 | cmdline_set_text(&e->cmdline, value); | ||
| 280 | } else { | ||
| 281 | info_msg("%s is set to: %s", name, value); | ||
| 282 | } | ||
| 283 | |||
| 284 | return true; | ||
| 285 | } | ||
| 286 | |||
| 287 | static void collect_all_options(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) | ||
| 288 | { | ||
| 289 | collect_options(a, prefix, false, false); | ||
| 290 | } | ||
| 291 | |||
| 292 | static void do_collect_cursor_modes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) | ||
| 293 | { | ||
| 294 | collect_cursor_modes(a, prefix); | ||
| 295 | } | ||
| 296 | |||
| 297 | static void do_collect_builtin_configs(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) | ||
| 298 | { | ||
| 299 | collect_builtin_configs(a, prefix); | ||
| 300 | } | ||
| 301 | |||
| 302 | static void do_collect_builtin_includes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) | ||
| 303 | { | ||
| 304 | collect_builtin_includes(a, prefix); | ||
| 305 | } | ||
| 306 | |||
| 307 | static bool show_wsplit(EditorState *e, const char *name, bool cflag) | ||
| 308 | { | ||
| 309 | if (!streq(name, "this")) { | ||
| 310 | return error_msg("invalid window: %s", name); | ||
| 311 | } | ||
| 312 | |||
| 313 | const Window *w = e->window; | ||
| 314 | char buf[(4 * DECIMAL_STR_MAX(w->x)) + 4]; | ||
| 315 | xsnprintf(buf, sizeof buf, "%d,%d %dx%d", w->x, w->y, w->w, w->h); | ||
| 316 | |||
| 317 | if (cflag) { | ||
| 318 | set_input_mode(e, INPUT_COMMAND); | ||
| 319 | cmdline_set_text(&e->cmdline, buf); | ||
| 320 | } else { | ||
| 321 | info_msg("current window dimensions: %s", buf); | ||
| 322 | } | ||
| 323 | |||
| 324 | return true; | ||
| 325 | } | ||
| 326 | |||
| 327 | static String do_history_dump(const History *history) | ||
| 328 | { | ||
| 329 | const size_t nr_entries = history->entries.count; | ||
| 330 | const size_t size = round_size_to_next_multiple(16 * nr_entries, 4096); | ||
| 331 | String buf = string_new(size); | ||
| 332 | size_t n = 0; | ||
| 333 | for (HistoryEntry *e = history->first; e; e = e->next, n++) { | ||
| 334 | string_append_cstring(&buf, e->text); | ||
| 335 | string_append_byte(&buf, '\n'); | ||
| 336 | } | ||
| 337 | BUG_ON(n != nr_entries); | ||
| 338 | return buf; | ||
| 339 | } | ||
| 340 | |||
| 341 | String dump_command_history(EditorState *e) | ||
| 342 | { | ||
| 343 | return do_history_dump(&e->command_history); | ||
| 344 | } | ||
| 345 | |||
| 346 | String dump_search_history(EditorState *e) | ||
| 347 | { | ||
| 348 | return do_history_dump(&e->search_history); | ||
| 349 | } | ||
| 350 | |||
| 351 | typedef struct { | ||
| 352 | const char *name; | ||
| 353 | const char *value; | ||
| 354 | } CommandAlias; | ||
| 355 | |||
| 356 | static int alias_cmp(const void *ap, const void *bp) | ||
| 357 | { | ||
| 358 | const CommandAlias *a = ap; | ||
| 359 | const CommandAlias *b = bp; | ||
| 360 | return strcmp(a->name, b->name); | ||
| 361 | } | ||
| 362 | |||
| 363 | String dump_normal_aliases(EditorState *e) | ||
| 364 | { | ||
| 365 | const size_t count = e->aliases.count; | ||
| 366 | if (unlikely(count == 0)) { | ||
| 367 | return string_new(0); | ||
| 368 | } | ||
| 369 | |||
| 370 | // Clone the contents of the HashMap as an array of name/value pairs | ||
| 371 | CommandAlias *array = xnew(CommandAlias, count); | ||
| 372 | size_t n = 0; | ||
| 373 | for (HashMapIter it = hashmap_iter(&e->aliases); hashmap_next(&it); ) { | ||
| 374 | array[n++] = (CommandAlias) { | ||
| 375 | .name = it.entry->key, | ||
| 376 | .value = it.entry->value, | ||
| 377 | }; | ||
| 378 | } | ||
| 379 | |||
| 380 | // Sort the array | ||
| 381 | BUG_ON(n != count); | ||
| 382 | qsort(array, count, sizeof(array[0]), alias_cmp); | ||
| 383 | |||
| 384 | // Serialize the aliases in sorted order | ||
| 385 | String buf = string_new(4096); | ||
| 386 | for (size_t i = 0; i < count; i++) { | ||
| 387 | const char *name = array[i].name; | ||
| 388 | string_append_literal(&buf, "alias "); | ||
| 389 | if (unlikely(name[0] == '-')) { | ||
| 390 | string_append_literal(&buf, "-- "); | ||
| 391 | } | ||
| 392 | string_append_escaped_arg(&buf, name, true); | ||
| 393 | string_append_byte(&buf, ' '); | ||
| 394 | string_append_escaped_arg(&buf, array[i].value, true); | ||
| 395 | string_append_byte(&buf, '\n'); | ||
| 396 | } | ||
| 397 | |||
| 398 | free(array); | ||
| 399 | return buf; | ||
| 400 | } | ||
| 401 | |||
| 402 | String dump_all_bindings(EditorState *e) | ||
| 403 | { | ||
| 404 | static const char flags[][4] = { | ||
| 405 | [INPUT_NORMAL] = "", | ||
| 406 | [INPUT_COMMAND] = "-c ", | ||
| 407 | [INPUT_SEARCH] = "-s ", | ||
| 408 | }; | ||
| 409 | |||
| 410 | static_assert(ARRAYLEN(flags) == ARRAYLEN(e->modes)); | ||
| 411 | String buf = string_new(4096); | ||
| 412 | for (InputMode i = 0, n = ARRAYLEN(e->modes); i < n; i++) { | ||
| 413 | const IntMap *bindings = &e->modes[i].key_bindings; | ||
| 414 | if (dump_bindings(bindings, flags[i], &buf) && i != n - 1) { | ||
| 415 | string_append_byte(&buf, '\n'); | ||
| 416 | } | ||
| 417 | } | ||
| 418 | return buf; | ||
| 419 | } | ||
| 420 | |||
| 421 | String dump_frames(EditorState *e) | ||
| 422 | { | ||
| 423 | String str = string_new(4096); | ||
| 424 | dump_frame(e->root_frame, 0, &str); | ||
| 425 | return str; | ||
| 426 | } | ||
| 427 | |||
| 428 | String dump_compilers(EditorState *e) | ||
| 429 | { | ||
| 430 | String buf = string_new(4096); | ||
| 431 | for (HashMapIter it = hashmap_iter(&e->compilers); hashmap_next(&it); ) { | ||
| 432 | const char *name = it.entry->key; | ||
| 433 | const Compiler *c = it.entry->value; | ||
| 434 | dump_compiler(c, name, &buf); | ||
| 435 | string_append_byte(&buf, '\n'); | ||
| 436 | } | ||
| 437 | return buf; | ||
| 438 | } | ||
| 439 | |||
| 440 | String dump_cursors(EditorState *e) | ||
| 441 | { | ||
| 442 | String buf = string_new(128); | ||
| 443 | for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) { | ||
| 444 | const TermCursorStyle *style = &e->cursor_styles[m]; | ||
| 445 | string_append_literal(&buf, "cursor "); | ||
| 446 | string_append_cstring(&buf, cursor_mode_to_str(m)); | ||
| 447 | string_append_byte(&buf, ' '); | ||
| 448 | string_append_cstring(&buf, cursor_type_to_str(style->type)); | ||
| 449 | string_append_byte(&buf, ' '); | ||
| 450 | string_append_cstring(&buf, cursor_color_to_str(style->color)); | ||
| 451 | string_append_byte(&buf, '\n'); | ||
| 452 | } | ||
| 453 | return buf; | ||
| 454 | } | ||
| 455 | |||
| 456 | // Dump option values only | ||
| 457 | String do_dump_options(EditorState *e) | ||
| 458 | { | ||
| 459 | return dump_options(&e->options, &e->buffer->options); | ||
| 460 | } | ||
| 461 | |||
| 462 | // Dump option values and FileOption entries | ||
| 463 | String dump_options_and_fileopts(EditorState *e) | ||
| 464 | { | ||
| 465 | String str = do_dump_options(e); | ||
| 466 | string_append_literal(&str, "\n\n"); | ||
| 467 | dump_file_options(&e->file_options, &str); | ||
| 468 | return str; | ||
| 469 | } | ||
| 470 | |||
| 471 | String do_dump_builtin_configs(EditorState* UNUSED_ARG(e)) | ||
| 472 | { | ||
| 473 | return dump_builtin_configs(); | ||
| 474 | } | ||
| 475 | |||
| 476 | String do_dump_hl_colors(EditorState *e) | ||
| 477 | { | ||
| 478 | return dump_hl_colors(&e->colors); | ||
| 479 | } | ||
| 480 | |||
| 481 | String do_dump_filetypes(EditorState *e) | ||
| 482 | { | ||
| 483 | return dump_filetypes(&e->filetypes); | ||
| 484 | } | ||
| 485 | |||
| 486 | static String do_dump_messages(EditorState *e) | ||
| 487 | { | ||
| 488 | return dump_messages(&e->messages); | ||
| 489 | } | ||
| 490 | |||
| 491 | static String do_dump_macro(EditorState *e) | ||
| 492 | { | ||
| 493 | return dump_macro(&e->macro); | ||
| 494 | } | ||
| 495 | |||
| 496 | static String do_dump_buffer(EditorState *e) | ||
| 497 | { | ||
| 498 | return dump_buffer(e->buffer); | ||
| 499 | } | ||
| 500 | |||
| 501 | static const ShowHandler show_handlers[] = { | ||
| 502 | {"alias", DTERC, dump_normal_aliases, show_normal_alias, collect_normal_aliases}, | ||
| 503 | {"bind", DTERC, dump_all_bindings, show_binding, collect_bound_normal_keys}, | ||
| 504 | {"buffer", 0, do_dump_buffer, NULL, NULL}, | ||
| 505 | {"builtin", 0, do_dump_builtin_configs, show_builtin, do_collect_builtin_configs}, | ||
| 506 | {"color", DTERC, do_dump_hl_colors, show_color, collect_hl_colors}, | ||
| 507 | {"command", DTERC | LASTLINE, dump_command_history, NULL, NULL}, | ||
| 508 | {"cursor", DTERC, dump_cursors, show_cursor, do_collect_cursor_modes}, | ||
| 509 | {"env", 0, dump_env, show_env, collect_env}, | ||
| 510 | {"errorfmt", DTERC, dump_compilers, show_compiler, collect_compilers}, | ||
| 511 | {"ft", DTERC, do_dump_filetypes, NULL, NULL}, | ||
| 512 | {"hi", DTERC, do_dump_hl_colors, show_color, collect_hl_colors}, | ||
| 513 | {"include", 0, do_dump_builtin_configs, show_builtin, do_collect_builtin_includes}, | ||
| 514 | {"macro", DTERC, do_dump_macro, NULL, NULL}, | ||
| 515 | {"msg", MSGLINE, do_dump_messages, NULL, NULL}, | ||
| 516 | {"option", DTERC, dump_options_and_fileopts, show_option, collect_all_options}, | ||
| 517 | {"search", LASTLINE, dump_search_history, NULL, NULL}, | ||
| 518 | {"set", DTERC, do_dump_options, show_option, collect_all_options}, | ||
| 519 | {"setenv", DTERC, dump_setenv, show_env, collect_env}, | ||
| 520 | {"wsplit", 0, dump_frames, show_wsplit, NULL}, | ||
| 521 | }; | ||
| 522 | |||
| 523 | UNITTEST { | ||
| 524 | CHECK_BSEARCH_ARRAY(show_handlers, name, strcmp); | ||
| 525 | } | ||
| 526 | |||
| 527 | bool show(EditorState *e, const char *type, const char *key, bool cflag) | ||
| 528 | { | ||
| 529 | const ShowHandler *handler = BSEARCH(type, show_handlers, vstrcmp); | ||
| 530 | if (!handler) { | ||
| 531 | return error_msg("invalid argument: '%s'", type); | ||
| 532 | } | ||
| 533 | |||
| 534 | if (key) { | ||
| 535 | if (!handler->show) { | ||
| 536 | return error_msg("'show %s' doesn't take extra arguments", type); | ||
| 537 | } | ||
| 538 | return handler->show(e, key, cflag); | ||
| 539 | } | ||
| 540 | |||
| 541 | String str = handler->dump(e); | ||
| 542 | open_temporary_buffer(e, str.buffer, str.len, "show", type, handler->flags); | ||
| 543 | string_free(&str); | ||
| 544 | return true; | ||
| 545 | } | ||
| 546 | |||
| 547 | void collect_show_subcommands(PointerArray *a, const char *prefix) | ||
| 548 | { | ||
| 549 | COLLECT_STRING_FIELDS(show_handlers, name, a, prefix); | ||
| 550 | } | ||
| 551 | |||
| 552 | void collect_show_subcommand_args(EditorState *e, PointerArray *a, const char *name, const char *arg_prefix) | ||
| 553 | { | ||
| 554 | const ShowHandler *handler = BSEARCH(name, show_handlers, vstrcmp); | ||
| 555 | if (handler && handler->complete_arg) { | ||
| 556 | handler->complete_arg(e, a, arg_prefix); | ||
| 557 | } | ||
| 558 | } | ||
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 @@ | |||
| 1 | #ifndef SHOW_H | ||
| 2 | #define SHOW_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "editor.h" | ||
| 6 | #include "util/macros.h" | ||
| 7 | #include "util/ptr-array.h" | ||
| 8 | #include "util/string.h" | ||
| 9 | |||
| 10 | bool show(EditorState *e, const char *type, const char *key, bool cflag) NONNULL_ARG(1, 2) WARN_UNUSED_RESULT; | ||
| 11 | void collect_show_subcommands(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 12 | void collect_show_subcommand_args(EditorState *e, PointerArray *a, const char *name, const char *arg_prefix) NONNULL_ARGS; | ||
| 13 | |||
| 14 | String dump_all_bindings(EditorState *e); | ||
| 15 | String dump_command_history(EditorState *e); | ||
| 16 | String dump_compilers(EditorState *e); | ||
| 17 | String dump_cursors(EditorState *e); | ||
| 18 | String dump_frames(EditorState *e); | ||
| 19 | String dump_normal_aliases(EditorState *e); | ||
| 20 | String dump_options_and_fileopts(EditorState *e); | ||
| 21 | String dump_search_history(EditorState *e); | ||
| 22 | String do_dump_builtin_configs(EditorState *e); | ||
| 23 | String do_dump_filetypes(EditorState *e); | ||
| 24 | String do_dump_hl_colors(EditorState *e); | ||
| 25 | String do_dump_options(EditorState *e); | ||
| 26 | |||
| 27 | #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 @@ | |||
| 1 | #include "compat.h" | ||
| 2 | #include <errno.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include <unistd.h> | ||
| 5 | #include "signals.h" | ||
| 6 | #include "util/debug.h" | ||
| 7 | #include "util/exitcode.h" | ||
| 8 | #include "util/log.h" | ||
| 9 | #include "util/macros.h" | ||
| 10 | |||
| 11 | volatile sig_atomic_t resized = 0; | ||
| 12 | |||
| 13 | static const int ignored_signals[] = { | ||
| 14 | SIGINT, // Terminal interrupt (see: VINTR in termios(3)) | ||
| 15 | SIGQUIT, // Terminal quit (see: VQUIT in termios(3)) | ||
| 16 | SIGTSTP, // Terminal stop (see: VSUSP in termios(3)) | ||
| 17 | SIGXFSZ, // File size limit exceeded (see: RLIMIT_FSIZE in getrlimit(3)) | ||
| 18 | SIGPIPE, // Broken pipe (see: EPIPE error in write(3)) | ||
| 19 | SIGUSR1, // User signal 1 (terminates by default, for no good reason) | ||
| 20 | SIGUSR2, // User signal 2 (as above) | ||
| 21 | }; | ||
| 22 | |||
| 23 | static const int default_signals[] = { | ||
| 24 | SIGABRT, // Terminate (cleanup already done) | ||
| 25 | SIGCHLD, // Ignore (see: wait(3)) | ||
| 26 | SIGURG, // Ignore | ||
| 27 | SIGTTIN, // Stop | ||
| 28 | SIGTTOU, // Stop | ||
| 29 | SIGCONT, // Continue | ||
| 30 | }; | ||
| 31 | |||
| 32 | static const int fatal_signals[] = { | ||
| 33 | SIGBUS, | ||
| 34 | SIGFPE, | ||
| 35 | SIGILL, | ||
| 36 | SIGSEGV, | ||
| 37 | SIGSYS, | ||
| 38 | SIGTRAP, | ||
| 39 | SIGXCPU, | ||
| 40 | SIGALRM, | ||
| 41 | SIGVTALRM, | ||
| 42 | SIGHUP, | ||
| 43 | SIGTERM, | ||
| 44 | #ifdef SIGPROF | ||
| 45 | SIGPROF, | ||
| 46 | #endif | ||
| 47 | #ifdef SIGEMT | ||
| 48 | SIGEMT, | ||
| 49 | #endif | ||
| 50 | }; | ||
| 51 | |||
| 52 | void handle_sigwinch(int UNUSED_ARG(signum)) | ||
| 53 | { | ||
| 54 | resized = 1; | ||
| 55 | } | ||
| 56 | |||
| 57 | static noreturn COLD void handle_fatal_signal(int signum) | ||
| 58 | { | ||
| 59 | LOG_CRITICAL("received signal %d (%s)", signum, strsignal(signum)); | ||
| 60 | |||
| 61 | // If `signum` is SIGHUP, there's no point in trying to clean up the | ||
| 62 | // state of the (disconnected) terminal | ||
| 63 | if (signum != SIGHUP) { | ||
| 64 | fatal_error_cleanup(); | ||
| 65 | } | ||
| 66 | |||
| 67 | // Restore and unblock `signum` and then re-raise it, to ensure the | ||
| 68 | // termination status (as seen by e.g. waitpid(3) in the parent) is | ||
| 69 | // set appropriately | ||
| 70 | struct sigaction sa = {.sa_handler = SIG_DFL}; | ||
| 71 | if ( | ||
| 72 | sigemptyset(&sa.sa_mask) == 0 | ||
| 73 | && sigaction(signum, &sa, NULL) == 0 | ||
| 74 | && sigaddset(&sa.sa_mask, signum) == 0 | ||
| 75 | && sigprocmask(SIG_UNBLOCK, &sa.sa_mask, NULL) == 0 | ||
| 76 | ) { | ||
| 77 | raise(signum); | ||
| 78 | } | ||
| 79 | |||
| 80 | // This is here just to make extra certain the handler never returns. | ||
| 81 | // If everything is working correctly, this code should be unreachable. | ||
| 82 | raise(SIGKILL); | ||
| 83 | _exit(EX_OSERR); | ||
| 84 | } | ||
| 85 | |||
| 86 | // strsignal(3) is fine in situations where a signal is being reported | ||
| 87 | // as terminating a process, but it tends to be confusing in most other | ||
| 88 | // circumstances, where the signal name (not description) is usually | ||
| 89 | // clearer | ||
| 90 | static const char *signum_to_str(int signum) | ||
| 91 | { | ||
| 92 | #if HAVE_SIG2STR | ||
| 93 | static char buf[SIG2STR_MAX + 3]; | ||
| 94 | if (sig2str(signum, buf + 3) == 0) { | ||
| 95 | return memcpy(buf, "SIG", 3); | ||
| 96 | } | ||
| 97 | #elif HAVE_SIGABBREV_NP | ||
| 98 | static char buf[16]; | ||
| 99 | const char *abbr = sigabbrev_np(signum); | ||
| 100 | if (abbr && memccpy(buf + 3, abbr, '\0', sizeof(buf) - 3)) { | ||
| 101 | return memcpy(buf, "SIG", 3); | ||
| 102 | } | ||
| 103 | #endif | ||
| 104 | |||
| 105 | const char *str = strsignal(signum); | ||
| 106 | return likely(str) ? str : "??"; | ||
| 107 | } | ||
| 108 | |||
| 109 | static void do_sigaction(int sig, const struct sigaction *action) | ||
| 110 | { | ||
| 111 | struct sigaction old_action; | ||
| 112 | if (unlikely(sigaction(sig, action, &old_action) != 0)) { | ||
| 113 | const char *err = strerror(errno); | ||
| 114 | LOG_ERROR("failed to set disposition for signal %d: %s", sig, err); | ||
| 115 | return; | ||
| 116 | } | ||
| 117 | if (unlikely(old_action.sa_handler == SIG_IGN)) { | ||
| 118 | const char *str = signum_to_str(sig); | ||
| 119 | LOG_WARNING("ignored signal was inherited: %d (%s)", sig, str); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | /* | ||
| 124 | * "A program that uses these functions should be written to catch all | ||
| 125 | * signals and take other appropriate actions to ensure that when the | ||
| 126 | * program terminates, whether planned or not, the terminal device's | ||
| 127 | * state is restored to its original state." | ||
| 128 | * | ||
| 129 | * (https://pubs.opengroup.org/onlinepubs/9699919799/functions/tcgetattr.html) | ||
| 130 | */ | ||
| 131 | void set_signal_handlers(void) | ||
| 132 | { | ||
| 133 | struct sigaction action = {.sa_handler = handle_fatal_signal}; | ||
| 134 | sigfillset(&action.sa_mask); | ||
| 135 | for (size_t i = 0; i < ARRAYLEN(fatal_signals); i++) { | ||
| 136 | do_sigaction(fatal_signals[i], &action); | ||
| 137 | } | ||
| 138 | |||
| 139 | // "The default actions for the realtime signals in the range SIGRTMIN | ||
| 140 | // to SIGRTMAX shall be to terminate the process abnormally." | ||
| 141 | // (POSIX.1-2017 §2.4.3) | ||
| 142 | #if defined(SIGRTMIN) && defined(SIGRTMAX) | ||
| 143 | for (int s = SIGRTMIN, max = SIGRTMAX; s <= max; s++) { | ||
| 144 | do_sigaction(s, &action); | ||
| 145 | } | ||
| 146 | #endif | ||
| 147 | |||
| 148 | action.sa_handler = SIG_IGN; | ||
| 149 | for (size_t i = 0; i < ARRAYLEN(ignored_signals); i++) { | ||
| 150 | do_sigaction(ignored_signals[i], &action); | ||
| 151 | } | ||
| 152 | |||
| 153 | action.sa_handler = SIG_DFL; | ||
| 154 | for (size_t i = 0; i < ARRAYLEN(default_signals); i++) { | ||
| 155 | do_sigaction(default_signals[i], &action); | ||
| 156 | } | ||
| 157 | |||
| 158 | #if defined(SIGWINCH) | ||
| 159 | LOG_INFO("setting SIGWINCH handler"); | ||
| 160 | action.sa_handler = handle_sigwinch; | ||
| 161 | do_sigaction(SIGWINCH, &action); | ||
| 162 | #endif | ||
| 163 | |||
| 164 | // Set signal mask explicitly, to avoid any possibility of | ||
| 165 | // inheriting blocked signals | ||
| 166 | sigset_t mask; | ||
| 167 | sigemptyset(&mask); | ||
| 168 | sigprocmask(SIG_SETMASK, &mask, NULL); | ||
| 169 | } | ||
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 @@ | |||
| 1 | #ifndef SIGNALS_H | ||
| 2 | #define SIGNALS_H | ||
| 3 | |||
| 4 | #include <signal.h> | ||
| 5 | |||
| 6 | extern volatile sig_atomic_t resized; | ||
| 7 | |||
| 8 | void set_signal_handlers(void); | ||
| 9 | void handle_sigwinch(int signum); | ||
| 10 | |||
| 11 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <poll.h> | ||
| 3 | #include <stddef.h> | ||
| 4 | #include <stdio.h> | ||
| 5 | #include <string.h> | ||
| 6 | #include <unistd.h> | ||
| 7 | #include "spawn.h" | ||
| 8 | #include "error.h" | ||
| 9 | #include "regexp.h" | ||
| 10 | #include "terminal/mode.h" | ||
| 11 | #include "util/debug.h" | ||
| 12 | #include "util/fd.h" | ||
| 13 | #include "util/fork-exec.h" | ||
| 14 | #include "util/ptr-array.h" | ||
| 15 | #include "util/str-util.h" | ||
| 16 | #include "util/strtonum.h" | ||
| 17 | #include "util/xmalloc.h" | ||
| 18 | #include "util/xreadwrite.h" | ||
| 19 | #include "util/xstdio.h" | ||
| 20 | |||
| 21 | static void handle_error_msg(const Compiler *c, MessageArray *msgs, char *str) | ||
| 22 | { | ||
| 23 | if (str[0] == '\0' || str[0] == '\n') { | ||
| 24 | return; | ||
| 25 | } | ||
| 26 | |||
| 27 | size_t str_len = str_replace_byte(str, '\t', ' '); | ||
| 28 | if (str[str_len - 1] == '\n') { | ||
| 29 | str[--str_len] = '\0'; | ||
| 30 | } | ||
| 31 | |||
| 32 | for (size_t i = 0, n = c->error_formats.count; i < n; i++) { | ||
| 33 | const ErrorFormat *p = c->error_formats.ptrs[i]; | ||
| 34 | regmatch_t m[ERRORFMT_CAPTURE_MAX]; | ||
| 35 | if (!regexp_exec(&p->re, str, str_len, ARRAYLEN(m), m, 0)) { | ||
| 36 | continue; | ||
| 37 | } | ||
| 38 | if (p->ignore) { | ||
| 39 | return; | ||
| 40 | } | ||
| 41 | |||
| 42 | int8_t mi = p->capture_index[ERRFMT_MESSAGE]; | ||
| 43 | if (m[mi].rm_so < 0) { | ||
| 44 | mi = 0; | ||
| 45 | } | ||
| 46 | |||
| 47 | Message *msg = new_message(str + m[mi].rm_so, m[mi].rm_eo - m[mi].rm_so); | ||
| 48 | msg->loc = xnew0(FileLocation, 1); | ||
| 49 | |||
| 50 | int8_t fi = p->capture_index[ERRFMT_FILE]; | ||
| 51 | if (fi >= 0 && m[fi].rm_so >= 0) { | ||
| 52 | msg->loc->filename = xstrslice(str, m[fi].rm_so, m[fi].rm_eo); | ||
| 53 | |||
| 54 | unsigned long *const ptrs[] = { | ||
| 55 | [ERRFMT_LINE] = &msg->loc->line, | ||
| 56 | [ERRFMT_COLUMN] = &msg->loc->column, | ||
| 57 | }; | ||
| 58 | |||
| 59 | static_assert(ARRAYLEN(ptrs) == 3); | ||
| 60 | static_assert(ERRFMT_LINE == 1); | ||
| 61 | static_assert(ERRFMT_COLUMN == 2); | ||
| 62 | |||
| 63 | for (size_t j = ERRFMT_LINE; j < ARRAYLEN(ptrs); j++) { | ||
| 64 | int8_t ci = p->capture_index[j]; | ||
| 65 | if (ci >= 0 && m[ci].rm_so >= 0) { | ||
| 66 | size_t len = m[ci].rm_eo - m[ci].rm_so; | ||
| 67 | unsigned long val; | ||
| 68 | if (len == buf_parse_ulong(str + m[ci].rm_so, len, &val)) { | ||
| 69 | *ptrs[j] = val; | ||
| 70 | } | ||
| 71 | } | ||
| 72 | } | ||
| 73 | } | ||
| 74 | |||
| 75 | add_message(msgs, msg); | ||
| 76 | return; | ||
| 77 | } | ||
| 78 | |||
| 79 | add_message(msgs, new_message(str, str_len)); | ||
| 80 | } | ||
| 81 | |||
| 82 | static void read_errors(const Compiler *c, MessageArray *msgs, int fd, bool quiet) | ||
| 83 | { | ||
| 84 | FILE *f = fdopen(fd, "r"); | ||
| 85 | if (unlikely(!f)) { | ||
| 86 | return; | ||
| 87 | } | ||
| 88 | char line[4096]; | ||
| 89 | while (xfgets(line, sizeof(line), f)) { | ||
| 90 | if (!quiet) { | ||
| 91 | xfputs(line, stderr); | ||
| 92 | } | ||
| 93 | handle_error_msg(c, msgs, line); | ||
| 94 | } | ||
| 95 | fclose(f); | ||
| 96 | } | ||
| 97 | |||
| 98 | static void handle_piped_data(int f[3], SpawnContext *ctx) | ||
| 99 | { | ||
| 100 | BUG_ON(f[0] < 0 && f[1] < 0 && f[2] < 0); | ||
| 101 | BUG_ON(f[0] >= 0 && f[0] <= 2); | ||
| 102 | BUG_ON(f[1] >= 0 && f[1] <= 2); | ||
| 103 | BUG_ON(f[2] >= 0 && f[2] <= 2); | ||
| 104 | |||
| 105 | if (ctx->input.length == 0) { | ||
| 106 | xclose(f[0]); | ||
| 107 | f[0] = -1; | ||
| 108 | if (f[1] < 0 && f[2] < 0) { | ||
| 109 | return; | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | struct pollfd fds[] = { | ||
| 114 | {.fd = f[0], .events = POLLOUT}, | ||
| 115 | {.fd = f[1], .events = POLLIN}, | ||
| 116 | {.fd = f[2], .events = POLLIN}, | ||
| 117 | }; | ||
| 118 | |||
| 119 | size_t wlen = 0; | ||
| 120 | while (1) { | ||
| 121 | if (unlikely(poll(fds, ARRAYLEN(fds), -1) < 0)) { | ||
| 122 | if (errno == EINTR) { | ||
| 123 | continue; | ||
| 124 | } | ||
| 125 | error_msg_errno("poll"); | ||
| 126 | return; | ||
| 127 | } | ||
| 128 | |||
| 129 | for (size_t i = 0; i < ARRAYLEN(ctx->outputs); i++) { | ||
| 130 | struct pollfd *pfd = fds + i + 1; | ||
| 131 | if (pfd->revents & POLLIN) { | ||
| 132 | String *output = &ctx->outputs[i]; | ||
| 133 | char *buf = string_reserve_space(output, 4096); | ||
| 134 | ssize_t rc = xread(pfd->fd, buf, output->alloc - output->len); | ||
| 135 | if (unlikely(rc < 0)) { | ||
| 136 | error_msg_errno("read"); | ||
| 137 | return; | ||
| 138 | } | ||
| 139 | if (rc == 0) { // EOF | ||
| 140 | if (xclose(pfd->fd)) { | ||
| 141 | error_msg_errno("close"); | ||
| 142 | return; | ||
| 143 | } | ||
| 144 | pfd->fd = -1; | ||
| 145 | continue; | ||
| 146 | } | ||
| 147 | output->len += rc; | ||
| 148 | } | ||
| 149 | } | ||
| 150 | |||
| 151 | if (fds[0].revents & POLLOUT) { | ||
| 152 | ssize_t rc = xwrite(fds[0].fd, ctx->input.data + wlen, ctx->input.length - wlen); | ||
| 153 | if (unlikely(rc < 0)) { | ||
| 154 | error_msg_errno("write"); | ||
| 155 | return; | ||
| 156 | } | ||
| 157 | wlen += (size_t) rc; | ||
| 158 | if (wlen == ctx->input.length) { | ||
| 159 | if (xclose(fds[0].fd)) { | ||
| 160 | error_msg_errno("close"); | ||
| 161 | return; | ||
| 162 | } | ||
| 163 | fds[0].fd = -1; | ||
| 164 | } | ||
| 165 | } | ||
| 166 | |||
| 167 | size_t active_fds = ARRAYLEN(fds); | ||
| 168 | for (size_t i = 0; i < ARRAYLEN(fds); i++) { | ||
| 169 | int rev = fds[i].revents; | ||
| 170 | if (fds[i].fd < 0 || rev & POLLNVAL) { | ||
| 171 | fds[i].fd = -1; | ||
| 172 | active_fds--; | ||
| 173 | continue; | ||
| 174 | } | ||
| 175 | if (rev & POLLERR || (rev & (POLLHUP | POLLIN)) == POLLHUP) { | ||
| 176 | if (xclose(fds[i].fd)) { | ||
| 177 | error_msg_errno("close"); | ||
| 178 | } | ||
| 179 | fds[i].fd = -1; | ||
| 180 | active_fds--; | ||
| 181 | } | ||
| 182 | } | ||
| 183 | if (active_fds == 0) { | ||
| 184 | return; | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | static int open_dev_null(int flags) | ||
| 190 | { | ||
| 191 | int fd = xopen("/dev/null", flags | O_CLOEXEC, 0); | ||
| 192 | if (unlikely(fd < 0)) { | ||
| 193 | error_msg_errno("Error opening /dev/null"); | ||
| 194 | } | ||
| 195 | return fd; | ||
| 196 | } | ||
| 197 | |||
| 198 | static int handle_child_error(pid_t pid) | ||
| 199 | { | ||
| 200 | int ret = wait_child(pid); | ||
| 201 | if (ret < 0) { | ||
| 202 | error_msg_errno("waitpid"); | ||
| 203 | } else if (ret >= 256) { | ||
| 204 | int sig = ret >> 8; | ||
| 205 | const char *str = strsignal(sig); | ||
| 206 | error_msg("Child received signal %d (%s)", sig, str ? str : "??"); | ||
| 207 | } else if (ret) { | ||
| 208 | error_msg("Child returned %d", ret); | ||
| 209 | } | ||
| 210 | return ret; | ||
| 211 | } | ||
| 212 | |||
| 213 | static void yield_terminal(EditorState *e, bool quiet) | ||
| 214 | { | ||
| 215 | if (quiet) { | ||
| 216 | term_raw_isig(); | ||
| 217 | } else { | ||
| 218 | e->child_controls_terminal = true; | ||
| 219 | ui_end(e); | ||
| 220 | } | ||
| 221 | } | ||
| 222 | |||
| 223 | static void resume_terminal(EditorState *e, bool quiet, bool prompt) | ||
| 224 | { | ||
| 225 | term_raw(); | ||
| 226 | if (!quiet && e->child_controls_terminal) { | ||
| 227 | if (prompt) { | ||
| 228 | any_key(&e->terminal, e->options.esc_timeout); | ||
| 229 | } | ||
| 230 | ui_start(e); | ||
| 231 | e->child_controls_terminal = false; | ||
| 232 | } | ||
| 233 | } | ||
| 234 | |||
| 235 | static void exec_error(const char *argv0) | ||
| 236 | { | ||
| 237 | error_msg("Unable to exec '%s': %s", argv0, strerror(errno)); | ||
| 238 | } | ||
| 239 | |||
| 240 | bool spawn_compiler(SpawnContext *ctx, const Compiler *c, MessageArray *msgs) | ||
| 241 | { | ||
| 242 | BUG_ON(!ctx->editor); | ||
| 243 | BUG_ON(!ctx->argv[0]); | ||
| 244 | |||
| 245 | int fd[3]; | ||
| 246 | fd[0] = open_dev_null(O_RDONLY); | ||
| 247 | if (fd[0] < 0) { | ||
| 248 | return false; | ||
| 249 | } | ||
| 250 | |||
| 251 | int dev_null = open_dev_null(O_WRONLY); | ||
| 252 | if (dev_null < 0) { | ||
| 253 | xclose(fd[0]); | ||
| 254 | return false; | ||
| 255 | } | ||
| 256 | |||
| 257 | int p[2]; | ||
| 258 | if (xpipe2(p, O_CLOEXEC) != 0) { | ||
| 259 | error_msg_errno("pipe"); | ||
| 260 | xclose(dev_null); | ||
| 261 | xclose(fd[0]); | ||
| 262 | return false; | ||
| 263 | } | ||
| 264 | |||
| 265 | SpawnFlags flags = ctx->flags; | ||
| 266 | bool read_stdout = !!(flags & SPAWN_READ_STDOUT); | ||
| 267 | bool quiet = !!(flags & SPAWN_QUIET); | ||
| 268 | bool prompt = !!(flags & SPAWN_PROMPT); | ||
| 269 | if (read_stdout) { | ||
| 270 | fd[1] = p[1]; | ||
| 271 | fd[2] = quiet ? dev_null : 2; | ||
| 272 | } else { | ||
| 273 | fd[1] = quiet ? dev_null : 1; | ||
| 274 | fd[2] = p[1]; | ||
| 275 | } | ||
| 276 | |||
| 277 | yield_terminal(ctx->editor, quiet); | ||
| 278 | pid_t pid = fork_exec(ctx->argv, NULL, fd, quiet); | ||
| 279 | if (pid == -1) { | ||
| 280 | exec_error(ctx->argv[0]); | ||
| 281 | xclose(p[1]); | ||
| 282 | prompt = false; | ||
| 283 | } else { | ||
| 284 | // Must close write end of the pipe before read_errors() or | ||
| 285 | // the read end never gets EOF! | ||
| 286 | xclose(p[1]); | ||
| 287 | read_errors(c, msgs, p[0], quiet); | ||
| 288 | handle_child_error(pid); | ||
| 289 | } | ||
| 290 | resume_terminal(ctx->editor, quiet, prompt); | ||
| 291 | |||
| 292 | xclose(p[0]); | ||
| 293 | xclose(dev_null); | ||
| 294 | xclose(fd[0]); | ||
| 295 | return (pid != -1); | ||
| 296 | } | ||
| 297 | |||
| 298 | // Close fd only if valid (positive) and not stdin/stdout/stderr | ||
| 299 | static int safe_xclose(int fd) | ||
| 300 | { | ||
| 301 | return (fd > STDERR_FILENO) ? xclose(fd) : 0; | ||
| 302 | } | ||
| 303 | |||
| 304 | static void safe_xclose_all(int fds[], size_t nr_fds) | ||
| 305 | { | ||
| 306 | for (size_t i = 0; i < nr_fds; i++) { | ||
| 307 | safe_xclose(fds[i]); | ||
| 308 | fds[i] = -1; | ||
| 309 | } | ||
| 310 | } | ||
| 311 | |||
| 312 | UNITTEST { | ||
| 313 | int fds[] = {-2, -3, -4}; | ||
| 314 | safe_xclose_all(fds, 2); | ||
| 315 | BUG_ON(fds[0] != -1); | ||
| 316 | BUG_ON(fds[1] != -1); | ||
| 317 | BUG_ON(fds[2] != -4); | ||
| 318 | safe_xclose_all(fds, 3); | ||
| 319 | BUG_ON(fds[2] != -1); | ||
| 320 | } | ||
| 321 | |||
| 322 | int spawn(SpawnContext *ctx) | ||
| 323 | { | ||
| 324 | BUG_ON(!ctx->editor); | ||
| 325 | BUG_ON(!ctx->argv[0]); | ||
| 326 | |||
| 327 | int child_fds[3] = {-1, -1, -1}; | ||
| 328 | int parent_fds[3] = {-1, -1, -1}; | ||
| 329 | bool quiet = !!(ctx->flags & SPAWN_QUIET); | ||
| 330 | size_t nr_pipes = 0; | ||
| 331 | |||
| 332 | for (size_t i = 0; i < ARRAYLEN(child_fds); i++) { | ||
| 333 | switch (ctx->actions[i]) { | ||
| 334 | case SPAWN_TTY: | ||
| 335 | if (!quiet) { | ||
| 336 | child_fds[i] = i; | ||
| 337 | break; | ||
| 338 | } | ||
| 339 | // Fallthrough | ||
| 340 | case SPAWN_NULL: | ||
| 341 | child_fds[i] = open_dev_null(O_RDWR); | ||
| 342 | if (child_fds[i] < 0) { | ||
| 343 | goto error_close; | ||
| 344 | } | ||
| 345 | break; | ||
| 346 | case SPAWN_PIPE: { | ||
| 347 | int p[2]; | ||
| 348 | if (xpipe2(p, O_CLOEXEC) != 0) { | ||
| 349 | error_msg_errno("pipe"); | ||
| 350 | goto error_close; | ||
| 351 | } | ||
| 352 | BUG_ON(p[0] <= STDERR_FILENO); | ||
| 353 | BUG_ON(p[1] <= STDERR_FILENO); | ||
| 354 | child_fds[i] = i ? p[1] : p[0]; | ||
| 355 | parent_fds[i] = i ? p[0] : p[1]; | ||
| 356 | if (!fd_set_nonblock(parent_fds[i], true)) { | ||
| 357 | error_msg_errno("fcntl"); | ||
| 358 | goto error_close; | ||
| 359 | } | ||
| 360 | nr_pipes++; | ||
| 361 | break; | ||
| 362 | } | ||
| 363 | default: | ||
| 364 | BUG("unhandled action type"); | ||
| 365 | goto error_close; | ||
| 366 | } | ||
| 367 | } | ||
| 368 | |||
| 369 | yield_terminal(ctx->editor, quiet); | ||
| 370 | pid_t pid = fork_exec(ctx->argv, ctx->env, child_fds, quiet); | ||
| 371 | if (pid == -1) { | ||
| 372 | exec_error(ctx->argv[0]); | ||
| 373 | goto error_resume; | ||
| 374 | } | ||
| 375 | |||
| 376 | safe_xclose_all(child_fds, ARRAYLEN(child_fds)); | ||
| 377 | if (nr_pipes > 0) { | ||
| 378 | handle_piped_data(parent_fds, ctx); | ||
| 379 | } | ||
| 380 | |||
| 381 | safe_xclose_all(parent_fds, ARRAYLEN(parent_fds)); | ||
| 382 | int err = wait_child(pid); | ||
| 383 | if (err < 0) { | ||
| 384 | error_msg_errno("waitpid"); | ||
| 385 | } | ||
| 386 | |||
| 387 | resume_terminal(ctx->editor, quiet, !!(ctx->flags & SPAWN_PROMPT)); | ||
| 388 | return err; | ||
| 389 | |||
| 390 | error_resume: | ||
| 391 | resume_terminal(ctx->editor, quiet, false); | ||
| 392 | error_close: | ||
| 393 | safe_xclose_all(child_fds, ARRAYLEN(child_fds)); | ||
| 394 | safe_xclose_all(parent_fds, ARRAYLEN(parent_fds)); | ||
| 395 | return -1; | ||
| 396 | } | ||
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 @@ | |||
| 1 | #ifndef SPAWN_H | ||
| 2 | #define SPAWN_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "compiler.h" | ||
| 6 | #include "editor.h" | ||
| 7 | #include "msg.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/string.h" | ||
| 10 | #include "util/string-view.h" | ||
| 11 | |||
| 12 | typedef enum { | ||
| 13 | SPAWN_QUIET = 1 << 0, // Interpret SPAWN_TTY as SPAWN_NULL and don't yield terminal to child | ||
| 14 | SPAWN_PROMPT = 1 << 1, // Show "press any key to continue" prompt | ||
| 15 | SPAWN_READ_STDOUT = 1 << 2, // Read errors from stdout instead of stderr | ||
| 16 | } SpawnFlags; | ||
| 17 | |||
| 18 | typedef enum { | ||
| 19 | SPAWN_NULL, | ||
| 20 | SPAWN_TTY, | ||
| 21 | SPAWN_PIPE, | ||
| 22 | } SpawnAction; | ||
| 23 | |||
| 24 | typedef struct { | ||
| 25 | EditorState *editor; | ||
| 26 | const char **argv; | ||
| 27 | const char **env; | ||
| 28 | StringView input; | ||
| 29 | String outputs[2]; // For stdout/stderr | ||
| 30 | SpawnFlags flags; | ||
| 31 | SpawnAction actions[3]; | ||
| 32 | } SpawnContext; | ||
| 33 | |||
| 34 | int spawn(SpawnContext *ctx) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 35 | bool spawn_compiler(SpawnContext *ctx, const Compiler *c, MessageArray *msgs) NONNULL_ARGS WARN_UNUSED_RESULT; | ||
| 36 | |||
| 37 | #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 @@ | |||
| 1 | #include <stdbool.h> | ||
| 2 | #include <stdint.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "status.h" | ||
| 5 | #include "search.h" | ||
| 6 | #include "selection.h" | ||
| 7 | #include "util/debug.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/numtostr.h" | ||
| 10 | #include "util/utf8.h" | ||
| 11 | #include "util/xsnprintf.h" | ||
| 12 | |||
| 13 | typedef struct { | ||
| 14 | char *buf; | ||
| 15 | size_t size; | ||
| 16 | size_t pos; | ||
| 17 | size_t separator; | ||
| 18 | const Window *window; | ||
| 19 | const GlobalOptions *opts; | ||
| 20 | InputMode input_mode; | ||
| 21 | } Formatter; | ||
| 22 | |||
| 23 | typedef enum { | ||
| 24 | STATUS_INVALID = 0, | ||
| 25 | STATUS_ESCAPED_PERCENT, | ||
| 26 | STATUS_ENCODING, | ||
| 27 | STATUS_MISC, | ||
| 28 | STATUS_IS_CRLF, | ||
| 29 | STATUS_SEPARATOR_LONG, | ||
| 30 | STATUS_CURSOR_COL_BYTES, | ||
| 31 | STATUS_TOTAL_ROWS, | ||
| 32 | STATUS_BOM, | ||
| 33 | STATUS_FILENAME, | ||
| 34 | STATUS_MODIFIED, | ||
| 35 | STATUS_LINE_ENDING, | ||
| 36 | STATUS_OVERWRITE, | ||
| 37 | STATUS_SCROLL_POSITION, | ||
| 38 | STATUS_READONLY, | ||
| 39 | STATUS_SEPARATOR, | ||
| 40 | STATUS_FILETYPE, | ||
| 41 | STATUS_UNICODE, | ||
| 42 | STATUS_CURSOR_COL, | ||
| 43 | STATUS_CURSOR_ROW, | ||
| 44 | } FormatSpecifierType; | ||
| 45 | |||
| 46 | static FormatSpecifierType lookup_format_specifier(unsigned char ch) | ||
| 47 | { | ||
| 48 | switch (ch) { | ||
| 49 | case '%': return STATUS_ESCAPED_PERCENT; | ||
| 50 | case 'E': return STATUS_ENCODING; | ||
| 51 | case 'M': return STATUS_MISC; | ||
| 52 | case 'N': return STATUS_IS_CRLF; | ||
| 53 | case 'S': return STATUS_SEPARATOR_LONG; | ||
| 54 | case 'X': return STATUS_CURSOR_COL_BYTES; | ||
| 55 | case 'Y': return STATUS_TOTAL_ROWS; | ||
| 56 | case 'b': return STATUS_BOM; | ||
| 57 | case 'f': return STATUS_FILENAME; | ||
| 58 | case 'm': return STATUS_MODIFIED; | ||
| 59 | case 'n': return STATUS_LINE_ENDING; | ||
| 60 | case 'o': return STATUS_OVERWRITE; | ||
| 61 | case 'p': return STATUS_SCROLL_POSITION; | ||
| 62 | case 'r': return STATUS_READONLY; | ||
| 63 | case 's': return STATUS_SEPARATOR; | ||
| 64 | case 't': return STATUS_FILETYPE; | ||
| 65 | case 'u': return STATUS_UNICODE; | ||
| 66 | case 'x': return STATUS_CURSOR_COL; | ||
| 67 | case 'y': return STATUS_CURSOR_ROW; | ||
| 68 | } | ||
| 69 | return STATUS_INVALID; | ||
| 70 | } | ||
| 71 | |||
| 72 | #define add_status_literal(f, s) add_status_bytes(f, s, STRLEN(s)) | ||
| 73 | |||
| 74 | static void add_ch(Formatter *f, char ch) | ||
| 75 | { | ||
| 76 | f->buf[f->pos++] = ch; | ||
| 77 | } | ||
| 78 | |||
| 79 | static void add_separator(Formatter *f) | ||
| 80 | { | ||
| 81 | while (f->separator && f->pos < f->size) { | ||
| 82 | add_ch(f, ' '); | ||
| 83 | f->separator--; | ||
| 84 | } | ||
| 85 | } | ||
| 86 | |||
| 87 | static void add_status_str(Formatter *f, const char *str) | ||
| 88 | { | ||
| 89 | BUG_ON(!str); | ||
| 90 | if (unlikely(!str[0])) { | ||
| 91 | return; | ||
| 92 | } | ||
| 93 | add_separator(f); | ||
| 94 | size_t idx = 0; | ||
| 95 | while (f->pos < f->size && str[idx]) { | ||
| 96 | u_set_char(f->buf, &f->pos, u_str_get_char(str, &idx)); | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | static void add_status_bytes(Formatter *f, const char *str, size_t len) | ||
| 101 | { | ||
| 102 | if (unlikely(len == 0)) { | ||
| 103 | return; | ||
| 104 | } | ||
| 105 | add_separator(f); | ||
| 106 | if (f->pos >= f->size) { | ||
| 107 | return; | ||
| 108 | } | ||
| 109 | const size_t avail = f->size - f->pos; | ||
| 110 | len = MIN(len, avail); | ||
| 111 | memcpy(f->buf + f->pos, str, len); | ||
| 112 | f->pos += len; | ||
| 113 | } | ||
| 114 | |||
| 115 | PRINTF(2) | ||
| 116 | static void add_status_format(Formatter *f, const char *format, ...) | ||
| 117 | { | ||
| 118 | char buf[1024]; | ||
| 119 | va_list ap; | ||
| 120 | va_start(ap, format); | ||
| 121 | size_t len = xvsnprintf(buf, sizeof(buf), format, ap); | ||
| 122 | va_end(ap); | ||
| 123 | add_status_bytes(f, buf, len); | ||
| 124 | } | ||
| 125 | |||
| 126 | static void add_status_umax(Formatter *f, uintmax_t x) | ||
| 127 | { | ||
| 128 | char buf[DECIMAL_STR_MAX(x)]; | ||
| 129 | size_t len = buf_umax_to_str(x, buf); | ||
| 130 | add_status_bytes(f, buf, len); | ||
| 131 | } | ||
| 132 | |||
| 133 | static void add_status_pos(Formatter *f) | ||
| 134 | { | ||
| 135 | size_t lines = f->window->view->buffer->nl; | ||
| 136 | int h = f->window->edit_h; | ||
| 137 | long pos = f->window->view->vy; | ||
| 138 | if (lines <= h) { | ||
| 139 | if (pos) { | ||
| 140 | add_status_literal(f, "Bot"); | ||
| 141 | } else { | ||
| 142 | add_status_literal(f, "All"); | ||
| 143 | } | ||
| 144 | } else if (pos == 0) { | ||
| 145 | add_status_literal(f, "Top"); | ||
| 146 | } else if (pos + h - 1 >= lines) { | ||
| 147 | add_status_literal(f, "Bot"); | ||
| 148 | } else { | ||
| 149 | unsigned int d = lines - (h - 1); | ||
| 150 | unsigned int percent = (pos * 100 + d / 2) / d; | ||
| 151 | BUG_ON(percent > 100); | ||
| 152 | char buf[4]; | ||
| 153 | size_t len = buf_uint_to_str(percent, buf); | ||
| 154 | buf[len++] = '%'; | ||
| 155 | add_status_bytes(f, buf, len); | ||
| 156 | } | ||
| 157 | } | ||
| 158 | |||
| 159 | static void add_misc_status(Formatter *f) | ||
| 160 | { | ||
| 161 | static const struct { | ||
| 162 | const char str[24]; | ||
| 163 | size_t len; | ||
| 164 | } css_strs[] = { | ||
| 165 | [CSS_FALSE] = {STRN("[case-sensitive = false]")}, | ||
| 166 | [CSS_TRUE] = {STRN("[case-sensitive = true]")}, | ||
| 167 | [CSS_AUTO] = {STRN("[case-sensitive = auto]")}, | ||
| 168 | }; | ||
| 169 | |||
| 170 | if (f->input_mode == INPUT_SEARCH) { | ||
| 171 | SearchCaseSensitivity css = f->opts->case_sensitive_search; | ||
| 172 | BUG_ON(css >= ARRAYLEN(css_strs)); | ||
| 173 | add_status_bytes(f, css_strs[css].str, css_strs[css].len); | ||
| 174 | return; | ||
| 175 | } | ||
| 176 | |||
| 177 | const View *view = f->window->view; | ||
| 178 | if (view->selection == SELECT_NONE) { | ||
| 179 | return; | ||
| 180 | } | ||
| 181 | |||
| 182 | SelectionInfo si; | ||
| 183 | init_selection(view, &si); | ||
| 184 | bool is_lines = (view->selection == SELECT_LINES); | ||
| 185 | size_t n = is_lines ? get_nr_selected_lines(&si) : get_nr_selected_chars(&si); | ||
| 186 | const char *unit = is_lines ? "line" : "char"; | ||
| 187 | const char *plural = unlikely(n == 1) ? "" : "s"; | ||
| 188 | add_status_format(f, "[%zu %s%s]", n, unit, plural); | ||
| 189 | } | ||
| 190 | |||
| 191 | void sf_format ( | ||
| 192 | const Window *window, | ||
| 193 | const GlobalOptions *opts, | ||
| 194 | InputMode mode, | ||
| 195 | char *buf, // NOLINT(readability-non-const-parameter) | ||
| 196 | size_t size, | ||
| 197 | const char *format | ||
| 198 | ) { | ||
| 199 | BUG_ON(size < 16); | ||
| 200 | Formatter f = { | ||
| 201 | .window = window, | ||
| 202 | .opts = opts, | ||
| 203 | .input_mode = mode, | ||
| 204 | .buf = buf, | ||
| 205 | .size = size - 5, // Max length of char and terminating NUL | ||
| 206 | }; | ||
| 207 | |||
| 208 | const View *view = window->view; | ||
| 209 | const Buffer *buffer = view->buffer; | ||
| 210 | CodePoint u; | ||
| 211 | |||
| 212 | while (f.pos < f.size && *format) { | ||
| 213 | unsigned char ch = *format++; | ||
| 214 | if (ch != '%') { | ||
| 215 | add_separator(&f); | ||
| 216 | add_ch(&f, ch); | ||
| 217 | continue; | ||
| 218 | } | ||
| 219 | |||
| 220 | switch (lookup_format_specifier(*format++)) { | ||
| 221 | case STATUS_BOM: | ||
| 222 | if (buffer->bom) { | ||
| 223 | add_status_literal(&f, "BOM"); | ||
| 224 | } | ||
| 225 | break; | ||
| 226 | case STATUS_FILENAME: | ||
| 227 | add_status_str(&f, buffer_filename(buffer)); | ||
| 228 | break; | ||
| 229 | case STATUS_MODIFIED: | ||
| 230 | if (buffer_modified(buffer)) { | ||
| 231 | add_separator(&f); | ||
| 232 | add_ch(&f, '*'); | ||
| 233 | } | ||
| 234 | break; | ||
| 235 | case STATUS_READONLY: | ||
| 236 | if (buffer->readonly) { | ||
| 237 | add_status_literal(&f, "RO"); | ||
| 238 | } else if (buffer->temporary) { | ||
| 239 | add_status_literal(&f, "TMP"); | ||
| 240 | } | ||
| 241 | break; | ||
| 242 | case STATUS_CURSOR_ROW: | ||
| 243 | add_status_umax(&f, view->cy + 1); | ||
| 244 | break; | ||
| 245 | case STATUS_TOTAL_ROWS: | ||
| 246 | add_status_umax(&f, buffer->nl); | ||
| 247 | break; | ||
| 248 | case STATUS_CURSOR_COL: | ||
| 249 | add_status_umax(&f, view->cx_display + 1); | ||
| 250 | break; | ||
| 251 | case STATUS_CURSOR_COL_BYTES: | ||
| 252 | add_status_umax(&f, view->cx_char + 1); | ||
| 253 | if (view->cx_display != view->cx_char) { | ||
| 254 | add_ch(&f, '-'); | ||
| 255 | add_status_umax(&f, view->cx_display + 1); | ||
| 256 | } | ||
| 257 | break; | ||
| 258 | case STATUS_SCROLL_POSITION: | ||
| 259 | add_status_pos(&f); | ||
| 260 | break; | ||
| 261 | case STATUS_ENCODING: | ||
| 262 | add_status_str(&f, buffer->encoding.name); | ||
| 263 | break; | ||
| 264 | case STATUS_MISC: | ||
| 265 | add_misc_status(&f); | ||
| 266 | break; | ||
| 267 | case STATUS_IS_CRLF: | ||
| 268 | if (buffer->crlf_newlines) { | ||
| 269 | add_status_literal(&f, "CRLF"); | ||
| 270 | } | ||
| 271 | break; | ||
| 272 | case STATUS_LINE_ENDING: | ||
| 273 | if (buffer->crlf_newlines) { | ||
| 274 | add_status_literal(&f, "CRLF"); | ||
| 275 | } else { | ||
| 276 | add_status_literal(&f, "LF"); | ||
| 277 | } | ||
| 278 | break; | ||
| 279 | case STATUS_OVERWRITE: | ||
| 280 | if (buffer->options.overwrite) { | ||
| 281 | add_status_literal(&f, "OVR"); | ||
| 282 | } else { | ||
| 283 | add_status_literal(&f, "INS"); | ||
| 284 | } | ||
| 285 | break; | ||
| 286 | case STATUS_SEPARATOR_LONG: | ||
| 287 | f.separator = 3; | ||
| 288 | break; | ||
| 289 | case STATUS_SEPARATOR: | ||
| 290 | f.separator = 1; | ||
| 291 | break; | ||
| 292 | case STATUS_FILETYPE: | ||
| 293 | add_status_str(&f, buffer->options.filetype); | ||
| 294 | break; | ||
| 295 | case STATUS_UNICODE: | ||
| 296 | if (unlikely(!block_iter_get_char(&view->cursor, &u))) { | ||
| 297 | break; | ||
| 298 | } | ||
| 299 | if (u_is_unicode(u)) { | ||
| 300 | char str[STRLEN("U+10FFFF") + 1]; | ||
| 301 | str[0] = 'U'; | ||
| 302 | str[1] = '+'; | ||
| 303 | size_t ndigits = buf_umax_to_hex_str(u, str + 2, 4); | ||
| 304 | add_status_bytes(&f, str, 2 + ndigits); | ||
| 305 | } else { | ||
| 306 | add_status_literal(&f, "Invalid"); | ||
| 307 | } | ||
| 308 | break; | ||
| 309 | case STATUS_ESCAPED_PERCENT: | ||
| 310 | add_separator(&f); | ||
| 311 | add_ch(&f, '%'); | ||
| 312 | break; | ||
| 313 | case STATUS_INVALID: | ||
| 314 | default: | ||
| 315 | BUG("should be unreachable, due to validate_statusline_format()"); | ||
| 316 | } | ||
| 317 | } | ||
| 318 | |||
| 319 | f.buf[f.pos] = '\0'; | ||
| 320 | } | ||
| 321 | |||
| 322 | // Returns the offset of the first invalid format specifier, or 0 if | ||
| 323 | // the whole format string is valid. It's safe to use 0 to indicate | ||
| 324 | // "no errors", since it's not possible for there to be an error at | ||
| 325 | // the very start of the string. | ||
| 326 | size_t statusline_format_find_error(const char *str) | ||
| 327 | { | ||
| 328 | for (size_t i = 0; str[i]; ) { | ||
| 329 | if (str[i++] != '%') { | ||
| 330 | continue; | ||
| 331 | } | ||
| 332 | if (lookup_format_specifier(str[i++]) == STATUS_INVALID) { | ||
| 333 | return i - 1; | ||
| 334 | } | ||
| 335 | } | ||
| 336 | return 0; | ||
| 337 | } | ||
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 @@ | |||
| 1 | #ifndef STATUS_H | ||
| 2 | #define STATUS_H | ||
| 3 | |||
| 4 | #include <stddef.h> | ||
| 5 | #include "editor.h" | ||
| 6 | #include "options.h" | ||
| 7 | #include "window.h" | ||
| 8 | |||
| 9 | size_t statusline_format_find_error(const char *str); | ||
| 10 | |||
| 11 | void sf_format ( | ||
| 12 | const Window *window, | ||
| 13 | const GlobalOptions *opts, | ||
| 14 | InputMode mode, | ||
| 15 | char *buf, | ||
| 16 | size_t size, | ||
| 17 | const char *format | ||
| 18 | ); | ||
| 19 | |||
| 20 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <sys/stat.h> | ||
| 4 | #include <sys/types.h> | ||
| 5 | #include <unistd.h> | ||
| 6 | #include "tag.h" | ||
| 7 | #include "error.h" | ||
| 8 | #include "util/debug.h" | ||
| 9 | #include "util/log.h" | ||
| 10 | #include "util/path.h" | ||
| 11 | #include "util/str-util.h" | ||
| 12 | #include "util/xmalloc.h" | ||
| 13 | #include "util/xreadwrite.h" | ||
| 14 | |||
| 15 | static const char *current_filename; // For sorting tags | ||
| 16 | |||
| 17 | static int visibility_cmp(const Tag *a, const Tag *b) | ||
| 18 | { | ||
| 19 | bool a_this_file = false; | ||
| 20 | bool b_this_file = false; | ||
| 21 | |||
| 22 | if (!a->local && !b->local) { | ||
| 23 | return 0; | ||
| 24 | } | ||
| 25 | |||
| 26 | // Is tag visibility limited to the current file? | ||
| 27 | if (a->local) { | ||
| 28 | a_this_file = current_filename && strview_equal_cstring(&a->filename, current_filename); | ||
| 29 | } | ||
| 30 | if (b->local) { | ||
| 31 | b_this_file = current_filename && strview_equal_cstring(&b->filename, current_filename); | ||
| 32 | } | ||
| 33 | |||
| 34 | // Tags local to other file than current are not interesting | ||
| 35 | if (a->local && !a_this_file) { | ||
| 36 | // a is not interesting | ||
| 37 | if (b->local && !b_this_file) { | ||
| 38 | // b is equally uninteresting | ||
| 39 | return 0; | ||
| 40 | } | ||
| 41 | // b is more interesting, sort it before a | ||
| 42 | return 1; | ||
| 43 | } | ||
| 44 | if (b->local && !b_this_file) { | ||
| 45 | // b is not interesting | ||
| 46 | return -1; | ||
| 47 | } | ||
| 48 | |||
| 49 | // Both are NOT UNinteresting | ||
| 50 | |||
| 51 | if (a->local && a_this_file) { | ||
| 52 | if (b->local && b_this_file) { | ||
| 53 | return 0; | ||
| 54 | } | ||
| 55 | // a is more interesting because it is local symbol | ||
| 56 | return -1; | ||
| 57 | } | ||
| 58 | if (b->local && b_this_file) { | ||
| 59 | // b is more interesting because it is local symbol | ||
| 60 | return 1; | ||
| 61 | } | ||
| 62 | return 0; | ||
| 63 | } | ||
| 64 | |||
| 65 | static int kind_cmp(const Tag *a, const Tag *b) | ||
| 66 | { | ||
| 67 | if (a->kind == b->kind) { | ||
| 68 | return 0; | ||
| 69 | } | ||
| 70 | |||
| 71 | // Struct member (m) is not very interesting | ||
| 72 | if (a->kind == 'm') { | ||
| 73 | return 1; | ||
| 74 | } | ||
| 75 | if (b->kind == 'm') { | ||
| 76 | return -1; | ||
| 77 | } | ||
| 78 | |||
| 79 | // Global variable (v) is not very interesting | ||
| 80 | if (a->kind == 'v') { | ||
| 81 | return 1; | ||
| 82 | } | ||
| 83 | if (b->kind == 'v') { | ||
| 84 | return -1; | ||
| 85 | } | ||
| 86 | |||
| 87 | // Struct (s), union (u) | ||
| 88 | return 0; | ||
| 89 | } | ||
| 90 | |||
| 91 | static int tag_cmp(const void *ap, const void *bp) | ||
| 92 | { | ||
| 93 | const Tag *const *a = ap; | ||
| 94 | const Tag *const *b = bp; | ||
| 95 | int r = visibility_cmp(*a, *b); | ||
| 96 | return r ? r : kind_cmp(*a, *b); | ||
| 97 | } | ||
| 98 | |||
| 99 | // Find "tags" file from directory path and its parent directories | ||
| 100 | static int open_tag_file(char *path) | ||
| 101 | { | ||
| 102 | static const char tags[] = "tags"; | ||
| 103 | while (*path) { | ||
| 104 | size_t len = strlen(path); | ||
| 105 | char *slash = strrchr(path, '/'); | ||
| 106 | if (slash != path + len - 1) { | ||
| 107 | path[len++] = '/'; | ||
| 108 | } | ||
| 109 | memcpy(path + len, tags, sizeof(tags)); | ||
| 110 | int fd = xopen(path, O_RDONLY | O_CLOEXEC, 0); | ||
| 111 | if (fd >= 0) { | ||
| 112 | return fd; | ||
| 113 | } | ||
| 114 | if (errno != ENOENT) { | ||
| 115 | return -1; | ||
| 116 | } | ||
| 117 | *slash = '\0'; | ||
| 118 | } | ||
| 119 | errno = ENOENT; | ||
| 120 | return -1; | ||
| 121 | } | ||
| 122 | |||
| 123 | static bool tag_file_changed ( | ||
| 124 | const TagFile *tf, | ||
| 125 | const char *filename, | ||
| 126 | const struct stat *st | ||
| 127 | ) { | ||
| 128 | return tf->mtime != st->st_mtime || !streq(tf->filename, filename); | ||
| 129 | } | ||
| 130 | |||
| 131 | // Note: does not free `tf` itself | ||
| 132 | void tag_file_free(TagFile *tf) | ||
| 133 | { | ||
| 134 | free(tf->filename); | ||
| 135 | free(tf->buf); | ||
| 136 | *tf = (TagFile){.filename = NULL}; | ||
| 137 | } | ||
| 138 | |||
| 139 | static bool load_tag_file(TagFile *tf) | ||
| 140 | { | ||
| 141 | char path[4096]; | ||
| 142 | if (unlikely(!getcwd(path, sizeof(path) - STRLEN("/tags")))) { | ||
| 143 | LOG_ERRNO("getcwd"); | ||
| 144 | return false; | ||
| 145 | } | ||
| 146 | |||
| 147 | int fd = open_tag_file(path); | ||
| 148 | if (fd < 0) { | ||
| 149 | return false; | ||
| 150 | } | ||
| 151 | |||
| 152 | struct stat st; | ||
| 153 | if (unlikely(fstat(fd, &st) != 0)) { | ||
| 154 | LOG_ERRNO("fstat"); | ||
| 155 | xclose(fd); | ||
| 156 | return false; | ||
| 157 | } | ||
| 158 | |||
| 159 | if (unlikely(st.st_size <= 0)) { | ||
| 160 | xclose(fd); | ||
| 161 | return false; | ||
| 162 | } | ||
| 163 | |||
| 164 | if (tf->filename) { | ||
| 165 | if (!tag_file_changed(tf, path, &st)) { | ||
| 166 | xclose(fd); | ||
| 167 | return true; | ||
| 168 | } | ||
| 169 | tag_file_free(tf); | ||
| 170 | BUG_ON(tf->filename); | ||
| 171 | } | ||
| 172 | |||
| 173 | char *buf = malloc(st.st_size); | ||
| 174 | if (unlikely(!buf)) { | ||
| 175 | LOG_ERRNO("malloc"); | ||
| 176 | xclose(fd); | ||
| 177 | return false; | ||
| 178 | } | ||
| 179 | |||
| 180 | ssize_t size = xread_all(fd, buf, st.st_size); | ||
| 181 | xclose(fd); | ||
| 182 | if (size < 0) { | ||
| 183 | free(buf); | ||
| 184 | return false; | ||
| 185 | } | ||
| 186 | |||
| 187 | *tf = (TagFile) { | ||
| 188 | .filename = xstrdup(path), | ||
| 189 | .buf = buf, | ||
| 190 | .size = size, | ||
| 191 | .mtime = st.st_mtime, | ||
| 192 | }; | ||
| 193 | |||
| 194 | return true; | ||
| 195 | } | ||
| 196 | |||
| 197 | static void free_tags_cb(Tag *t) | ||
| 198 | { | ||
| 199 | free_tag(t); | ||
| 200 | free(t); | ||
| 201 | } | ||
| 202 | |||
| 203 | static void free_tags(PointerArray *tags) | ||
| 204 | { | ||
| 205 | ptr_array_free_cb(tags, FREE_FUNC(free_tags_cb)); | ||
| 206 | } | ||
| 207 | |||
| 208 | // Both parameters must be absolute and clean | ||
| 209 | static const char *path_slice_relative(const char *filename, const StringView dir) | ||
| 210 | { | ||
| 211 | if (strncmp(filename, dir.data, dir.length) != 0) { | ||
| 212 | // Filename doesn't start with dir | ||
| 213 | return NULL; | ||
| 214 | } | ||
| 215 | switch (filename[dir.length]) { | ||
| 216 | case '\0': // Equal strings | ||
| 217 | return "."; | ||
| 218 | case '/': | ||
| 219 | return filename + dir.length + 1; | ||
| 220 | } | ||
| 221 | return NULL; | ||
| 222 | } | ||
| 223 | |||
| 224 | static void tag_file_find_tags ( | ||
| 225 | const TagFile *tf, | ||
| 226 | const char *filename, | ||
| 227 | const StringView *name, | ||
| 228 | PointerArray *tags | ||
| 229 | ) { | ||
| 230 | Tag *t = xnew(Tag, 1); | ||
| 231 | size_t pos = 0; | ||
| 232 | while (next_tag(tf->buf, tf->size, &pos, name, true, t)) { | ||
| 233 | ptr_array_append(tags, t); | ||
| 234 | t = xnew(Tag, 1); | ||
| 235 | } | ||
| 236 | free(t); | ||
| 237 | |||
| 238 | if (!filename) { | ||
| 239 | current_filename = NULL; | ||
| 240 | } else { | ||
| 241 | StringView dir = path_slice_dirname(tf->filename); | ||
| 242 | current_filename = path_slice_relative(filename, dir); | ||
| 243 | } | ||
| 244 | ptr_array_sort(tags, tag_cmp); | ||
| 245 | current_filename = NULL; | ||
| 246 | } | ||
| 247 | |||
| 248 | // Note: this moves ownership of tag->pattern to the generated Message | ||
| 249 | // and assigns NULL to the old pointer | ||
| 250 | void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir) | ||
| 251 | { | ||
| 252 | BUG_ON(dir->length == 0); | ||
| 253 | BUG_ON(dir->data[0] != '/'); | ||
| 254 | |||
| 255 | static const char prefix[] = "Tag "; | ||
| 256 | size_t prefix_len = sizeof(prefix) - 1; | ||
| 257 | size_t msg_len = prefix_len + tag->name.length; | ||
| 258 | Message *m = xmalloc(sizeof(*m) + msg_len + 1); | ||
| 259 | |||
| 260 | memcpy(m->msg, prefix, prefix_len); | ||
| 261 | memcpy(m->msg + prefix_len, tag->name.data, tag->name.length); | ||
| 262 | m->msg[msg_len] = '\0'; | ||
| 263 | |||
| 264 | m->loc = xnew0(FileLocation, 1); | ||
| 265 | m->loc->filename = path_join_sv(dir, &tag->filename, false); | ||
| 266 | |||
| 267 | if (tag->pattern) { | ||
| 268 | m->loc->pattern = tag->pattern; // Message takes ownership | ||
| 269 | tag->pattern = NULL; | ||
| 270 | } else { | ||
| 271 | m->loc->line = tag->lineno; | ||
| 272 | } | ||
| 273 | |||
| 274 | add_message(messages, m); | ||
| 275 | } | ||
| 276 | |||
| 277 | size_t tag_lookup(TagFile *tf, const StringView *name, const char *filename, MessageArray *messages) | ||
| 278 | { | ||
| 279 | clear_messages(messages); | ||
| 280 | if (!load_tag_file(tf)) { | ||
| 281 | error_msg("No tags file"); | ||
| 282 | return 0; | ||
| 283 | } | ||
| 284 | |||
| 285 | // Filename helps to find correct tags | ||
| 286 | PointerArray tags = PTR_ARRAY_INIT; | ||
| 287 | tag_file_find_tags(tf, filename, name, &tags); | ||
| 288 | |||
| 289 | size_t ntags = tags.count; | ||
| 290 | if (ntags == 0) { | ||
| 291 | error_msg("Tag '%.*s' not found", (int)name->length, name->data); | ||
| 292 | return 0; | ||
| 293 | } | ||
| 294 | |||
| 295 | StringView tf_dir = path_slice_dirname(tf->filename); | ||
| 296 | for (size_t i = 0; i < ntags; i++) { | ||
| 297 | Tag *tag = tags.ptrs[i]; | ||
| 298 | add_message_for_tag(messages, tag, &tf_dir); | ||
| 299 | } | ||
| 300 | |||
| 301 | free_tags(&tags); | ||
| 302 | return ntags; | ||
| 303 | } | ||
| 304 | |||
| 305 | void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix) | ||
| 306 | { | ||
| 307 | if (!load_tag_file(tf)) { | ||
| 308 | return; | ||
| 309 | } | ||
| 310 | |||
| 311 | Tag t; | ||
| 312 | size_t pos = 0; | ||
| 313 | StringView prev = STRING_VIEW_INIT; | ||
| 314 | while (next_tag(tf->buf, tf->size, &pos, prefix, false, &t)) { | ||
| 315 | BUG_ON(t.name.length == 0); | ||
| 316 | if (prev.length == 0 || !strview_equal(&t.name, &prev)) { | ||
| 317 | ptr_array_append(a, xstrcut(t.name.data, t.name.length)); | ||
| 318 | prev = t.name; | ||
| 319 | } | ||
| 320 | free_tag(&t); | ||
| 321 | } | ||
| 322 | } | ||
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 @@ | |||
| 1 | #ifndef TAG_H | ||
| 2 | #define TAG_H | ||
| 3 | |||
| 4 | #include <sys/types.h> | ||
| 5 | #include "ctags.h" | ||
| 6 | #include "msg.h" | ||
| 7 | #include "util/macros.h" | ||
| 8 | #include "util/ptr-array.h" | ||
| 9 | #include "util/string-view.h" | ||
| 10 | |||
| 11 | typedef struct { | ||
| 12 | char *filename; | ||
| 13 | char *buf; | ||
| 14 | size_t size; | ||
| 15 | time_t mtime; | ||
| 16 | } TagFile; | ||
| 17 | |||
| 18 | void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir) NONNULL_ARGS; | ||
| 19 | size_t tag_lookup(TagFile *tf, const StringView *name, const char *filename, MessageArray *messages) NONNULL_ARG(1, 2, 4); | ||
| 20 | void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix) NONNULL_ARGS; | ||
| 21 | void tag_file_free(TagFile *tf) NONNULL_ARGS; | ||
| 22 | |||
| 23 | #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 @@ | |||
| 1 | #include <stddef.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <unistd.h> | ||
| 4 | #include "vars.h" | ||
| 5 | #include "buffer.h" | ||
| 6 | #include "editor.h" | ||
| 7 | #include "selection.h" | ||
| 8 | #include "util/array.h" | ||
| 9 | #include "util/bsearch.h" | ||
| 10 | #include "util/numtostr.h" | ||
| 11 | #include "util/path.h" | ||
| 12 | #include "util/xmalloc.h" | ||
| 13 | #include "view.h" | ||
| 14 | |||
| 15 | typedef struct { | ||
| 16 | char name[12]; | ||
| 17 | char *(*expand)(const EditorState *e); | ||
| 18 | } BuiltinVar; | ||
| 19 | |||
| 20 | static char *expand_dte_home(const EditorState *e) | ||
| 21 | { | ||
| 22 | return xstrdup(e->user_config_dir); | ||
| 23 | } | ||
| 24 | |||
| 25 | static char *expand_file(const EditorState *e) | ||
| 26 | { | ||
| 27 | if (!e->buffer || !e->buffer->abs_filename) { | ||
| 28 | return NULL; | ||
| 29 | } | ||
| 30 | return xstrdup(e->buffer->abs_filename); | ||
| 31 | } | ||
| 32 | |||
| 33 | static char *expand_file_dir(const EditorState *e) | ||
| 34 | { | ||
| 35 | if (!e->buffer || !e->buffer->abs_filename) { | ||
| 36 | return NULL; | ||
| 37 | } | ||
| 38 | return path_dirname(e->buffer->abs_filename); | ||
| 39 | } | ||
| 40 | |||
| 41 | static char *expand_rfile(const EditorState *e) | ||
| 42 | { | ||
| 43 | if (!e->buffer || !e->buffer->abs_filename) { | ||
| 44 | return NULL; | ||
| 45 | } | ||
| 46 | char buf[8192]; | ||
| 47 | const char *cwd = getcwd(buf, sizeof buf); | ||
| 48 | const char *abs = e->buffer->abs_filename; | ||
| 49 | return likely(cwd) ? path_relative(abs, cwd) : xstrdup(abs); | ||
| 50 | } | ||
| 51 | |||
| 52 | static char *expand_filetype(const EditorState *e) | ||
| 53 | { | ||
| 54 | return e->buffer ? xstrdup(e->buffer->options.filetype) : NULL; | ||
| 55 | } | ||
| 56 | |||
| 57 | static char *expand_colno(const EditorState *e) | ||
| 58 | { | ||
| 59 | return e->view ? xstrdup(umax_to_str(e->view->cx_display + 1)) : NULL; | ||
| 60 | } | ||
| 61 | |||
| 62 | static char *expand_lineno(const EditorState *e) | ||
| 63 | { | ||
| 64 | return e->view ? xstrdup(umax_to_str(e->view->cy + 1)) : NULL; | ||
| 65 | } | ||
| 66 | |||
| 67 | static char *expand_word(const EditorState *e) | ||
| 68 | { | ||
| 69 | if (!e->view) { | ||
| 70 | return NULL; | ||
| 71 | } | ||
| 72 | |||
| 73 | size_t size; | ||
| 74 | char *selection = view_get_selection(e->view, &size); | ||
| 75 | if (selection) { | ||
| 76 | xrenew(selection, size + 1); | ||
| 77 | selection[size] = '\0'; | ||
| 78 | return selection; | ||
| 79 | } | ||
| 80 | |||
| 81 | StringView word = view_get_word_under_cursor(e->view); | ||
| 82 | return word.length ? xstrcut(word.data, word.length) : NULL; | ||
| 83 | } | ||
| 84 | |||
| 85 | static const BuiltinVar normal_vars[] = { | ||
| 86 | {"COLNO", expand_colno}, | ||
| 87 | {"DTE_HOME", expand_dte_home}, | ||
| 88 | {"FILE", expand_file}, | ||
| 89 | {"FILEDIR", expand_file_dir}, | ||
| 90 | {"FILETYPE", expand_filetype}, | ||
| 91 | {"LINENO", expand_lineno}, | ||
| 92 | {"RFILE", expand_rfile}, | ||
| 93 | {"WORD", expand_word}, | ||
| 94 | }; | ||
| 95 | |||
| 96 | UNITTEST { | ||
| 97 | CHECK_BSEARCH_ARRAY(normal_vars, name, strcmp); | ||
| 98 | } | ||
| 99 | |||
| 100 | bool expand_normal_var(const char *name, char **value, const void *userdata) | ||
| 101 | { | ||
| 102 | const BuiltinVar *var = BSEARCH(name, normal_vars, vstrcmp); | ||
| 103 | if (!var) { | ||
| 104 | return false; | ||
| 105 | } | ||
| 106 | *value = var->expand(userdata); | ||
| 107 | return true; | ||
| 108 | } | ||
| 109 | |||
| 110 | void collect_normal_vars(PointerArray *a, const char *prefix) | ||
| 111 | { | ||
| 112 | COLLECT_STRING_FIELDS(normal_vars, name, a, prefix); | ||
| 113 | } | ||
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 @@ | |||
| 1 | #ifndef VARS_H | ||
| 2 | #define VARS_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include "util/macros.h" | ||
| 6 | #include "util/ptr-array.h" | ||
| 7 | |||
| 8 | bool expand_normal_var(const char *name, char **value, const void *userdata) NONNULL_ARGS; | ||
| 9 | void collect_normal_vars(PointerArray *a, const char *prefix) NONNULL_ARGS; | ||
| 10 | |||
| 11 | #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 @@ | |||
| 1 | #include "view.h" | ||
| 2 | #include "buffer.h" | ||
| 3 | #include "indent.h" | ||
| 4 | #include "util/ascii.h" | ||
| 5 | #include "util/debug.h" | ||
| 6 | #include "util/str-util.h" | ||
| 7 | #include "util/utf8.h" | ||
| 8 | #include "window.h" | ||
| 9 | |||
| 10 | void view_update_cursor_y(View *view) | ||
| 11 | { | ||
| 12 | Buffer *buffer = view->buffer; | ||
| 13 | Block *blk; | ||
| 14 | size_t nl = 0; | ||
| 15 | block_for_each(blk, &buffer->blocks) { | ||
| 16 | if (blk == view->cursor.blk) { | ||
| 17 | nl += count_nl(blk->data, view->cursor.offset); | ||
| 18 | view->cy = nl; | ||
| 19 | return; | ||
| 20 | } | ||
| 21 | nl += blk->nl; | ||
| 22 | } | ||
| 23 | BUG("unreachable"); | ||
| 24 | } | ||
| 25 | |||
| 26 | void view_update_cursor_x(View *view) | ||
| 27 | { | ||
| 28 | StringView line; | ||
| 29 | const unsigned int tw = view->buffer->options.tab_width; | ||
| 30 | const size_t cx = fetch_this_line(&view->cursor, &line); | ||
| 31 | long cx_char = 0; | ||
| 32 | long w = 0; | ||
| 33 | |||
| 34 | for (size_t idx = 0; idx < cx; cx_char++) { | ||
| 35 | CodePoint u = line.data[idx++]; | ||
| 36 | if (likely(u < 0x80)) { | ||
| 37 | if (likely(!ascii_iscntrl(u))) { | ||
| 38 | w++; | ||
| 39 | } else if (u == '\t') { | ||
| 40 | w = next_indent_width(w, tw); | ||
| 41 | } else { | ||
| 42 | w += 2; | ||
| 43 | } | ||
| 44 | } else { | ||
| 45 | idx--; | ||
| 46 | u = u_get_nonascii(line.data, line.length, &idx); | ||
| 47 | w += u_char_width(u); | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | view->cx = cx; | ||
| 52 | view->cx_char = cx_char; | ||
| 53 | view->cx_display = w; | ||
| 54 | } | ||
| 55 | |||
| 56 | static bool view_is_cursor_visible(const View *v) | ||
| 57 | { | ||
| 58 | return v->cy < v->vy || v->cy > v->vy + v->window->edit_h - 1; | ||
| 59 | } | ||
| 60 | |||
| 61 | static void view_center_to_cursor(View *v) | ||
| 62 | { | ||
| 63 | size_t lines = v->buffer->nl; | ||
| 64 | Window *window = v->window; | ||
| 65 | unsigned int hh = window->edit_h / 2; | ||
| 66 | |||
| 67 | if (window->edit_h >= lines || v->cy < hh) { | ||
| 68 | v->vy = 0; | ||
| 69 | return; | ||
| 70 | } | ||
| 71 | |||
| 72 | v->vy = v->cy - hh; | ||
| 73 | if (v->vy + window->edit_h > lines) { | ||
| 74 | // -1 makes one ~ line visible so that you know where the EOF is | ||
| 75 | v->vy -= v->vy + window->edit_h - lines - 1; | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | static void view_update_vx(View *v) | ||
| 80 | { | ||
| 81 | Window *window = v->window; | ||
| 82 | unsigned int c = 8; | ||
| 83 | |||
| 84 | if (v->cx_display - v->vx >= window->edit_w) { | ||
| 85 | v->vx = (v->cx_display - window->edit_w + c) / c * c; | ||
| 86 | } | ||
| 87 | if (v->cx_display < v->vx) { | ||
| 88 | v->vx = v->cx_display / c * c; | ||
| 89 | } | ||
| 90 | } | ||
| 91 | |||
| 92 | static void view_update_vy(View *v, unsigned int scroll_margin) | ||
| 93 | { | ||
| 94 | Window *window = v->window; | ||
| 95 | int margin = window_get_scroll_margin(window, scroll_margin); | ||
| 96 | long max_y = v->vy + window->edit_h - 1 - margin; | ||
| 97 | |||
| 98 | if (v->cy < v->vy + margin) { | ||
| 99 | v->vy = MAX(v->cy - margin, 0); | ||
| 100 | } else if (v->cy > max_y) { | ||
| 101 | v->vy += v->cy - max_y; | ||
| 102 | max_y = v->buffer->nl - window->edit_h + 1; | ||
| 103 | if (v->vy > max_y && max_y >= 0) { | ||
| 104 | v->vy = max_y; | ||
| 105 | } | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | void view_update(View *v, unsigned int scroll_margin) | ||
| 110 | { | ||
| 111 | view_update_vx(v); | ||
| 112 | if (v->force_center || (v->center_on_scroll && view_is_cursor_visible(v))) { | ||
| 113 | view_center_to_cursor(v); | ||
| 114 | } else { | ||
| 115 | view_update_vy(v, scroll_margin); | ||
| 116 | } | ||
| 117 | v->force_center = false; | ||
| 118 | v->center_on_scroll = false; | ||
| 119 | } | ||
| 120 | |||
| 121 | long view_get_preferred_x(View *v) | ||
| 122 | { | ||
| 123 | if (v->preferred_x < 0) { | ||
| 124 | view_update_cursor_x(v); | ||
| 125 | v->preferred_x = v->cx_display; | ||
| 126 | } | ||
| 127 | return v->preferred_x; | ||
| 128 | } | ||
| 129 | |||
| 130 | bool view_can_close(const View *view) | ||
| 131 | { | ||
| 132 | const Buffer *buffer = view->buffer; | ||
| 133 | return !buffer_modified(buffer) || buffer->views.count > 1; | ||
| 134 | } | ||
| 135 | |||
| 136 | StringView view_do_get_word_under_cursor(const View *view, size_t *offset_in_line) | ||
| 137 | { | ||
| 138 | StringView line; | ||
| 139 | size_t si = fetch_this_line(&view->cursor, &line); | ||
| 140 | while (si < line.length) { | ||
| 141 | size_t i = si; | ||
| 142 | if (u_is_word_char(u_get_char(line.data, line.length, &i))) { | ||
| 143 | break; | ||
| 144 | } | ||
| 145 | si = i; | ||
| 146 | } | ||
| 147 | |||
| 148 | if (si == line.length) { | ||
| 149 | *offset_in_line = 0; | ||
| 150 | return string_view(NULL, 0); | ||
| 151 | } | ||
| 152 | |||
| 153 | size_t ei = si; | ||
| 154 | while (si > 0) { | ||
| 155 | size_t i = si; | ||
| 156 | if (!u_is_word_char(u_prev_char(line.data, &i))) { | ||
| 157 | break; | ||
| 158 | } | ||
| 159 | si = i; | ||
| 160 | } | ||
| 161 | |||
| 162 | while (ei < line.length) { | ||
| 163 | size_t i = ei; | ||
| 164 | if (!u_is_word_char(u_get_char(line.data, line.length, &i))) { | ||
| 165 | break; | ||
| 166 | } | ||
| 167 | ei = i; | ||
| 168 | } | ||
| 169 | |||
| 170 | *offset_in_line = si; | ||
| 171 | return string_view(line.data + si, ei - si); | ||
| 172 | } | ||
| 173 | |||
| 174 | StringView view_get_word_under_cursor(const View *view) | ||
| 175 | { | ||
| 176 | size_t offset_in_line; | ||
| 177 | return view_do_get_word_under_cursor(view, &offset_in_line); | ||
| 178 | } | ||
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 @@ | |||
| 1 | #ifndef VIEW_H | ||
| 2 | #define VIEW_H | ||
| 3 | |||
| 4 | #include <limits.h> | ||
| 5 | #include <stdbool.h> | ||
| 6 | #include <sys/types.h> | ||
| 7 | #include "block-iter.h" | ||
| 8 | #include "util/macros.h" | ||
| 9 | #include "util/string-view.h" | ||
| 10 | |||
| 11 | typedef enum { | ||
| 12 | SELECT_NONE, | ||
| 13 | SELECT_CHARS, | ||
| 14 | SELECT_LINES, | ||
| 15 | } SelectionType; | ||
| 16 | |||
| 17 | // A view into a Buffer, with its own cursor position and selection. | ||
| 18 | // Visually speaking, each tab in a Window corresponds to a View. | ||
| 19 | typedef struct View { | ||
| 20 | struct Buffer *buffer; | ||
| 21 | struct Window *window; | ||
| 22 | BlockIter cursor; | ||
| 23 | long cx, cy; // Cursor position | ||
| 24 | long cx_display; // Visual cursor x (char widths: wide 2, tab 1-8, control 2, invalid char 4) | ||
| 25 | long cx_char; // Cursor x in characters (invalid UTF-8 character (byte) is 1 char) | ||
| 26 | long vx, vy; // Top left corner | ||
| 27 | long preferred_x; // Preferred cursor x (preferred value for cx_display) | ||
| 28 | int tt_width; // Tab title width | ||
| 29 | int tt_truncated_width; | ||
| 30 | bool center_on_scroll; // Center view to cursor if scrolled | ||
| 31 | bool force_center; // Force centering view to cursor | ||
| 32 | |||
| 33 | SelectionType selection; | ||
| 34 | SelectionType select_mode; | ||
| 35 | ssize_t sel_so; // Cursor offset when selection was started | ||
| 36 | ssize_t sel_eo; // See `SEL_EO_RECALC` below | ||
| 37 | |||
| 38 | // Used to save cursor state when multiple views share same buffer | ||
| 39 | bool restore_cursor; | ||
| 40 | size_t saved_cursor_offset; | ||
| 41 | } View; | ||
| 42 | |||
| 43 | // If View::sel_eo is set to this value it means the offset must | ||
| 44 | // be calculated from the cursor iterator. Otherwise the offset | ||
| 45 | // is precalculated and may not be the same as the cursor position | ||
| 46 | // (see search/replace code). | ||
| 47 | #define SEL_EO_RECALC SSIZE_MAX | ||
| 48 | |||
| 49 | static inline void view_reset_preferred_x(View *view) | ||
| 50 | { | ||
| 51 | view->preferred_x = -1; | ||
| 52 | } | ||
| 53 | |||
| 54 | void view_update_cursor_y(View *view) NONNULL_ARGS; | ||
| 55 | void view_update_cursor_x(View *view) NONNULL_ARGS; | ||
| 56 | void view_update(View *view, unsigned int scroll_margin) NONNULL_ARGS; | ||
| 57 | long view_get_preferred_x(View *view) NONNULL_ARGS; | ||
| 58 | bool view_can_close(const View *view) NONNULL_ARGS; | ||
| 59 | StringView view_do_get_word_under_cursor(const View *view, size_t *offset_in_line) NONNULL_ARGS; | ||
| 60 | StringView view_get_word_under_cursor(const View *view) NONNULL_ARGS; | ||
| 61 | |||
| 62 | #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 @@ | |||
| 1 | #include <errno.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <unistd.h> | ||
| 4 | #include "window.h" | ||
| 5 | #include "editor.h" | ||
| 6 | #include "error.h" | ||
| 7 | #include "file-history.h" | ||
| 8 | #include "load-save.h" | ||
| 9 | #include "lock.h" | ||
| 10 | #include "move.h" | ||
| 11 | #include "util/path.h" | ||
| 12 | #include "util/str-util.h" | ||
| 13 | #include "util/strtonum.h" | ||
| 14 | #include "util/xmalloc.h" | ||
| 15 | |||
| 16 | Window *new_window(EditorState *e) | ||
| 17 | { | ||
| 18 | Window *window = xnew0(Window, 1); | ||
| 19 | window->editor = e; | ||
| 20 | return window; | ||
| 21 | } | ||
| 22 | |||
| 23 | View *window_add_buffer(Window *window, Buffer *buffer) | ||
| 24 | { | ||
| 25 | // We rely on this being 0, for implicit initialization of | ||
| 26 | // View::selection and View::select_mode | ||
| 27 | static_assert(SELECT_NONE == 0); | ||
| 28 | |||
| 29 | View *view = xnew(View, 1); | ||
| 30 | *view = (View) { | ||
| 31 | .buffer = buffer, | ||
| 32 | .window = window, | ||
| 33 | .cursor = { | ||
| 34 | .blk = BLOCK(buffer->blocks.next), | ||
| 35 | .head = &buffer->blocks, | ||
| 36 | } | ||
| 37 | }; | ||
| 38 | |||
| 39 | ptr_array_append(&buffer->views, view); | ||
| 40 | ptr_array_append(&window->views, view); | ||
| 41 | window->update_tabbar = true; | ||
| 42 | return view; | ||
| 43 | } | ||
| 44 | |||
| 45 | View *window_open_empty_buffer(Window *window) | ||
| 46 | { | ||
| 47 | EditorState *e = window->editor; | ||
| 48 | return window_add_buffer(window, open_empty_buffer(&e->buffers, &e->options)); | ||
| 49 | } | ||
| 50 | |||
| 51 | View *window_open_buffer ( | ||
| 52 | Window *window, | ||
| 53 | const char *filename, | ||
| 54 | bool must_exist, | ||
| 55 | const Encoding *encoding | ||
| 56 | ) { | ||
| 57 | if (unlikely(filename[0] == '\0')) { | ||
| 58 | error_msg("Empty filename not allowed"); | ||
| 59 | return NULL; | ||
| 60 | } | ||
| 61 | |||
| 62 | EditorState *e = window->editor; | ||
| 63 | bool dir_missing = false; | ||
| 64 | char *absolute = path_absolute(filename); | ||
| 65 | if (absolute) { | ||
| 66 | // Already open? | ||
| 67 | Buffer *buffer = find_buffer(&e->buffers, absolute); | ||
| 68 | if (buffer) { | ||
| 69 | if (!streq(absolute, buffer->abs_filename)) { | ||
| 70 | const char *bufname = buffer_filename(buffer); | ||
| 71 | char *s = short_filename(absolute, &e->home_dir); | ||
| 72 | info_msg("%s and %s are the same file", s, bufname); | ||
| 73 | free(s); | ||
| 74 | } | ||
| 75 | free(absolute); | ||
| 76 | return window_get_view(window, buffer); | ||
| 77 | } | ||
| 78 | } else { | ||
| 79 | // Let load_buffer() create error message | ||
| 80 | dir_missing = (errno == ENOENT); | ||
| 81 | } | ||
| 82 | |||
| 83 | /* | ||
| 84 | /proc/$PID/fd/ contains symbolic links to files that have been opened | ||
| 85 | by process $PID. Some of the files may have been deleted but can still | ||
| 86 | be opened using the symbolic link but not by using the absolute path. | ||
| 87 | |||
| 88 | # create file | ||
| 89 | mkdir /tmp/x | ||
| 90 | echo foo > /tmp/x/file | ||
| 91 | |||
| 92 | # in another shell: keep the file open | ||
| 93 | tail -f /tmp/x/file | ||
| 94 | |||
| 95 | # make the absolute path unavailable | ||
| 96 | rm /tmp/x/file | ||
| 97 | rmdir /tmp/x | ||
| 98 | |||
| 99 | # this should still succeed | ||
| 100 | dte /proc/$(pidof tail)/fd/3 | ||
| 101 | */ | ||
| 102 | |||
| 103 | Buffer *buffer = buffer_new(&e->buffers, &e->options, encoding); | ||
| 104 | if (!load_buffer(buffer, filename, &e->options, must_exist)) { | ||
| 105 | remove_and_free_buffer(&e->buffers, buffer); | ||
| 106 | free(absolute); | ||
| 107 | return NULL; | ||
| 108 | } | ||
| 109 | if (unlikely(buffer->file.mode == 0 && dir_missing)) { | ||
| 110 | // New file in non-existing directory; this is usually a mistake | ||
| 111 | error_msg("Error opening %s: Directory does not exist", filename); | ||
| 112 | remove_and_free_buffer(&e->buffers, buffer); | ||
| 113 | free(absolute); | ||
| 114 | return NULL; | ||
| 115 | } | ||
| 116 | |||
| 117 | if (absolute) { | ||
| 118 | buffer->abs_filename = absolute; | ||
| 119 | } else { | ||
| 120 | // FIXME: obviously wrong | ||
| 121 | buffer->abs_filename = xstrdup(filename); | ||
| 122 | } | ||
| 123 | update_short_filename(buffer, &e->home_dir); | ||
| 124 | |||
| 125 | if (e->options.lock_files) { | ||
| 126 | if (!lock_file(buffer->abs_filename)) { | ||
| 127 | buffer->readonly = true; | ||
| 128 | } else { | ||
| 129 | buffer->locked = true; | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | if (buffer->file.mode != 0 && !buffer->readonly && access(filename, W_OK)) { | ||
| 134 | error_msg("No write permission to %s, marking read-only", filename); | ||
| 135 | buffer->readonly = true; | ||
| 136 | } | ||
| 137 | |||
| 138 | return window_add_buffer(window, buffer); | ||
| 139 | } | ||
| 140 | |||
| 141 | View *window_get_view(Window *window, Buffer *buffer) | ||
| 142 | { | ||
| 143 | View *view = window_find_view(window, buffer); | ||
| 144 | if (!view) { | ||
| 145 | // Open the buffer in other window to this window | ||
| 146 | view = window_add_buffer(window, buffer); | ||
| 147 | view->cursor = ((View*)buffer->views.ptrs[0])->cursor; | ||
| 148 | } | ||
| 149 | return view; | ||
| 150 | } | ||
| 151 | |||
| 152 | View *window_find_view(Window *window, Buffer *buffer) | ||
| 153 | { | ||
| 154 | for (size_t i = 0, n = buffer->views.count; i < n; i++) { | ||
| 155 | View *view = buffer->views.ptrs[i]; | ||
| 156 | if (view->window == window) { | ||
| 157 | return view; | ||
| 158 | } | ||
| 159 | } | ||
| 160 | // Buffer isn't open in this window | ||
| 161 | return NULL; | ||
| 162 | } | ||
| 163 | |||
| 164 | View *window_find_unclosable_view(Window *window) | ||
| 165 | { | ||
| 166 | // Check active view first | ||
| 167 | if (window->view && !view_can_close(window->view)) { | ||
| 168 | return window->view; | ||
| 169 | } | ||
| 170 | for (size_t i = 0, n = window->views.count; i < n; i++) { | ||
| 171 | View *view = window->views.ptrs[i]; | ||
| 172 | if (!view_can_close(view)) { | ||
| 173 | return view; | ||
| 174 | } | ||
| 175 | } | ||
| 176 | return NULL; | ||
| 177 | } | ||
| 178 | |||
| 179 | static void window_remove_views(Window *window) | ||
| 180 | { | ||
| 181 | while (window->views.count > 0) { | ||
| 182 | View *view = window->views.ptrs[window->views.count - 1]; | ||
| 183 | remove_view(view); | ||
| 184 | } | ||
| 185 | } | ||
| 186 | |||
| 187 | // NOTE: window->frame isn't removed | ||
| 188 | void window_free(Window *window) | ||
| 189 | { | ||
| 190 | window_remove_views(window); | ||
| 191 | free(window->views.ptrs); | ||
| 192 | window->frame = NULL; | ||
| 193 | free(window); | ||
| 194 | } | ||
| 195 | |||
| 196 | // Remove view from view->window and view->buffer->views and free it | ||
| 197 | size_t remove_view(View *view) | ||
| 198 | { | ||
| 199 | Window *window = view->window; | ||
| 200 | EditorState *e = window->editor; | ||
| 201 | if (view == window->prev_view) { | ||
| 202 | window->prev_view = NULL; | ||
| 203 | } | ||
| 204 | if (view == e->view) { | ||
| 205 | e->view = NULL; | ||
| 206 | e->buffer = NULL; | ||
| 207 | } | ||
| 208 | |||
| 209 | size_t idx = ptr_array_idx(&window->views, view); | ||
| 210 | BUG_ON(idx >= window->views.count); | ||
| 211 | ptr_array_remove_idx(&window->views, idx); | ||
| 212 | window->update_tabbar = true; | ||
| 213 | |||
| 214 | Buffer *buffer = view->buffer; | ||
| 215 | ptr_array_remove(&buffer->views, view); | ||
| 216 | if (buffer->views.count == 0) { | ||
| 217 | if (buffer->options.file_history && buffer->abs_filename) { | ||
| 218 | FileHistory *hist = &e->file_history; | ||
| 219 | file_history_add(hist, view->cy + 1, view->cx_char + 1, buffer->abs_filename); | ||
| 220 | } | ||
| 221 | remove_and_free_buffer(&e->buffers, buffer); | ||
| 222 | } | ||
| 223 | |||
| 224 | free(view); | ||
| 225 | return idx; | ||
| 226 | } | ||
| 227 | |||
| 228 | void window_close_current_view(Window *window) | ||
| 229 | { | ||
| 230 | size_t idx = remove_view(window->view); | ||
| 231 | if (window->prev_view) { | ||
| 232 | window->view = window->prev_view; | ||
| 233 | window->prev_view = NULL; | ||
| 234 | return; | ||
| 235 | } | ||
| 236 | if (window->views.count == 0) { | ||
| 237 | window_open_empty_buffer(window); | ||
| 238 | } | ||
| 239 | if (window->views.count == idx) { | ||
| 240 | idx--; | ||
| 241 | } | ||
| 242 | window->view = window->views.ptrs[idx]; | ||
| 243 | } | ||
| 244 | |||
| 245 | static void restore_cursor_from_history(const FileHistory *hist, View *view) | ||
| 246 | { | ||
| 247 | unsigned long row, col; | ||
| 248 | if (file_history_find(hist, view->buffer->abs_filename, &row, &col)) { | ||
| 249 | move_to_filepos(view, row, col); | ||
| 250 | } | ||
| 251 | } | ||
| 252 | |||
| 253 | void set_view(View *view) | ||
| 254 | { | ||
| 255 | EditorState *e = view->window->editor; | ||
| 256 | if (e->view == view) { | ||
| 257 | return; | ||
| 258 | } | ||
| 259 | |||
| 260 | // Forget previous view when changing view using any other command but open | ||
| 261 | if (e->window) { | ||
| 262 | e->window->prev_view = NULL; | ||
| 263 | } | ||
| 264 | |||
| 265 | e->view = view; | ||
| 266 | e->buffer = view->buffer; | ||
| 267 | e->window = view->window; | ||
| 268 | e->window->view = view; | ||
| 269 | |||
| 270 | if (!view->buffer->setup) { | ||
| 271 | buffer_setup(e, view->buffer); | ||
| 272 | if (view->buffer->options.file_history && view->buffer->abs_filename) { | ||
| 273 | restore_cursor_from_history(&e->file_history, view); | ||
| 274 | } | ||
| 275 | } | ||
| 276 | |||
| 277 | // view.cursor can be invalid if same buffer was modified from another view | ||
| 278 | if (view->restore_cursor) { | ||
| 279 | view->cursor.blk = BLOCK(view->buffer->blocks.next); | ||
| 280 | block_iter_goto_offset(&view->cursor, view->saved_cursor_offset); | ||
| 281 | view->restore_cursor = false; | ||
| 282 | view->saved_cursor_offset = 0; | ||
| 283 | } | ||
| 284 | |||
| 285 | // Save cursor states of views sharing same buffer | ||
| 286 | for (size_t i = 0, n = view->buffer->views.count; i < n; i++) { | ||
| 287 | View *other = view->buffer->views.ptrs[i]; | ||
| 288 | if (other != view) { | ||
| 289 | other->saved_cursor_offset = block_iter_get_offset(&other->cursor); | ||
| 290 | other->restore_cursor = true; | ||
| 291 | } | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | View *window_open_new_file(Window *window) | ||
| 296 | { | ||
| 297 | View *prev = window->view; | ||
| 298 | View *view = window_open_empty_buffer(window); | ||
| 299 | set_view(view); | ||
| 300 | window->prev_view = prev; | ||
| 301 | return view; | ||
| 302 | } | ||
| 303 | |||
| 304 | static bool buffer_is_empty_and_untouched(const Buffer *b) | ||
| 305 | { | ||
| 306 | return !b->abs_filename && b->change_head.nr_prev == 0 && !b->display_filename; | ||
| 307 | } | ||
| 308 | |||
| 309 | // If window contains only one untouched buffer it'll be closed after | ||
| 310 | // opening another file. This is done because closing the last buffer | ||
| 311 | // causes an empty buffer to be opened (windows must contain at least | ||
| 312 | // one buffer). | ||
| 313 | static bool is_useless_empty_view(const View *v) | ||
| 314 | { | ||
| 315 | return v && v->window->views.count == 1 && buffer_is_empty_and_untouched(v->buffer); | ||
| 316 | } | ||
| 317 | |||
| 318 | View *window_open_file(Window *window, const char *filename, const Encoding *encoding) | ||
| 319 | { | ||
| 320 | View *prev = window->view; | ||
| 321 | bool useless = is_useless_empty_view(prev); | ||
| 322 | View *view = window_open_buffer(window, filename, false, encoding); | ||
| 323 | if (view) { | ||
| 324 | set_view(view); | ||
| 325 | if (useless) { | ||
| 326 | remove_view(prev); | ||
| 327 | } else { | ||
| 328 | window->prev_view = prev; | ||
| 329 | } | ||
| 330 | } | ||
| 331 | return view; | ||
| 332 | } | ||
| 333 | |||
| 334 | // Open multiple files in window and return the first opened View | ||
| 335 | View *window_open_files(Window *window, char **filenames, const Encoding *encoding) | ||
| 336 | { | ||
| 337 | View *empty = window->view; | ||
| 338 | bool useless = is_useless_empty_view(empty); | ||
| 339 | View *first = NULL; | ||
| 340 | |||
| 341 | for (size_t i = 0; filenames[i]; i++) { | ||
| 342 | View *view = window_open_buffer(window, filenames[i], false, encoding); | ||
| 343 | if (view && !first) { | ||
| 344 | set_view(view); | ||
| 345 | first = view; | ||
| 346 | } | ||
| 347 | } | ||
| 348 | |||
| 349 | if (useless && window->view != empty) { | ||
| 350 | remove_view(empty); | ||
| 351 | } | ||
| 352 | |||
| 353 | return first; | ||
| 354 | } | ||
| 355 | |||
| 356 | void mark_buffer_tabbars_changed(Buffer *buffer) | ||
| 357 | { | ||
| 358 | for (size_t i = 0, n = buffer->views.count; i < n; i++) { | ||
| 359 | View *view = buffer->views.ptrs[i]; | ||
| 360 | view->window->update_tabbar = true; | ||
| 361 | } | ||
| 362 | } | ||
| 363 | |||
| 364 | static int line_numbers_width(const Window *window, const GlobalOptions *options) | ||
| 365 | { | ||
| 366 | if (!options->show_line_numbers || !window->view) { | ||
| 367 | return 0; | ||
| 368 | } | ||
| 369 | size_t width = size_str_width(window->view->buffer->nl) + 1; | ||
| 370 | return MAX(width, LINE_NUMBERS_MIN_WIDTH); | ||
| 371 | } | ||
| 372 | |||
| 373 | static int edit_x_offset(const Window *window, const GlobalOptions *options) | ||
| 374 | { | ||
| 375 | return line_numbers_width(window, options); | ||
| 376 | } | ||
| 377 | |||
| 378 | static int edit_y_offset(const GlobalOptions *options) | ||
| 379 | { | ||
| 380 | return options->tab_bar ? 1 : 0; | ||
| 381 | } | ||
| 382 | |||
| 383 | static void set_edit_size(Window *window, const GlobalOptions *options) | ||
| 384 | { | ||
| 385 | int xo = edit_x_offset(window, options); | ||
| 386 | int yo = edit_y_offset(options); | ||
| 387 | |||
| 388 | window->edit_w = window->w - xo; | ||
| 389 | window->edit_h = window->h - yo - 1; // statusline | ||
| 390 | window->edit_x = window->x + xo; | ||
| 391 | } | ||
| 392 | |||
| 393 | void calculate_line_numbers(Window *window) | ||
| 394 | { | ||
| 395 | const GlobalOptions *options = &window->editor->options; | ||
| 396 | int w = line_numbers_width(window, options); | ||
| 397 | if (w != window->line_numbers.width) { | ||
| 398 | window->line_numbers.width = w; | ||
| 399 | window->line_numbers.first = 0; | ||
| 400 | window->line_numbers.last = 0; | ||
| 401 | mark_all_lines_changed(window->view->buffer); | ||
| 402 | } | ||
| 403 | set_edit_size(window, options); | ||
| 404 | } | ||
| 405 | |||
| 406 | void set_window_coordinates(Window *window, int x, int y) | ||
| 407 | { | ||
| 408 | const GlobalOptions *options = &window->editor->options; | ||
| 409 | window->x = x; | ||
| 410 | window->y = y; | ||
| 411 | window->edit_x = x + edit_x_offset(window, options); | ||
| 412 | window->edit_y = y + edit_y_offset(options); | ||
| 413 | } | ||
| 414 | |||
| 415 | void set_window_size(Window *window, int w, int h) | ||
| 416 | { | ||
| 417 | window->w = w; | ||
| 418 | window->h = h; | ||
| 419 | calculate_line_numbers(window); | ||
| 420 | } | ||
| 421 | |||
| 422 | int window_get_scroll_margin(const Window *window, unsigned int scroll_margin) | ||
| 423 | { | ||
| 424 | int max = (window->edit_h - 1) / 2; | ||
| 425 | BUG_ON(max < 0); | ||
| 426 | return MIN(max, scroll_margin); | ||
| 427 | } | ||
| 428 | |||
| 429 | void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data) | ||
| 430 | { | ||
| 431 | if (frame->window) { | ||
| 432 | func(frame->window, data); | ||
| 433 | return; | ||
| 434 | } | ||
| 435 | for (size_t i = 0, n = frame->frames.count; i < n; i++) { | ||
| 436 | frame_for_each_window(frame->frames.ptrs[i], func, data); | ||
| 437 | } | ||
| 438 | } | ||
| 439 | |||
| 440 | typedef struct { | ||
| 441 | const Window *const target; // Window to search for (set at init.) | ||
| 442 | Window *first; // Window passed in first callback invocation | ||
| 443 | Window *last; // Window passed in last callback invocation | ||
| 444 | Window *prev; // Window immediately before target (if any) | ||
| 445 | Window *next; // Window immediately after target (if any) | ||
| 446 | bool found; // Set to true when target is found | ||
| 447 | } WindowCallbackData; | ||
| 448 | |||
| 449 | static void find_prev_and_next(Window *window, void *ud) | ||
| 450 | { | ||
| 451 | WindowCallbackData *data = ud; | ||
| 452 | data->last = window; | ||
| 453 | if (data->found) { | ||
| 454 | if (!data->next) { | ||
| 455 | data->next = window; | ||
| 456 | } | ||
| 457 | return; | ||
| 458 | } | ||
| 459 | if (!data->first) { | ||
| 460 | data->first = window; | ||
| 461 | } | ||
| 462 | if (window == data->target) { | ||
| 463 | data->found = true; | ||
| 464 | return; | ||
| 465 | } | ||
| 466 | data->prev = window; | ||
| 467 | } | ||
| 468 | |||
| 469 | Window *prev_window(Window *window) | ||
| 470 | { | ||
| 471 | WindowCallbackData data = {.target = window}; | ||
| 472 | frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data); | ||
| 473 | BUG_ON(!data.found); | ||
| 474 | return data.prev ? data.prev : data.last; | ||
| 475 | } | ||
| 476 | |||
| 477 | Window *next_window(Window *window) | ||
| 478 | { | ||
| 479 | WindowCallbackData data = {.target = window}; | ||
| 480 | frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data); | ||
| 481 | BUG_ON(!data.found); | ||
| 482 | return data.next ? data.next : data.first; | ||
| 483 | } | ||
| 484 | |||
| 485 | void window_close(Window *window) | ||
| 486 | { | ||
| 487 | EditorState *e = window->editor; | ||
| 488 | if (!window->frame->parent) { | ||
| 489 | // Don't close last window | ||
| 490 | window_remove_views(window); | ||
| 491 | set_view(window_open_empty_buffer(window)); | ||
| 492 | return; | ||
| 493 | } | ||
| 494 | |||
| 495 | WindowCallbackData data = {.target = window}; | ||
| 496 | frame_for_each_window(e->root_frame, find_prev_and_next, &data); | ||
| 497 | BUG_ON(!data.found); | ||
| 498 | Window *next_or_prev = data.next ? data.next : data.prev; | ||
| 499 | BUG_ON(!next_or_prev); | ||
| 500 | |||
| 501 | remove_frame(e, window->frame); | ||
| 502 | e->window = NULL; | ||
| 503 | set_view(next_or_prev->view); | ||
| 504 | |||
| 505 | mark_everything_changed(e); | ||
| 506 | debug_frame(e->root_frame); | ||
| 507 | } | ||
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 @@ | |||
| 1 | #ifndef WINDOW_H | ||
| 2 | #define WINDOW_H | ||
| 3 | |||
| 4 | #include <stdbool.h> | ||
| 5 | #include <stddef.h> | ||
| 6 | #include "buffer.h" | ||
| 7 | #include "encoding.h" | ||
| 8 | #include "frame.h" | ||
| 9 | #include "util/macros.h" | ||
| 10 | #include "util/ptr-array.h" | ||
| 11 | #include "view.h" | ||
| 12 | |||
| 13 | enum { | ||
| 14 | // Minimum width of line numbers bar (including padding) | ||
| 15 | LINE_NUMBERS_MIN_WIDTH = 5 | ||
| 16 | }; | ||
| 17 | |||
| 18 | // A sub-division of the screen, similar to a window in a tiling window | ||
| 19 | // manager. There can be multiple Views associated with each Window, but | ||
| 20 | // only one is visible at a time. Each tab displayed in the tab bar | ||
| 21 | // corresponds to a View and the editable text area corresponds to the | ||
| 22 | // Buffer of the *current* View (Window::view::buffer). | ||
| 23 | typedef struct Window { | ||
| 24 | struct EditorState *editor; | ||
| 25 | PointerArray views; | ||
| 26 | Frame *frame; | ||
| 27 | View *view; // Current view | ||
| 28 | View *prev_view; // Previous view, if set | ||
| 29 | int x, y; // Coordinates for top left of window | ||
| 30 | int w, h; // Width and height of window (including tabbar and status) | ||
| 31 | int edit_x, edit_y; // Top left of editable area | ||
| 32 | int edit_w, edit_h; // Width and height of editable area | ||
| 33 | size_t first_tab_idx; | ||
| 34 | bool update_tabbar; | ||
| 35 | struct { | ||
| 36 | int width; | ||
| 37 | long first; | ||
| 38 | long last; | ||
| 39 | } line_numbers; | ||
| 40 | } Window; | ||
| 41 | |||
| 42 | struct EditorState; | ||
| 43 | |||
| 44 | Window *new_window(struct EditorState *e) NONNULL_ARGS_AND_RETURN; | ||
| 45 | View *window_add_buffer(Window *window, Buffer *buffer); | ||
| 46 | View *window_open_empty_buffer(Window *window); | ||
| 47 | View *window_open_buffer(Window *window, const char *filename, bool must_exist, const Encoding *encoding); | ||
| 48 | View *window_get_view(Window *window, Buffer *buffer); | ||
| 49 | View *window_find_view(Window *window, Buffer *buffer); | ||
| 50 | View *window_find_unclosable_view(Window *window); | ||
| 51 | void window_free(Window *window); | ||
| 52 | size_t remove_view(View *view); | ||
| 53 | void window_close(Window *window); | ||
| 54 | void window_close_current_view(Window *window); | ||
| 55 | void set_view(View *view); | ||
| 56 | View *window_open_new_file(Window *window); | ||
| 57 | View *window_open_file(Window *window, const char *filename, const Encoding *encoding); | ||
| 58 | View *window_open_files(Window *window, char **filenames, const Encoding *encoding); | ||
| 59 | void mark_buffer_tabbars_changed(Buffer *buffer); | ||
| 60 | void calculate_line_numbers(Window *window); | ||
| 61 | void set_window_coordinates(Window *window, int x, int y); | ||
| 62 | void set_window_size(Window *window, int w, int h); | ||
| 63 | int window_get_scroll_margin(const Window *window, unsigned int scroll_margin); | ||
| 64 | void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data); | ||
| 65 | Window *prev_window(Window *window); | ||
| 66 | Window *next_window(Window *window); | ||
| 67 | |||
| 68 | #endif | ||
