aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
Diffstat (limited to 'examples')
-rw-r--r--examples/dte/README.md19
-rw-r--r--examples/dte/bind.c115
-rw-r--r--examples/dte/bind.h20
-rw-r--r--examples/dte/block-iter.c343
-rw-r--r--examples/dte/block-iter.h78
-rw-r--r--examples/dte/block.c19
-rw-r--r--examples/dte/block.h39
-rw-r--r--examples/dte/bookmark.c103
-rw-r--r--examples/dte/bookmark.h24
-rw-r--r--examples/dte/buffer.c480
-rw-r--r--examples/dte/buffer.h103
-rw-r--r--examples/dte/change.c417
-rw-r--r--examples/dte/change.h39
-rw-r--r--examples/dte/cmdline.c540
-rw-r--r--examples/dte/cmdline.h40
-rw-r--r--examples/dte/commands.c2594
-rw-r--r--examples/dte/commands.h22
-rw-r--r--examples/dte/compat.c35
-rw-r--r--examples/dte/compat.h8
-rw-r--r--examples/dte/compiler.c151
-rw-r--r--examples/dte/compiler.h49
-rw-r--r--examples/dte/completion.c879
-rw-r--r--examples/dte/completion.h21
-rw-r--r--examples/dte/config.c185
-rw-r--r--examples/dte/config.h42
-rw-r--r--examples/dte/convert.c581
-rw-r--r--examples/dte/convert.h24
-rw-r--r--examples/dte/copy.c74
-rw-r--r--examples/dte/copy.h26
-rw-r--r--examples/dte/ctags.c157
-rw-r--r--examples/dte/ctags.h31
-rw-r--r--examples/dte/edit.c394
-rw-r--r--examples/dte/edit.h13
-rw-r--r--examples/dte/editor.c321
-rw-r--r--examples/dte/editor.h121
-rw-r--r--examples/dte/encoding.c132
-rw-r--r--examples/dte/encoding.h46
-rw-r--r--examples/dte/error.c95
-rw-r--r--examples/dte/error.h15
-rw-r--r--examples/dte/exec.c366
-rw-r--r--examples/dte/exec.h37
-rw-r--r--examples/dte/file-history.c153
-rw-r--r--examples/dte/file-history.h29
-rw-r--r--examples/dte/file-option.c193
-rw-r--r--examples/dte/file-option.h32
-rw-r--r--examples/dte/filetype.c333
-rw-r--r--examples/dte/filetype.h35
-rw-r--r--examples/dte/frame.c496
-rw-r--r--examples/dte/frame.h48
-rw-r--r--examples/dte/history.c146
-rw-r--r--examples/dte/history.h35
-rw-r--r--examples/dte/indent.c193
-rw-r--r--examples/dte/indent.h58
-rw-r--r--examples/dte/load-save.c505
-rw-r--r--examples/dte/load-save.h14
-rw-r--r--examples/dte/lock.c201
-rw-r--r--examples/dte/lock.h12
-rw-r--r--examples/dte/main.c575
-rw-r--r--examples/dte/misc.c764
-rw-r--r--examples/dte/misc.h22
-rw-r--r--examples/dte/mode.c74
-rw-r--r--examples/dte/mode.h11
-rw-r--r--examples/dte/move.c311
-rw-r--r--examples/dte/move.h31
-rw-r--r--examples/dte/msg.c139
-rw-r--r--examples/dte/msg.h30
-rw-r--r--examples/dte/options.c987
-rw-r--r--examples/dte/options.h114
-rw-r--r--examples/dte/regexp.c151
-rw-r--r--examples/dte/regexp.h85
-rw-r--r--examples/dte/replace.c256
-rw-r--r--examples/dte/replace.h18
-rw-r--r--examples/dte/screen-cmdline.c91
-rw-r--r--examples/dte/screen-prompt.c134
-rw-r--r--examples/dte/screen-status.c46
-rw-r--r--examples/dte/screen-tabbar.c176
-rw-r--r--examples/dte/screen-view.c427
-rw-r--r--examples/dte/screen-window.c130
-rw-r--r--examples/dte/screen.c211
-rw-r--r--examples/dte/screen.h59
-rw-r--r--examples/dte/search.c244
-rw-r--r--examples/dte/search.h34
-rw-r--r--examples/dte/selection.c110
-rw-r--r--examples/dte/selection.h22
-rw-r--r--examples/dte/shift.c147
-rw-r--r--examples/dte/shift.h8
-rw-r--r--examples/dte/show.c558
-rw-r--r--examples/dte/show.h27
-rw-r--r--examples/dte/signals.c169
-rw-r--r--examples/dte/signals.h11
-rw-r--r--examples/dte/spawn.c396
-rw-r--r--examples/dte/spawn.h37
-rw-r--r--examples/dte/status.c337
-rw-r--r--examples/dte/status.h20
-rw-r--r--examples/dte/tag.c322
-rw-r--r--examples/dte/tag.h23
-rw-r--r--examples/dte/vars.c113
-rw-r--r--examples/dte/vars.h11
-rw-r--r--examples/dte/view.c178
-rw-r--r--examples/dte/view.h62
-rw-r--r--examples/dte/window.c507
-rw-r--r--examples/dte/window.h68
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 @@
1dte source code
2===============
3
4This directory contains the `dte` source code. It makes liberal use
5of ISO C99 features and POSIX 2008 APIs, but generally requires very
6little else.
7
8The 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
11void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc)
12{
13 cached_command_free(intmap_insert_or_replace(bindings, key, cc));
14}
15
16void remove_binding(IntMap *bindings, KeyCode key)
17{
18 cached_command_free(intmap_remove(bindings, key));
19}
20
21const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key)
22{
23 return intmap_get(bindings, key);
24}
25
26void free_bindings(IntMap *bindings)
27{
28 intmap_free(bindings, (FreeFunction)cached_command_free);
29}
30
31bool 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
56typedef struct {
57 KeyCode key;
58 const char *cmd;
59} KeyBinding;
60
61static 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
69UNITTEST {
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
79bool 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
12void add_binding(IntMap *bindings, KeyCode key, CachedCommand *cc) NONNULL_ARGS;
13void remove_binding(IntMap *bindings, KeyCode key) NONNULL_ARGS;
14const CachedCommand *lookup_binding(const IntMap *bindings, KeyCode key) NONNULL_ARGS;
15bool handle_binding(EditorState *e, InputMode mode, KeyCode key) NONNULL_ARGS WARN_UNUSED_RESULT;
16void free_bindings(IntMap *bindings) NONNULL_ARGS;
17bool 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
7void 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 */
20size_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 */
46size_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 */
76size_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
103size_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
109size_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
131size_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
153size_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
163size_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
174size_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
195size_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
214void 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
224void 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
236void 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
249void 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
268size_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
281char *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
306void 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
327void 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)
337size_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
12typedef struct {
13 Block *blk;
14 const ListHead *head;
15 size_t offset;
16} BlockIter;
17
18static inline void block_iter_bof(BlockIter *bi)
19{
20 bi->blk = BLOCK(bi->head->next);
21 bi->offset = 0;
22}
23
24static inline void block_iter_eof(BlockIter *bi)
25{
26 bi->blk = BLOCK(bi->head->prev);
27 bi->offset = bi->blk->size;
28}
29
30static 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
35static inline bool block_iter_is_bol(const BlockIter *bi)
36{
37 return bi->offset == 0 || bi->blk->data[bi->offset - 1] == '\n';
38}
39
40static 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
56void block_iter_normalize(BlockIter *bi);
57size_t block_iter_eat_line(BlockIter *bi);
58size_t block_iter_next_line(BlockIter *bi);
59size_t block_iter_prev_line(BlockIter *bi);
60size_t block_iter_next_char(BlockIter *bi, CodePoint *up);
61size_t block_iter_prev_char(BlockIter *bi, CodePoint *up);
62size_t block_iter_next_column(BlockIter *bi);
63size_t block_iter_prev_column(BlockIter *bi);
64size_t block_iter_bol(BlockIter *bi);
65size_t block_iter_eol(BlockIter *bi);
66void block_iter_back_bytes(BlockIter *bi, size_t count);
67void block_iter_skip_bytes(BlockIter *bi, size_t count);
68void block_iter_goto_offset(BlockIter *bi, size_t offset);
69void block_iter_goto_line(BlockIter *bi, size_t line);
70size_t block_iter_get_offset(const BlockIter *bi) WARN_UNUSED_RESULT;
71size_t block_iter_get_char(const BlockIter *bi, CodePoint *up) WARN_UNUSED_RESULT;
72char *block_iter_get_bytes(const BlockIter *bi, size_t len) WARN_UNUSED_RESULT;
73
74void fill_line_ref(BlockIter *bi, StringView *line);
75void fill_line_nl_ref(BlockIter *bi, StringView *line);
76size_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
5Block *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
14void 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
8enum {
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.
15typedef 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
30static inline Block *BLOCK(ListHead *item)
31{
32 static_assert(offsetof(Block, node) == 0);
33 return (Block*)item;
34}
35
36Block *block_new(size_t alloc) RETURNS_NONNULL;
37void 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
11FileLocation *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
24bool 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
50static 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
75void file_location_free(FileLocation *loc)
76{
77 free(loc->filename);
78 free(loc->pattern);
79 free(loc);
80}
81
82void 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
92void 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
10typedef 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
17FileLocation *get_current_file_location(const View *view) NONNULL_ARGS_AND_RETURN;
18bool file_location_go(Window *window, const FileLocation *loc) NONNULL_ARGS WARN_UNUSED_RESULT;
19void file_location_free(FileLocation *loc) NONNULL_ARGS;
20
21void bookmark_push(PointerArray *bookmarks, FileLocation *loc) NONNULL_ARGS;
22void 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
19void 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 */
34void 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
45const char *buffer_filename(const Buffer *buffer)
46{
47 const char *name = buffer->display_filename;
48 return name ? name : "(No name)";
49}
50
51void 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
67Buffer *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
93Buffer *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
105void 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
117void 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
137void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer)
138{
139 ptr_array_remove(buffers, buffer);
140 free_buffer(buffer);
141}
142
143static 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
148Buffer *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
162Buffer *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
173bool 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
192void 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
202void 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
208void 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
236static 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
242static 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
288UNITTEST {
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
317static 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
385void 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
399void 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
413String 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
21typedef 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.
34typedef 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
62static inline void mark_all_lines_changed(Buffer *buffer)
63{
64 buffer->changed_line_min = 0;
65 buffer->changed_line_max = LONG_MAX;
66}
67
68static inline bool buffer_modified(const Buffer *buffer)
69{
70 return buffer->saved_change != buffer->cur_change && !buffer->temporary;
71}
72
73static 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
82struct EditorState;
83
84void buffer_mark_lines_changed(Buffer *buffer, long min, long max) NONNULL_ARGS;
85void buffer_set_encoding(Buffer *buffer, Encoding encoding, bool utf8_bom) NONNULL_ARGS;
86const char *buffer_filename(const Buffer *buffer) NONNULL_ARGS_AND_RETURN;
87void set_display_filename(Buffer *buffer, char *name) NONNULL_ARG(1);
88void update_short_filename_cwd(Buffer *buffer, const StringView *home, const char *cwd) NONNULL_ARG(1, 2);
89void update_short_filename(Buffer *buffer, const StringView *home) NONNULL_ARGS;
90Buffer *find_buffer(const PointerArray *buffers, const char *abs_filename) NONNULL_ARGS;
91Buffer *find_buffer_by_id(const PointerArray *buffers, unsigned long id) NONNULL_ARGS;
92Buffer *buffer_new(PointerArray *buffers, const GlobalOptions *gopts, const Encoding *encoding) RETURNS_NONNULL NONNULL_ARG(1, 2);
93Buffer *open_empty_buffer(PointerArray *buffers, const GlobalOptions *gopts) NONNULL_ARGS_AND_RETURN;
94void free_buffer(Buffer *buffer) NONNULL_ARGS;
95void remove_and_free_buffer(PointerArray *buffers, Buffer *buffer) NONNULL_ARGS;
96void free_blocks(Buffer *buffer) NONNULL_ARGS;
97bool buffer_detect_filetype(Buffer *buffer, const PointerArray *filetypes) NONNULL_ARGS;
98void buffer_update_syntax(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
99void buffer_setup(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
100void buffer_count_blocks_and_bytes(const Buffer *buffer, uintmax_t counts[2]) NONNULL_ARGS;
101String 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
10static ChangeMergeEnum change_merge;
11static ChangeMergeEnum prev_change_merge;
12
13static Change *alloc_change(void)
14{
15 return xcalloc(sizeof(Change));
16}
17
18static 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
28static Change *change_barrier;
29
30static bool is_change_chain_barrier(const Change *change)
31{
32 return !change->ins_count && !change->del_count;
33}
34
35static 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
56static size_t buffer_offset(const View *view)
57{
58 return block_iter_get_offset(&view->cursor);
59}
60
61static 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
79static 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
111static 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
124void begin_change(ChangeMergeEnum m)
125{
126 change_merge = m;
127}
128
129void end_change(void)
130{
131 prev_change_merge = change_merge;
132}
133
134void 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
144void 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
156static 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
172static 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
206bool 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
235bool 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
287void free_changes(Change *c)
288{
289top:
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
310void 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
332static 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
352static 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
378void buffer_delete_bytes(View *view, size_t len)
379{
380 buffer_delete_bytes_internal(view, len, false);
381}
382
383void buffer_erase_bytes(View *view, size_t len)
384{
385 buffer_delete_bytes_internal(view, len, true);
386}
387
388void 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
9typedef enum {
10 CHANGE_MERGE_NONE,
11 CHANGE_MERGE_INSERT,
12 CHANGE_MERGE_DELETE,
13 CHANGE_MERGE_ERASE,
14} ChangeMergeEnum;
15
16typedef 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
27void begin_change(ChangeMergeEnum m);
28void end_change(void);
29void begin_change_chain(void);
30void end_change_chain(View *view) NONNULL_ARGS;
31bool undo(View *view) NONNULL_ARGS WARN_UNUSED_RESULT;
32bool redo(View *view, unsigned long change_id) NONNULL_ARGS WARN_UNUSED_RESULT;
33void free_changes(Change *c) NONNULL_ARGS;
34void buffer_insert_bytes(View *view, const char *buf, size_t len) NONNULL_ARG(1);
35void buffer_delete_bytes(View *view, size_t len) NONNULL_ARGS;
36void buffer_erase_bytes(View *view, size_t len) NONNULL_ARGS;
37void 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
20static 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
34void cmdline_clear(CommandLine *c)
35{
36 string_clear(&c->buf);
37 c->pos = 0;
38 c->search_pos = NULL;
39}
40
41void 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
49static 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
57void cmdline_set_text(CommandLine *c, const char *text)
58{
59 c->search_pos = NULL;
60 set_text(c, text);
61}
62
63static 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
71static 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
81static 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
88static 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
113static 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
123static 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
133static 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
160static 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
169static 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
182static 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
193static 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
226static 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
242static 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
256out:
257 reset_completion(c);
258 return true;
259}
260
261static 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
267static 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
273static 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
279static 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
285static 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
296static 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
309static 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
320static 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
333static 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
362static 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
383static 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
390static 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
397static 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
404static 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
423static 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
483IGNORE_WARNING("-Wincompatible-pointer-types")
484
485static 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
505static 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
512static 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
520UNIGNORE_WARNINGS
521
522static 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
528static 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
534const CommandSet cmd_mode_commands = {
535 .lookup = find_cmd_mode_command
536};
537
538const 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
13typedef 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
25typedef struct {
26 String buf;
27 size_t pos;
28 const HistoryEntry *search_pos;
29 char *search_text;
30 CompletionState completion;
31} CommandLine;
32
33extern const CommandSet cmd_mode_commands;
34extern const CommandSet search_mode_commands;
35
36void cmdline_set_text(CommandLine *c, const char *text) NONNULL_ARGS;
37void cmdline_clear(CommandLine *c) NONNULL_ARGS;
38void 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
69NOINLINE
70static 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
100static 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
111static 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
117static char last_flag(const CommandArgs *a)
118{
119 return last_flag_or_default(a, 0);
120}
121
122static bool has_flag(const CommandArgs *a, unsigned char flag)
123{
124 return cmdargs_has_flag(a, flag);
125}
126
127static 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
141static 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
147static 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
180static 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
213static 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
220static 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
233static 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
253static 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
264static 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
270static void mark_tabbar_changed(Window *window, void* UNUSED_ARG(data))
271{
272 window->update_tabbar = true;
273}
274
275static 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
319static 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
326static 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
333static 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
367static 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
377static 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
404static 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
459static 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
495static 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
517static 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
524static 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
546static 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
553static 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
561static 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
568static 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
575static 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
582static 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
603static bool cmd_erase(EditorState *e, const CommandArgs *a)
604{
605 BUG_ON(a->nr_args);
606 erase(e->view);
607 return true;
608}
609
610static 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
617static 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
625static 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
638static 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
684static 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
718static 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
753update:
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
763static 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
774static 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
789static 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
796static 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
803static 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
827static 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
848static 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
896message:
897 info_msg("%s", msg);
898 // TODO: make this conditional?
899 return true;
900}
901
902static 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
937search_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
954search_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
967not_found:
968 return error_msg("No matching bracket found");
969}
970
971static 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
993static 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
1024static bool cmd_new_line(EditorState *e, const CommandArgs *a)
1025{
1026 new_line(e->view, has_flag(a, 'a'));
1027 return true;
1028}
1029
1030static 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
1040static 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
1061static 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
1132static 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
1163static 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
1199static 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
1233static 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
1252static 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
1272static 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
1292static 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
1302static 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
1316static 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
1336static 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
1384exit:
1385 e->status = exit_code;
1386 return true;
1387}
1388
1389static 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
1406static 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
1413static 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
1466insert:
1467 insert_text(e->view, buf, bufsize, move_after);
1468 free(buf);
1469 return true;
1470}
1471
1472static 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
1508static 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
1521static 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
1528static 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
1537static 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, &times[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
1563static 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
1775error:
1776 if (new_locked) {
1777 unlock_file(absolute);
1778 }
1779 if (absolute != buffer->abs_filename) {
1780 free(absolute);
1781 }
1782 return false;
1783}
1784
1785static 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
1796static 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
1815static 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
1830static 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
1844static 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
1853static 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
1914static 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
1923static 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
1937static 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
1967static 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
1994static 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
2008static 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
2017static 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
2040static 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
2063static 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
2074static 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
2088static 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
2111static bool cmd_unselect(EditorState *e, const CommandArgs *a)
2112{
2113 BUG_ON(a->nr_args);
2114 unselect(e->view);
2115 return true;
2116}
2117
2118static 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
2125static 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
2143static 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
2164close:
2165 window_close(e->window);
2166 return true;
2167}
2168
2169static 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
2181static 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
2191static 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
2200static 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
2209static 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
2219static 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
2236static 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
2275static 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
2338static 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
2360IGNORE_WARNING("-Wincompatible-pointer-types")
2361
2362static 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
2452UNIGNORE_WARNINGS
2453
2454static 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
2484UNITTEST {
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
2520static 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
2529const Command *find_normal_command(const char *name)
2530{
2531 return BSEARCH(name, cmds, command_cmp);
2532}
2533
2534const 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
2541const char *find_normal_alias(const char *name, void *userdata)
2542{
2543 EditorState *e = userdata;
2544 return find_alias(&e->aliases, name);
2545}
2546
2547bool 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
2553void 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
2559int 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
2565void collect_normal_commands(PointerArray *a, const char *prefix)
2566{
2567 COLLECT_STRING_FIELDS(cmds, name, a, prefix);
2568}
2569
2570UNITTEST {
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
11extern const CommandSet normal_commands;
12
13struct EditorState;
14
15const Command *find_normal_command(const char *name) NONNULL_ARGS;
16const char *find_normal_alias(const char *name, void *userdata) NONNULL_ARGS;
17bool handle_normal_command(struct EditorState *e, const char *cmd, bool allow_recording) NONNULL_ARGS;
18void exec_normal_config(struct EditorState *e, StringView config) NONNULL_ARGS;
19int read_normal_config(struct EditorState *e, const char *filename, ConfigFlags flags) NONNULL_ARGS;
20void 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
3const 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
6extern 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
13static const char capture_names[][8] = {
14 [ERRFMT_FILE] = "file",
15 [ERRFMT_LINE] = "line",
16 [ERRFMT_COLUMN] = "column",
17 [ERRFMT_MESSAGE] = "message"
18};
19
20UNITTEST {
21 CHECK_STRING_ARRAY(capture_names);
22}
23
24static 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
30Compiler *find_compiler(const HashMap *compilers, const char *name)
31{
32 return hashmap_get(compilers, name);
33}
34
35bool 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
89static void free_error_format(ErrorFormat *f)
90{
91 regfree(&f->re);
92 free(f);
93}
94
95void free_compiler(Compiler *c)
96{
97 ptr_array_free_cb(&c->error_formats, FREE_FUNC(free_error_format));
98 free(c);
99}
100
101void 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
109void 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
117void 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
12enum {
13 ERRORFMT_CAPTURE_MAX = 16
14};
15
16enum {
17 ERRFMT_FILE,
18 ERRFMT_LINE,
19 ERRFMT_COLUMN,
20 ERRFMT_MESSAGE,
21};
22
23typedef 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
30typedef struct {
31 PointerArray error_formats;
32} Compiler;
33
34Compiler *find_compiler(const HashMap *compilers, const char *name) NONNULL_ARGS;
35void remove_compiler(HashMap *compilers, const char *name) NONNULL_ARGS;
36void free_compiler(Compiler *c) NONNULL_ARGS;
37void collect_errorfmt_capture_names(PointerArray *a, const char *prefix) NONNULL_ARGS;
38void dump_compiler(const Compiler *c, const char *name, String *s) NONNULL_ARGS;
39
40NONNULL_ARGS WARN_UNUSED_RESULT
41bool 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
41extern char **environ;
42
43typedef enum {
44 COLLECT_ALL, // (directories and files)
45 COLLECT_EXECUTABLES, // (directories and executable files)
46 COLLECT_DIRS_ONLY,
47} FileCollectionType;
48
49static bool is_executable(int dir_fd, const char *filename)
50{
51 return faccessat(dir_fd, filename, X_OK, 0) == 0;
52}
53
54static 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
128static 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
164void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix)
165{
166 collect_hashmap_keys(&e->aliases, a, prefix);
167}
168
169static 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
180void 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
185void 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
191void collect_compilers(EditorState *e, PointerArray *a, const char *prefix)
192{
193 collect_hashmap_keys(&e->compilers, a, prefix);
194}
195
196void 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
213static 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
226static 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
268static 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
279static 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
286static 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
297static 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
315static 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
325static 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
333static 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
343static 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
355static 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
373static 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
384static 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
391static 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
405static void complete_save(EditorState *e, const CommandArgs* UNUSED_ARG(a))
406{
407 collect_files(e, &e->cmdline.completion, COLLECT_ALL);
408}
409
410static 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
421static 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
430static 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
442static 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
456static 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
467static 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
477static 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
486static 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
494typedef struct {
495 char cmd_name[12];
496 void (*complete)(EditorState *e, const CommandArgs *a);
497} CompletionHandler;
498
499static 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
525UNITTEST {
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
536static 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
566static 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
606static 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
660out:
661 free_string_array(args_copy);
662}
663
664static 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
683UNITTEST {
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
700static 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
707static 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
800static 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
828void 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
845void 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
862void 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
871void 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
10void complete_command_next(EditorState *e) NONNULL_ARGS;
11void complete_command_prev(EditorState *e) NONNULL_ARGS;
12void reset_completion(CommandLine *cmdline) NONNULL_ARGS;
13
14void collect_env(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
15void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
16void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *keystr_prefix) NONNULL_ARGS;
17void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
18void collect_compilers(EditorState *e, PointerArray *a, const char *prefix) NONNULL_ARGS;
19void 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
16ConfigState current_config;
17
18// Odd number of backslashes at end of line?
19static 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
28UNITTEST {
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
37void 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
66String 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
76const 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
86const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs)
87{
88 *nconfigs = ARRAYLEN(builtin_configs);
89 return &builtin_configs[0];
90}
91
92int 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
131int 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
140void 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
151void 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
162void 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
172void 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
182UNITTEST {
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
11typedef enum {
12 CFG_NOFLAGS = 0,
13 CFG_MUST_EXIST = 1 << 0,
14 CFG_BUILTIN = 1 << 1
15} ConfigFlags;
16
17typedef struct {
18 const char *const name;
19 const StringView text;
20} BuiltinConfig;
21
22typedef struct {
23 const char *file;
24 unsigned int line;
25} ConfigState;
26
27extern ConfigState current_config;
28
29struct EditorState;
30
31String dump_builtin_configs(void);
32const BuiltinConfig *get_builtin_config(const char *name) PURE;
33const BuiltinConfig *get_builtin_configs_array(size_t *nconfigs);
34void exec_config(CommandRunner *runner, StringView config);
35int do_read_config(CommandRunner *runner, const char *filename, ConfigFlags flags) WARN_UNUSED_RESULT;
36int read_config(CommandRunner *runner, const char *filename, ConfigFlags f);
37void exec_builtin_color_reset(struct EditorState *e);
38void exec_builtin_rc(struct EditorState *e);
39void collect_builtin_configs(PointerArray *a, const char *prefix) NONNULL_ARGS;
40void 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
14struct FileEncoder {
15 struct cconv *cconv;
16 unsigned char *nbuf;
17 size_t nsize;
18 bool crlf;
19 int fd;
20};
21
22struct 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
30const char *file_decoder_get_encoding(const FileDecoder *dec)
31{
32 return dec->encoding;
33}
34
35static 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
57static 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
79bool 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
87FileEncoder *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
99void free_file_encoder(FileEncoder *enc)
100{
101 free(enc->nbuf);
102 free(enc);
103}
104
105ssize_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
114size_t file_encoder_get_nr_errors(const FileEncoder* UNUSED_ARG(enc))
115{
116 return 0;
117}
118
119FileDecoder *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
131void free_file_decoder(FileDecoder *dec)
132{
133 free(dec);
134}
135
136bool 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
145static const unsigned char replacement[2] = "\xc2\xbf"; // U+00BF
146
147struct 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
167static 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
176static 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
187static 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
205static 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
221static void resize_obuf(struct cconv *c)
222{
223 c->osize *= 2;
224 xrenew(c->obuf, c->osize);
225}
226
227static 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
237static 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
255static 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
284static 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
319static 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
358static 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
371static 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
382static 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
392static 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
407static 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
415static void cconv_free(struct cconv *c)
416{
417 iconv_close(c->cd);
418 free(c->obuf);
419 free(c);
420}
421
422bool 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
438FileEncoder *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
455void 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!
465ssize_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
482size_t file_encoder_get_nr_errors(const FileEncoder *enc)
483{
484 return enc->cconv ? enc->cconv->errors : 0;
485}
486
487static 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
506static 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
532static 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
547FileDecoder *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
568void free_file_decoder(FileDecoder *dec)
569{
570 if (dec->cconv) {
571 cconv_free(dec->cconv);
572 }
573 free(dec);
574}
575
576bool 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
9typedef struct FileDecoder FileDecoder;
10typedef struct FileEncoder FileEncoder;
11
12bool conversion_supported_by_iconv(const char *from, const char *to) NONNULL_ARGS;
13
14FileDecoder *new_file_decoder(const char *encoding, const unsigned char *buf, size_t size);
15void free_file_decoder(FileDecoder *dec);
16bool file_decoder_read_line(FileDecoder *dec, const char **line, size_t *len) NONNULL_ARGS WARN_UNUSED_RESULT;
17const char *file_decoder_get_encoding(const FileDecoder *dec) NONNULL_ARGS;
18
19FileEncoder *new_file_encoder(const Encoding *encoding, bool crlf, int fd) NONNULL_ARGS;
20void free_file_encoder(FileEncoder *enc) NONNULL_ARGS;
21ssize_t file_encoder_write(FileEncoder *enc, const unsigned char *buf, size_t size) NONNULL_ARGS WARN_UNUSED_RESULT;
22size_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
10void 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
19void 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
27void 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
35void 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
9typedef struct {
10 char *buf;
11 size_t len;
12 bool is_lines;
13} Clipboard;
14
15typedef enum {
16 PASTE_LINES_BELOW_CURSOR,
17 PASTE_LINES_ABOVE_CURSOR,
18 PASTE_LINES_INLINE,
19} PasteLinesType;
20
21void record_copy(Clipboard *clip, char *buf, size_t len, bool is_lines);
22void copy(Clipboard *clip, View *view, size_t len, bool is_lines);
23void cut(Clipboard *clip, View *view, size_t len, bool is_lines);
24void 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
10static 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
47static 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
71bool 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
123bool 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
154void 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
9typedef 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
18NONNULL_ARGS WARN_UNUSED_RESULT
19bool 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
28bool parse_ctags_line(Tag *t, const char *line, size_t line_len) NONNULL_ARG(1);
29void 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
10enum {
11 BLOCK_EDIT_SIZE = 512
12};
13
14static 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
53static 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
65static 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 */
90static 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
206static 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
226void 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
240static bool only_block(const Buffer *buffer, const Block *blk)
241{
242 return blk->node.prev == &buffer->blocks && blk->node.next == &buffer->blocks;
243}
244
245char *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
331char *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
387slow:
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
9void do_insert(View *view, const char *buf, size_t len) NONNULL_ARG(1);
10char *do_delete(View *view, size_t len, bool sanity_check_newlines) NONNULL_ARGS;
11char *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
43static 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
71EditorState *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
183void 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
219static 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
235void 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
248NOINLINE
249void 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
259void 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
274void 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
292int 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
31typedef 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
39typedef enum {
40 INPUT_NORMAL,
41 INPUT_COMMAND,
42 INPUT_SEARCH,
43} InputMode;
44
45typedef struct {
46 const CommandSet *cmds;
47 IntMap key_bindings;
48} ModeHandler;
49
50typedef 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
89static inline void mark_everything_changed(EditorState *e)
90{
91 e->everything_changed = true;
92}
93
94static inline void set_input_mode(EditorState *e, InputMode mode)
95{
96 e->cursor_style_changed = true;
97 e->input_mode = mode;
98}
99
100static 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
113EditorState *init_editor_state(void) RETURNS_NONNULL;
114void free_editor_state(EditorState *e) NONNULL_ARGS;
115void any_key(Terminal *term, unsigned int esc_timeout) NONNULL_ARGS;
116int main_loop(EditorState *e) NONNULL_ARGS WARN_UNUSED_RESULT;
117void ui_start(EditorState *e) NONNULL_ARGS;
118void ui_end(EditorState *e) NONNULL_ARGS;
119void 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
8typedef struct {
9 const char alias[8];
10 EncodingType encoding;
11} EncodingAlias;
12
13static 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
21static 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
41static 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
49UNITTEST {
50 CHECK_BSEARCH_ARRAY(encoding_aliases, alias, ascii_strcmp_icase);
51}
52
53static 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
60EncodingType 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
73static 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
81Encoding 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
101Encoding encoding_from_type(EncodingType type)
102{
103 return (Encoding) {
104 .type = type,
105 .name = encoding_type_to_string(type)
106 };
107}
108
109EncodingType 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
126const 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
8typedef 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
24typedef struct {
25 EncodingType type;
26 // An interned encoding name compatible with iconv_open(3)
27 const char *name;
28} Encoding;
29
30typedef struct {
31 const unsigned char bytes[4];
32 unsigned int len;
33} ByteOrderMark;
34
35static inline bool same_encoding(const Encoding *a, const Encoding *b)
36{
37 return a->type == b->type && a->name == b->name;
38}
39
40Encoding encoding_from_type(EncodingType type);
41Encoding encoding_from_name(const char *name) NONNULL_ARGS;
42EncodingType lookup_encoding(const char *name) NONNULL_ARGS;
43EncodingType detect_encoding_from_bom(const unsigned char *buf, size_t size);
44const 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
11static char error_buf[512];
12static unsigned int nr_errors;
13static bool msg_is_error;
14static bool print_errors_to_stderr;
15
16void clear_error(void)
17{
18 error_buf[0] = '\0';
19}
20
21bool 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
67bool error_msg_errno(const char *prefix)
68{
69 return error_msg("%s: %s", prefix, strerror(errno));
70}
71
72void 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
81const char *get_msg(bool *is_error)
82{
83 *is_error = msg_is_error;
84 return error_buf;
85}
86
87unsigned int get_nr_errors(void)
88{
89 return nr_errors;
90}
91
92void 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
7bool error_msg(const char *format, ...) COLD PRINTF(1);
8bool error_msg_errno(const char *prefix) COLD NONNULL_ARGS;
9void info_msg(const char *format, ...) PRINTF(1);
10void clear_error(void);
11const char *get_msg(bool *is_error) NONNULL_ARGS;
12unsigned int get_nr_errors(void);
13void 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
31enum {
32 IN = 1 << 0,
33 OUT = 1 << 1,
34 ERR = 1 << 2,
35 ALL = IN | OUT | ERR,
36};
37
38static 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
56UNITTEST {
57 CHECK_BSEARCH_ARRAY(exec_map, name, strcmp);
58}
59
60ExecAction 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
67static 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
90static 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
102static 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
133activate:
134 activate_current_message_save(e);
135}
136
137static 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
152static 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
177static 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
189ssize_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
10typedef 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
27ssize_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
35ExecAction 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
15enum {
16 MAX_ENTRIES = 512
17};
18
19void 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
68static 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
78void 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
115void 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
135bool 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
146void 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
8typedef struct FileHistoryEntry {
9 struct FileHistoryEntry *next;
10 struct FileHistoryEntry *prev;
11 char *filename;
12 unsigned long row;
13 unsigned long col;
14} FileHistoryEntry;
15
16typedef struct {
17 char *filename;
18 HashMap entries;
19 FileHistoryEntry *first;
20 FileHistoryEntry *last;
21} FileHistory;
22
23void file_history_add(FileHistory *hist, unsigned long row, unsigned long col, const char *filename);
24void file_history_load(FileHistory *hist, char *filename);
25void file_history_save(const FileHistory *hist);
26bool file_history_find(const FileHistory *hist, const char *filename, unsigned long *row, unsigned long *col) WARN_UNUSED_RESULT;
27void 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
15typedef struct {
16 FileOptionType type;
17 char **strs;
18 union {
19 char *filetype;
20 CachedRegexp *filename;
21 } u;
22} FileOption;
23
24static 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
31void 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
89void 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
114bool 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
142append:
143 opt->type = type;
144 opt->strs = copy_string_array(strs, nstrs);
145 ptr_array_append(file_options, opt);
146 return true;
147}
148
149void 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
178static 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
190void 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
11typedef enum {
12 FOPTS_FILENAME,
13 FOPTS_FILETYPE,
14} FileOptionType;
15
16struct EditorState;
17
18void set_file_options(struct EditorState *e, Buffer *buffer) NONNULL_ARGS;
19void set_editorconfig_options(Buffer *buffer) NONNULL_ARGS;
20void dump_file_options(const PointerArray *file_options, String *buf);
21void free_file_options(PointerArray *file_options);
22
23NONNULL_ARGS WARN_UNUSED_RESULT
24bool 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
14static 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
34UNITTEST {
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
50typedef 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.
57typedef struct {
58 union {
59 FlexArrayStr *str;
60 CachedRegexp *regexp;
61 } u;
62 uint8_t type; // FileDetectionType
63 char name[];
64} UserFileTypeEntry;
65
66static bool ft_uses_regex(FileDetectionType type)
67{
68 return type == FT_CONTENT || type == FT_FILENAME;
69}
70
71bool 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
107static 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
119static 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.
134static 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
168static 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
175static 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
182static 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
190const 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
262bool 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
278void 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
290static 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
295String 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
320static 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
330void 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()
13typedef enum {
14 FT_INTERPRETER,
15 FT_BASENAME,
16 FT_CONTENT,
17 FT_EXTENSION,
18 FT_FILENAME,
19} FileDetectionType;
20
21PURE
22static 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
28bool add_filetype(PointerArray *filetypes, const char *name, const char *str, FileDetectionType type) NONNULL_ARGS WARN_UNUSED_RESULT;
29bool is_ft(const PointerArray *filetypes, const char *name);
30const char *find_ft(const PointerArray *filetypes, const char *filename, StringView line);
31void collect_ft(const PointerArray *filetypes, PointerArray *a, const char *prefix);
32String dump_filetypes(const PointerArray *filetypes);
33void 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
7enum {
8 WINDOW_MIN_WIDTH = 8,
9 WINDOW_MIN_HEIGHT = 3,
10};
11
12static 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
22static 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
46static 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
70static int get_min(const Frame *frame)
71{
72 return frame->parent->vertical ? get_min_h(frame) : get_min_w(frame);
73}
74
75static int get_size(const Frame *frame)
76{
77 return frame->parent->vertical ? frame->h : frame->w;
78}
79
80static int get_container_size(const Frame *frame)
81{
82 return frame->vertical ? frame->h : frame->w;
83}
84
85static 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
93static 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
135static 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
168static 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
181static 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
192static 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
213static 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
236static 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
250static Frame *new_frame(void)
251{
252 Frame *frame = xnew0(Frame, 1);
253 frame->equal_size = true;
254 return frame;
255}
256
257static 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
271Frame *new_root_frame(Window *window)
272{
273 return add_frame(NULL, window, 0);
274}
275
276static 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
294void 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
316void equalize_frame_sizes(Frame *parent)
317{
318 parent->equal_size = true;
319 divide_equally(parent);
320 update_window_coordinates(parent);
321}
322
323void 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
336void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount)
337{
338 resize_frame(frame, dir, get_size(frame) + amount);
339}
340
341static 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
359static 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
368void update_window_coordinates(Frame *frame)
369{
370 update_frame_coordinates(get_root_frame(frame), 0, 0);
371}
372
373Frame *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
397Frame *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
413static 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
426void 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
461void 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
487void 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.
12typedef 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
23typedef enum {
24 RESIZE_DIRECTION_AUTO,
25 RESIZE_DIRECTION_HORIZONTAL,
26 RESIZE_DIRECTION_VERTICAL,
27} ResizeDirection;
28
29struct EditorState;
30
31Frame *new_root_frame(struct Window *window);
32void set_frame_size(Frame *frame, int w, int h);
33void equalize_frame_sizes(Frame *parent);
34void add_to_frame_size(Frame *frame, ResizeDirection dir, int amount);
35void resize_frame(Frame *frame, ResizeDirection dir, int size);
36void update_window_coordinates(Frame *frame);
37Frame *split_frame(struct Window *window, bool vertical, bool before);
38Frame *split_root_frame(struct EditorState *e, bool vertical, bool before);
39void remove_frame(struct EditorState *e, Frame *frame);
40void 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
12void 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
62bool 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
77bool 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
92void 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
118void 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
139void 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
9typedef 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.
20typedef struct {
21 char *filename;
22 HashMap entries;
23 HistoryEntry *first;
24 HistoryEntry *last;
25 size_t max_entries;
26} History;
27
28void history_add(History *history, const char *text);
29bool history_search_forward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT;
30bool history_search_backward(const History *history, const HistoryEntry **pos, const char *text) WARN_UNUSED_RESULT;
31void history_load(History *history, char *filename);
32void history_save(const History *history);
33void 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
6char *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
28static 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
61char *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
70IndentInfo 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
103size_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
119static 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
153size_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
164size_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
14typedef 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.
29static 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
35static 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
41static 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
51char *make_indent(const LocalOptions *options, size_t width);
52char *get_indent_for_next_line(const LocalOptions *options, const StringView *line);
53IndentInfo get_indent_info(const LocalOptions *options, const StringView *line);
54size_t get_indent_width(const LocalOptions *options, const StringView *line);
55size_t get_indent_level_bytes_left(const LocalOptions *options, BlockIter *cursor);
56size_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
25static void add_block(Buffer *buffer, Block *blk)
26{
27 buffer->nl += blk->nl;
28 list_add_before(&blk->node, &buffer->blocks);
29}
30
31static 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);
47copy:
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
55static 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
115static 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
136static 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
150static 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
164static 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
170static bool buffer_fstat(FileInfo *info, int fd)
171{
172 struct stat st;
173 return !fstat(fd, &st) && update_file_info(info, &st);
174}
175
176bool 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
240decode:
241 ret = decode_and_add_blocks(buffer, buf, size, utf8_bom);
242
243error:
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
257bool 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
303error:
304 xclose(fd);
305 return false;
306}
307
308static 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
316static 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
355static 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
398static 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
424bool 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
489error:
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
10bool load_buffer(Buffer *buffer, const char *filename, const GlobalOptions *gopts, bool must_exist) WARN_UNUSED_RESULT;
11bool save_buffer(Buffer *buffer, const char *filename, const Encoding *encoding, bool crlf, bool write_bom, bool hardlinks) WARN_UNUSED_RESULT;
12bool 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:
25static const char *file_locks;
26static const char *file_locks_lock;
27static mode_t file_locks_mode = 0666;
28static pid_t editor_pid;
29
30void 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
53static bool process_exists(pid_t pid)
54{
55 return !kill(pid, 0);
56}
57
58static 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
102static 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
184error:
185 unlink(file_locks_lock);
186 free(buf);
187 if (wfd >= 0) {
188 xclose(wfd);
189 }
190 return false;
191}
192
193bool lock_file(const char *filename)
194{
195 return lock_or_unlock(filename, true);
196}
197
198void 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
8void init_file_locks_context(const char *fallback_dir, pid_t pid);
9bool lock_file(const char *filename) WARN_UNUSED_RESULT;
10void 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
47static 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
55static void cleanup_handler(void *userdata)
56{
57 term_cleanup(userdata);
58}
59
60static 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
69static 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
78static 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
88static 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
105static 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
151static 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
197static 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
234static 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
281static 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
310static 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
319static 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
335int 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
387loop_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
17typedef 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
26static 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
41static 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 */
55void 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
111static 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
133void 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
142void 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
155void 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
175void 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
198static 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
223static 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
235static 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
248static 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
303void 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
366static 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
418void 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
460void 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
496void 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
513void 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
551static 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
578static 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
591static 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
599static 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
635void 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
701void 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
9void select_block(View *view);
10void unselect(View *view);
11void insert_text(View *view, const char *text, size_t size, bool move_after);
12void delete_ch(View *view);
13void erase(View *view);
14void insert_ch(View *view, CodePoint ch);
15void join_lines(View *view);
16void clear_lines(View *view, bool auto_indent);
17void delete_lines(View *view);
18void new_line(View *view, bool above);
19void format_paragraph(View *view, size_t text_width);
20void 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
14static 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
33static 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
52bool 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
9bool 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
9typedef enum {
10 CT_SPACE,
11 CT_NEWLINE,
12 CT_WORD,
13 CT_OTHER,
14} CharTypeEnum;
15
16void 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
74void 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
89void 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
104void move_bol(View *view)
105{
106 block_iter_bol(&view->cursor);
107 view_reset_preferred_x(view);
108}
109
110void 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
139out:
140 view_reset_preferred_x(view);
141}
142
143void move_eol(View *view)
144{
145 block_iter_eol(&view->cursor);
146 view_reset_preferred_x(view);
147}
148
149void 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
161void 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
173void move_bof(View *view)
174{
175 block_iter_bof(&view->cursor);
176 view_reset_preferred_x(view);
177}
178
179void move_eof(View *view)
180{
181 block_iter_eof(&view->cursor);
182 view_reset_preferred_x(view);
183}
184
185void 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
192void 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
209void 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
219static 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
233static 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
244static 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
258static 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
272size_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
294size_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
9typedef 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
14void move_to_preferred_x(View *view, long preferred_x);
15void move_cursor_left(View *view);
16void move_cursor_right(View *view);
17void move_bol(View *view);
18void move_bol_smart(View *view, SmartBolFlags flags);
19void move_eol(View *view);
20void move_up(View *view, long count);
21void move_down(View *view, long count);
22void move_bof(View *view);
23void move_eof(View *view);
24void move_to_line(View *view, size_t line);
25void move_to_column(View *view, size_t column);
26void move_to_filepos(View *view, size_t line, size_t column);
27
28size_t word_fwd(BlockIter *bi, bool skip_non_word);
29size_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
12static void free_message(Message *m)
13{
14 if (m->loc) {
15 file_location_free(m->loc);
16 }
17 free(m);
18}
19
20Message *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
31void add_message(MessageArray *msgs, Message *m)
32{
33 ptr_array_append(&msgs->array, m);
34}
35
36bool 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
62bool 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
80void clear_messages(MessageArray *msgs)
81{
82 msgs->pos = 0;
83 ptr_array_free_cb(&msgs->array, FREE_FUNC(free_message));
84}
85
86String 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
11typedef struct {
12 FileLocation *loc;
13 char msg[];
14} Message;
15
16typedef struct {
17 PointerArray array;
18 size_t pos;
19} MessageArray;
20
21struct EditorState;
22
23Message *new_message(const char *msg, size_t len) RETURNS_NONNULL;
24void add_message(MessageArray *msgs, Message *m) NONNULL_ARGS;
25bool activate_current_message(struct EditorState *e) NONNULL_ARGS;
26bool activate_current_message_save(struct EditorState *e) NONNULL_ARGS;
27void clear_messages(MessageArray *msgs) NONNULL_ARGS;
28String 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
23typedef enum {
24 OPT_STR,
25 OPT_UINT,
26 OPT_ENUM,
27 OPT_BOOL,
28 OPT_FLAG,
29 OPT_REGEX,
30} OptionType;
31
32typedef 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
38typedef 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
44typedef 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
113static 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
121static 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
135static void syntax_changed(EditorState *e, bool global)
136{
137 if (e->buffer && !global) {
138 buffer_update_syntax(e, e->buffer);
139 }
140}
141
142static void overwrite_changed(EditorState *e, bool global)
143{
144 if (!global) {
145 e->cursor_style_changed = true;
146 }
147}
148
149static void redraw_buffer(EditorState *e, bool global)
150{
151 if (e->buffer && !global) {
152 mark_all_lines_changed(e->buffer);
153 }
154}
155
156static void redraw_screen(EditorState *e, bool global)
157{
158 BUG_ON(!global);
159 mark_everything_changed(e);
160}
161
162static 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
175static 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
183static 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
189static 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
195static 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
202static 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
208static 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
214static 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
220static 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
226static 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
238static 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
244static 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
250static 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
256static 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
273static const char *uint_string(const OptionDesc* UNUSED_ARG(desc), OptionValue value)
274{
275 return uint_to_str(value.uint_val);
276}
277
278static 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
284static OptionValue bool_get(const OptionDesc* UNUSED_ARG(d), void *ptr)
285{
286 const bool *valp = ptr;
287 return (OptionValue){.bool_val = *valp};
288}
289
290static void bool_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
291{
292 bool *valp = ptr;
293 *valp = v.bool_val;
294}
295
296static 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
308static const char *bool_string(const OptionDesc* UNUSED_ARG(d), OptionValue v)
309{
310 return v.bool_val ? "true" : "false";
311}
312
313static 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
319static 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
339static const char *enum_string(const OptionDesc *desc, OptionValue value)
340{
341 return desc->u.enum_opt.values[value.uint_val];
342}
343
344static 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
374static 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
403static 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
418static const char *const bool_enum[] = {"false", "true", NULL};
419static const char *const newline_enum[] = {"unix", "dos", NULL};
420static const char *const tristate_enum[] = {"false", "true", "auto", NULL};
421static const char *const save_unmodified_enum[] = {"none", "touch", "full", NULL};
422
423static 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
429static 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
441static 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
476static char *local_ptr(const OptionDesc *desc, LocalOptions *opt)
477{
478 BUG_ON(!desc->local);
479 return (char*)opt + desc->offset;
480}
481
482static char *global_ptr(const OptionDesc *desc, GlobalOptions *opt)
483{
484 BUG_ON(!desc->global);
485 return (char*)opt + desc->offset;
486}
487
488static 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
493static 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
502UNITTEST {
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
565static OptionValue desc_get(const OptionDesc *desc, void *ptr)
566{
567 return option_ops[desc->type].get(desc, ptr);
568}
569
570static 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
578static bool desc_parse(const OptionDesc *desc, const char *str, OptionValue *value)
579{
580 return option_ops[desc->type].parse(desc, str, value);
581}
582
583static const char *desc_string(const OptionDesc *desc, OptionValue value)
584{
585 return option_ops[desc->type].string(desc, value);
586}
587
588static bool desc_equals(const OptionDesc *desc, void *ptr, OptionValue value)
589{
590 return option_ops[desc->type].equals(desc, ptr, value);
591}
592
593static 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
600static const OptionDesc *find_option(const char *name)
601{
602 return BSEARCH(name, option_desc, option_cmp);
603}
604
605static 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
614static 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
624static 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
659bool 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
668bool 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
680static 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
694bool 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
726bool 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
769bool 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
795static 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
831static 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
843void sanity_check_global_options(const GlobalOptions *gopts)
844{
845 sanity_check_options(gopts, true);
846}
847
848void sanity_check_local_options(const LocalOptions *lopts)
849{
850 sanity_check_options(lopts, false);
851}
852#endif
853
854void 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
868void 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
881void 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
896void 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
940static 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
953String 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
979const 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
11enum {
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[]
18typedef 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[]
30typedef 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
52typedef struct {
53 COMMON_OPTIONS;
54} CommonOptions;
55
56// Note: all members should be initialized in buffer_new()
57typedef struct {
58 COMMON_OPTIONS;
59 // Only local
60 bool brace_indent;
61 const char *filetype;
62 const InternedRegexp *indent_regex;
63} LocalOptions;
64
65typedef 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
87static inline bool use_spaces_for_indent(const LocalOptions *opt)
88{
89 return opt->expand_tab || opt->indent_width != opt->tab_width;
90}
91
92struct EditorState;
93
94bool set_option(struct EditorState *e, const char *name, const char *value, bool local, bool global);
95bool set_bool_option(struct EditorState *e, const char *name, bool local, bool global);
96bool toggle_option(struct EditorState *e, const char *name, bool global, bool verbose);
97bool toggle_option_values(struct EditorState *e, const char *name, bool global, bool verbose, char **values, size_t count);
98bool validate_local_options(char **strs);
99void collect_options(PointerArray *a, const char *prefix, bool local, bool global);
100void collect_auto_options(PointerArray *a, const char *prefix);
101void collect_toggleable_options(PointerArray *a, const char *prefix, bool global);
102void collect_option_values(struct EditorState *e, PointerArray *a, const char *option, const char *prefix);
103String dump_options(GlobalOptions *gopts, LocalOptions *lopts);
104const 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
11static HashMap interned_regexps;
12
13bool 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
20bool 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
29void 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
42bool 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
72bool 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
106void free_cached_regexp(CachedRegexp *cr)
107{
108 regfree(&cr->re);
109 free(cr);
110}
111
112const 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
135bool 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()
142static void free_interned_regexp(InternedRegexp *ir)
143{
144 regfree(&ir->re);
145 free(ir);
146}
147
148void 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
9enum {
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
25typedef struct {
26 regex_t re;
27 char str[];
28} CachedRegexp;
29
30typedef 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()
37typedef struct {
38 char start[8];
39 char end[8];
40} RegexpWordBoundaryTokens;
41
42bool regexp_compile_internal(regex_t *re, const char *pattern, int flags) WARN_UNUSED_RESULT;
43
44WARN_UNUSED_RESULT
45static 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
50WARN_UNUSED_RESULT
51static inline bool regexp_compile_basic(regex_t *re, const char *pattern, int flags)
52{
53 return regexp_compile_internal(re, pattern, flags);
54}
55
56WARN_UNUSED_RESULT
57static 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
67void regexp_compile_or_fatal_error(regex_t *re, const char *pattern, int flags);
68bool regexp_init_word_boundary_tokens(RegexpWordBoundaryTokens *rwbt);
69bool regexp_error_msg(const regex_t *re, const char *pattern, int err);
70void free_cached_regexp(CachedRegexp *cr);
71
72const InternedRegexp *regexp_intern(const char *pattern);
73bool regexp_is_interned(const char *pattern);
74void free_interned_regexps(void);
75
76bool 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
16static 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 */
57static 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
153out:
154 free(alloc);
155 return nr;
156}
157
158bool 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
8typedef 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
16bool 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
5static 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
23void 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
31static 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
68void 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
5static 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
34static 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
73char 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
102char 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
4void 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
5static 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
10static 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
17static 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
43static 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
121static 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
146void 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
10typedef 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
26static 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
42static 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
54static 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
62static 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
69static 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
123static 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
166static 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
188static 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
199static 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
247static 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
275static 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
310static 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
354void 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
3static 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
16static 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
22static 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
65static 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
82void 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
89static 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
109void 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
8void 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
24void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c)
25{
26 set_color(term, colors, &colors->builtin[c]);
27}
28
29static 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
64void 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
80void 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
93void 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
112static void clear_update_tabbar(Window *window, void* UNUSED_ARG(data))
113{
114 window->update_tabbar = false;
115}
116
117void 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
130void start_update(Terminal *term)
131{
132 term_begin_sync_update(term);
133 term_hide_cursor(term);
134}
135
136void 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
142void 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
158NOINLINE
159void 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
170void 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
17typedef struct {
18 bool is_modified;
19 unsigned long id;
20 long cy;
21 long vx;
22 long vy;
23} ScreenState;
24
25// screen.c
26void update_screen(EditorState *e, const ScreenState *s);
27void update_term_title(Terminal *term, const Buffer *buffer, bool set_window_title);
28void update_window_sizes(Terminal *term, Frame *frame);
29void update_screen_size(Terminal *term, Frame *root_frame);
30void set_color(Terminal *term, const ColorScheme *colors, const TermColor *color);
31void set_builtin_color(Terminal *term, const ColorScheme *colors, BuiltinColorEnum c);
32void mask_color(TermColor *color, const TermColor *over);
33void start_update(Terminal *term);
34void end_update(EditorState *e);
35void normal_update(EditorState *e);
36void restore_cursor(EditorState *e);
37
38// screen-cmdline.c
39void update_command_line(EditorState *e);
40void show_message(Terminal *term, const ColorScheme *colors, const char *msg, bool is_error);
41
42// screen-tabbar.c
43void print_tabbar(Terminal *term, const ColorScheme *colors, Window *window);
44
45// screen-status.c
46void update_status_line(const Window *window);
47
48// screen-view.c
49void update_range(EditorState *e, const View *view, long y1, long y2);
50
51// screen-window.c
52void update_all_windows(EditorState *e);
53void update_buffer_windows(EditorState *e, const Buffer *buffer);
54
55// screen-prompt.c
56char status_prompt(EditorState *e, const char *question, const char *choices) NONNULL_ARGS;
57char 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
10static 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
54static 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
109bool search_tag(View *view, const char *pattern)
110{
111 regex_t regex;
112 if (!regexp_compile_basic(&regex, pattern, REG_NEWLINE)) {
113 return false;
114 }
115
116 BlockIter bi = block_iter(view->buffer);
117 bool found = do_search_fwd(view, &regex, &bi, false);
118 regfree(&regex);
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
130static void free_regex(SearchState *search)
131{
132 if (search->re_flags) {
133 regfree(&search->regex);
134 search->re_flags = 0;
135 }
136}
137
138static 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
148static 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
181void search_free_regexp(SearchState *search)
182{
183 free_regex(search);
184 free(search->pattern);
185}
186
187void search_set_regexp(SearchState *search, const char *pattern)
188{
189 search_free_regexp(search);
190 search->pattern = xstrdup(pattern);
191}
192
193static 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
228bool 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
236bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs)
237{
238 return do_search_next(view, search, cs, false);
239}
240
241bool 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
9typedef enum {
10 CSS_FALSE,
11 CSS_TRUE,
12 CSS_AUTO,
13} SearchCaseSensitivity;
14
15typedef 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
22static inline void toggle_search_direction(SearchState *search)
23{
24 search->reverse ^= 1;
25}
26
27bool search_tag(View *view, const char *pattern) NONNULL_ARGS WARN_UNUSED_RESULT;
28void search_set_regexp(SearchState *search, const char *pattern) NONNULL_ARGS;
29void search_free_regexp(SearchState *search) NONNULL_ARGS;
30bool search_prev(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT;
31bool search_next(View *view, SearchState *search, SearchCaseSensitivity cs) NONNULL_ARGS WARN_UNUSED_RESULT;
32bool 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
5static 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
25void 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
60size_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
68char *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
82size_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
98size_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
9typedef struct {
10 BlockIter si;
11 size_t so;
12 size_t eo;
13 bool swapped;
14} SelectionInfo;
15
16void init_selection(const View *view, SelectionInfo *info);
17size_t prepare_selection(View *view);
18char *view_get_selection(View *view, size_t *size);
19size_t get_nr_selected_lines(const SelectionInfo *info);
20size_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
16static 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
24static 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
58static 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
97static 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
109void 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
145out:
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
6void 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
39extern char **environ;
40
41typedef 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
47typedef 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
55static 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
84static 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
106static 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
139static 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
161static 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
183static 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
201static 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
211static 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
233static 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
250static 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
270static 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
287static void collect_all_options(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
288{
289 collect_options(a, prefix, false, false);
290}
291
292static void do_collect_cursor_modes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
293{
294 collect_cursor_modes(a, prefix);
295}
296
297static void do_collect_builtin_configs(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
298{
299 collect_builtin_configs(a, prefix);
300}
301
302static void do_collect_builtin_includes(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix)
303{
304 collect_builtin_includes(a, prefix);
305}
306
307static 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
327static 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
341String dump_command_history(EditorState *e)
342{
343 return do_history_dump(&e->command_history);
344}
345
346String dump_search_history(EditorState *e)
347{
348 return do_history_dump(&e->search_history);
349}
350
351typedef struct {
352 const char *name;
353 const char *value;
354} CommandAlias;
355
356static 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
363String 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
402String 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
421String dump_frames(EditorState *e)
422{
423 String str = string_new(4096);
424 dump_frame(e->root_frame, 0, &str);
425 return str;
426}
427
428String 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
440String 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
457String do_dump_options(EditorState *e)
458{
459 return dump_options(&e->options, &e->buffer->options);
460}
461
462// Dump option values and FileOption entries
463String 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
471String do_dump_builtin_configs(EditorState* UNUSED_ARG(e))
472{
473 return dump_builtin_configs();
474}
475
476String do_dump_hl_colors(EditorState *e)
477{
478 return dump_hl_colors(&e->colors);
479}
480
481String do_dump_filetypes(EditorState *e)
482{
483 return dump_filetypes(&e->filetypes);
484}
485
486static String do_dump_messages(EditorState *e)
487{
488 return dump_messages(&e->messages);
489}
490
491static String do_dump_macro(EditorState *e)
492{
493 return dump_macro(&e->macro);
494}
495
496static String do_dump_buffer(EditorState *e)
497{
498 return dump_buffer(e->buffer);
499}
500
501static 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
523UNITTEST {
524 CHECK_BSEARCH_ARRAY(show_handlers, name, strcmp);
525}
526
527bool 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
547void collect_show_subcommands(PointerArray *a, const char *prefix)
548{
549 COLLECT_STRING_FIELDS(show_handlers, name, a, prefix);
550}
551
552void 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
10bool show(EditorState *e, const char *type, const char *key, bool cflag) NONNULL_ARG(1, 2) WARN_UNUSED_RESULT;
11void collect_show_subcommands(PointerArray *a, const char *prefix) NONNULL_ARGS;
12void collect_show_subcommand_args(EditorState *e, PointerArray *a, const char *name, const char *arg_prefix) NONNULL_ARGS;
13
14String dump_all_bindings(EditorState *e);
15String dump_command_history(EditorState *e);
16String dump_compilers(EditorState *e);
17String dump_cursors(EditorState *e);
18String dump_frames(EditorState *e);
19String dump_normal_aliases(EditorState *e);
20String dump_options_and_fileopts(EditorState *e);
21String dump_search_history(EditorState *e);
22String do_dump_builtin_configs(EditorState *e);
23String do_dump_filetypes(EditorState *e);
24String do_dump_hl_colors(EditorState *e);
25String 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
11volatile sig_atomic_t resized = 0;
12
13static 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
23static 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
32static 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
52void handle_sigwinch(int UNUSED_ARG(signum))
53{
54 resized = 1;
55}
56
57static 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
90static 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
109static 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 */
131void 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
6extern volatile sig_atomic_t resized;
7
8void set_signal_handlers(void);
9void 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
21static 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
82static 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
98static 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
189static 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
198static 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
213static 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
223static 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
235static void exec_error(const char *argv0)
236{
237 error_msg("Unable to exec '%s': %s", argv0, strerror(errno));
238}
239
240bool 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
299static int safe_xclose(int fd)
300{
301 return (fd > STDERR_FILENO) ? xclose(fd) : 0;
302}
303
304static 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
312UNITTEST {
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
322int 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
390error_resume:
391 resume_terminal(ctx->editor, quiet, false);
392error_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
12typedef 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
18typedef enum {
19 SPAWN_NULL,
20 SPAWN_TTY,
21 SPAWN_PIPE,
22} SpawnAction;
23
24typedef 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
34int spawn(SpawnContext *ctx) NONNULL_ARGS WARN_UNUSED_RESULT;
35bool 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
13typedef 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
23typedef 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
46static 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
74static void add_ch(Formatter *f, char ch)
75{
76 f->buf[f->pos++] = ch;
77}
78
79static void add_separator(Formatter *f)
80{
81 while (f->separator && f->pos < f->size) {
82 add_ch(f, ' ');
83 f->separator--;
84 }
85}
86
87static 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
100static 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
115PRINTF(2)
116static 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
126static 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
133static 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
159static 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
191void 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.
326size_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
9size_t statusline_format_find_error(const char *str);
10
11void 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
15static const char *current_filename; // For sorting tags
16
17static 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
65static 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
91static 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
100static 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
123static 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
132void tag_file_free(TagFile *tf)
133{
134 free(tf->filename);
135 free(tf->buf);
136 *tf = (TagFile){.filename = NULL};
137}
138
139static 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
197static void free_tags_cb(Tag *t)
198{
199 free_tag(t);
200 free(t);
201}
202
203static 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
209static 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
224static 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
250void 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
277size_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
305void 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
11typedef struct {
12 char *filename;
13 char *buf;
14 size_t size;
15 time_t mtime;
16} TagFile;
17
18void add_message_for_tag(MessageArray *messages, Tag *tag, const StringView *dir) NONNULL_ARGS;
19size_t tag_lookup(TagFile *tf, const StringView *name, const char *filename, MessageArray *messages) NONNULL_ARG(1, 2, 4);
20void collect_tags(TagFile *tf, PointerArray *a, const StringView *prefix) NONNULL_ARGS;
21void 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
15typedef struct {
16 char name[12];
17 char *(*expand)(const EditorState *e);
18} BuiltinVar;
19
20static char *expand_dte_home(const EditorState *e)
21{
22 return xstrdup(e->user_config_dir);
23}
24
25static 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
33static 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
41static 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
52static char *expand_filetype(const EditorState *e)
53{
54 return e->buffer ? xstrdup(e->buffer->options.filetype) : NULL;
55}
56
57static char *expand_colno(const EditorState *e)
58{
59 return e->view ? xstrdup(umax_to_str(e->view->cx_display + 1)) : NULL;
60}
61
62static char *expand_lineno(const EditorState *e)
63{
64 return e->view ? xstrdup(umax_to_str(e->view->cy + 1)) : NULL;
65}
66
67static 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
85static 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
96UNITTEST {
97 CHECK_BSEARCH_ARRAY(normal_vars, name, strcmp);
98}
99
100bool 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
110void 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
8bool expand_normal_var(const char *name, char **value, const void *userdata) NONNULL_ARGS;
9void 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
10void 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
26void 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
56static 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
61static 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
79static 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
92static 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
109void 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
121long 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
130bool view_can_close(const View *view)
131{
132 const Buffer *buffer = view->buffer;
133 return !buffer_modified(buffer) || buffer->views.count > 1;
134}
135
136StringView 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
174StringView 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
11typedef 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.
19typedef 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
49static inline void view_reset_preferred_x(View *view)
50{
51 view->preferred_x = -1;
52}
53
54void view_update_cursor_y(View *view) NONNULL_ARGS;
55void view_update_cursor_x(View *view) NONNULL_ARGS;
56void view_update(View *view, unsigned int scroll_margin) NONNULL_ARGS;
57long view_get_preferred_x(View *view) NONNULL_ARGS;
58bool view_can_close(const View *view) NONNULL_ARGS;
59StringView view_do_get_word_under_cursor(const View *view, size_t *offset_in_line) NONNULL_ARGS;
60StringView 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
16Window *new_window(EditorState *e)
17{
18 Window *window = xnew0(Window, 1);
19 window->editor = e;
20 return window;
21}
22
23View *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
45View *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
51View *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
141View *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
152View *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
164View *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
179static 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
188void 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
197size_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
228void 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
245static 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
253void 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
295View *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
304static 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).
313static 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
318View *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
335View *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
356void 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
364static 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
373static int edit_x_offset(const Window *window, const GlobalOptions *options)
374{
375 return line_numbers_width(window, options);
376}
377
378static int edit_y_offset(const GlobalOptions *options)
379{
380 return options->tab_bar ? 1 : 0;
381}
382
383static 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
393void 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
406void 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
415void 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
422int 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
429void 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
440typedef 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
449static 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
469Window *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
477Window *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
485void 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
13enum {
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).
23typedef 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
42struct EditorState;
43
44Window *new_window(struct EditorState *e) NONNULL_ARGS_AND_RETURN;
45View *window_add_buffer(Window *window, Buffer *buffer);
46View *window_open_empty_buffer(Window *window);
47View *window_open_buffer(Window *window, const char *filename, bool must_exist, const Encoding *encoding);
48View *window_get_view(Window *window, Buffer *buffer);
49View *window_find_view(Window *window, Buffer *buffer);
50View *window_find_unclosable_view(Window *window);
51void window_free(Window *window);
52size_t remove_view(View *view);
53void window_close(Window *window);
54void window_close_current_view(Window *window);
55void set_view(View *view);
56View *window_open_new_file(Window *window);
57View *window_open_file(Window *window, const char *filename, const Encoding *encoding);
58View *window_open_files(Window *window, char **filenames, const Encoding *encoding);
59void mark_buffer_tabbars_changed(Buffer *buffer);
60void calculate_line_numbers(Window *window);
61void set_window_coordinates(Window *window, int x, int y);
62void set_window_size(Window *window, int w, int h);
63int window_get_scroll_margin(const Window *window, unsigned int scroll_margin);
64void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data);
65Window *prev_window(Window *window);
66Window *next_window(Window *window);
67
68#endif