#define _POSIX_C_SOURCE 202511L #include #include #include #include #include #include #include #include #include #define ARENA_IMPL #define UTF8_IMPL #define STR_IMPL #include "wrmr.h" #include "arena.h" #include "txt.h" #include "vui.h" #include "utf8.h" #include "str.h" #include "freelist.h" #include "regex.h" #define ED_BUF_MAX 1024 typedef enum { ED_BUF_SCRATCH, ED_BUF_FILE } EditBufType; typedef struct { EditBufType type; Arena arena; Str path; Txt *txt; TxtLoc cur; } EditBuf; typedef enum { MODE_NORMAL, MODE_INSERT, MODE_REPLACE_ONE, MODE_REPLACE_MULT } EditMode; static const char *mode_str[] = { "N", "I", "R", "R", }; typedef struct { Arena scratch; EditBuf buf[ED_BUF_MAX]; u32 bufn, bufi; Txt *txt_free; EditMode mode; u32 count; Str input_line, input_prompt; Str search; int search_dir; u32 search_char; int search_char_dir; int search_char_incl; } Editor; Editor e = { 0 }; int ed_buf_open(Editor *e, const char *path) { if (e->bufn == ED_BUF_MAX) return -1; EditBuf b = { 0 }; b.arena = arena_init(1L << 30); b.txt = FREELIST_NEW(&e->txt_free, &b.arena); if (path) { b.path = str_dup(str_from_cstr(path), &b.arena); b.type = ED_BUF_FILE; txt_load(b.txt, path); } else { b.path = S("*scratch*"); b.type = ED_BUF_SCRATCH; txt_load_empty(b.txt); } b.cur = txt_end(b.txt); e->buf[e->bufn] = b; return e->bufn++; } void ed_buf_free(EditBuf *b) { txt_free(b->txt); arena_free(&b->arena); } void ed_init(Editor *e) { memset(e, 0, sizeof(Editor)); e->scratch = arena_init(1L << 30); e->bufi = ed_buf_open(e, NULL); Str s = S("(Scratch buffer, type whatever)"); e->buf[e->bufi].cur = txt_insert(e->buf[e->bufi].cur, s.s, s.n); } void ed_fini(Editor *e) { for (u32 i = 0; i < e->bufn; i++) { ed_buf_free(&e->buf[i]); } if (e->search.s) free(e->search.s); } u32 ed_buf_close(Editor *e, u32 i) { ed_buf_free(&e->buf[i]); if (i + 1 < e->bufn) { MOVE(&e->buf[i], &e->buf[i+1], e->bufn - (i + 1)); } e->bufn--; return i > 0 ? i - 1 : 0; } int ed_buf_save(Editor *e, u32 i) { EditBuf *b = &e->buf[i]; if (b->type == ED_BUF_FILE) { return txt_save(b->txt, str_to_cstr(b->path, &e->scratch)); } else { return 0; } } static inline int is_space(u32 c) { return c <= 0x20 && c != 0; } TxtLoc next_word(TxtLoc l) { while (!at_end(l) && is_space(txt_chr(l))) l = cnext(l); while (!at_end(l) && !is_space(txt_chr(l))) l = cnext(l); return l; } TxtLoc prev_word(TxtLoc l) { while (!at_start(l)) { TxtLoc n = bprev(l); if (!is_space(txt_chr(n))) break; l = n; } while (!at_start(l)) { TxtLoc n = bprev(l); if (is_space(txt_chr(n))) break; l = n; } return l; } static inline u32 bracket_opp(u32 c) { switch (c) { case '{': return '}'; case '}': return '{'; case '[': return ']'; case ']': return '['; case '(': return ')'; case ')': return '('; case '<': return '>'; case '>': return '<'; default: return c; } } static inline int bracket_dir(u32 c) { switch (c) { case '{': case '[': case '(': case '<': return 1; case '}': case ']': case ')': case '>': return -1; default: return 0; } } int match_bracket(TxtLoc l, TxtLoc *out) { u32 depth = 1; u32 c = txt_chr(l); u32 o = bracket_opp(c); int dir = bracket_dir(c); if (dir < 0) { while (!at_start(l) && depth > 0) { l = cprev(l); u32 x = txt_chr(l); if (x == c) depth++; else if (x == o) depth--; } } else if (dir > 0) { while (!at_end(l) && depth > 0) { l = cnext(l); u32 x = txt_chr(l); if (x == c) depth++; else if (x == o) depth--; } } if (depth == 0) { *out = l; return 1; } else { return 0; } } TxtLoc next_func_end(TxtLoc l) { l = start_of_line(l); while (!at_end(l)) { l = next_line_start(l); if (txt_chr(l) == '}' && txt_chr(cnext(l)) == '\n') break; } return l; } TxtLoc prev_func_end(TxtLoc l) { l = start_of_line(l); while (!at_start(l)) { l = prev_line_start(l); if (txt_chr(l) == '}' && txt_chr(cnext(l)) == '\n') break; } return l; } TxtLoc prev_func(TxtLoc l) { match_bracket(prev_func_end(l), &l); return l; } TxtLoc next_func(TxtLoc l) { match_bracket(l, &l); match_bracket(next_func_end(l), &l); return l; } int empty_line(TxtLoc l) { u8 b = txt_byte(start_of_line(l)); return b == '\n' || b == 0 /* last line of buffer */; } TxtLoc next_par(TxtLoc l) { while (!at_end(l) && empty_line(l)) l = next_newline(l); while (!at_end(l) && !empty_line(l)) l = next_newline(l); return l; } TxtLoc prev_par(TxtLoc l) { while (!at_start(l) && empty_line(l)) l = prev_newline(l); while (!at_start(l) && !empty_line(l)) l = prev_newline(l); return l; } TxtLoc txt_at_line(Txt *t, u32 line) { TxtLoc l = txt_start(t); while (line-- > 1) l = next_line(l); return l; } typedef struct { pid_t pid; int from, to; } ChildProcess; int popen2(const char *cmd, int *in, int *out) { int rd[2], wr[2]; if(pipe(rd)) return -1; if(pipe(wr)) return -1; pid_t p = fork(); if(p < 0) return -1; if(p == 0) { /* child */ close(rd[1]); close(wr[0]); dup2(rd[0], STDIN_FILENO); dup2(wr[1], STDOUT_FILENO); close(rd[0]); close(wr[1]); if (execl("/bin/sh", "sh", "-c", cmd, 0)) { err(1, "execl"); } } close(rd[0]); close(wr[1]); *in = rd[1]; *out = wr[0]; return p; } int shell_replace(TxtLoc start, TxtLoc end, const char *cmd) { if (txt_before(end, start)) { TxtLoc t = start; start = end; end = t; } int in, out; int p = popen2(cmd, &in, &out); if (p < 0) return -1; struct pollfd pfds[2] = { { .fd = in, .events = POLLOUT }, { .fd = out, .events = POLLIN } }; Str wrs = txt_collect_range(start, end, &e.scratch); DYNARR(char) rds = { 0 }; u32 wr_i = 0; while ((pfds[0].fd >= 0 || pfds[1].fd >= 0) && poll(pfds, 2, 200) >= 0) { const u32 chunksz = 8L << 10; if (pfds[0].revents & POLLOUT) { u32 wr_n = wrs.n - wr_i; if (wr_n > chunksz) wr_n = chunksz; isize r = write(in, wrs.s + wr_i, wr_n); if (r > 0) { wr_i += wr_n; } else { close(in); pfds[0].fd = -1; } } else if (pfds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) { close(in); pfds[0].fd = -1; } if (pfds[1].revents & POLLIN) { DA_AFIT(&rds, &e.scratch, rds.n + (-rds.n & (chunksz-1)) + chunksz); isize r = read(out, &rds.v[rds.n], chunksz); if (r > 0) { rds.n += r; } else { close(out); pfds[1].fd = -1; } } else if (pfds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) { close(out); pfds[1].fd = -1; } } /* TODO: wrap this logic in a general txt_replace() function, * and make it so it will avoid deleting as much as possible; * reflowing text often only changes lines towards the end, * for example. maybe do something like skip as many pieces * as match 100%, then iteratively halve the length of the * final one until it matches or gets too small. */ if (!txt_range_equal(start, end, (Str){rds.v,rds.n})) { EditBuf *eb = &e.buf[e.bufi]; u32 b = txt_ofs(eb->cur); start = txt_delete_range(start, end); end = txt_insert(start, rds.v, rds.n); eb->cur = txt_at(start.t, b); } return 0; } #define ANY_KEY_PROMPT "[Press any key to resume editing]" static void press_any_key(void) { printf("%s", ANY_KEY_PROMPT); fflush(stdout); vui_enable(); vui_key(); vui_redraw(); } int shell_run(const char *cmd) { vui_disable(); int r = system(cmd); press_any_key(); return r; } /* main */ #define ODD_ATTR (FG_CYAN | BG_BLACK) #define EVEN_ATTR (FG_WHITE | BG_BLACK) void find_view_window(TxtLoc l, TxtLoc *start, TxtLoc *end, u32 lines) { u32 u = lines / 2; TxtLoc a = l; for (u32 i = 0; i < u; i++) a = prev_newline(a); u32 n = 0; TxtLoc b = a; while (!at_end(b) && n++ < lines) b = next_newline(b); while (!at_start(a) && n++ < lines) a = prev_newline(a); if (!at_start(a) && txt_byte(a) == '\n') a = bnext(a); *start = a; *end = b; } void draw(void *ctx) { (void)ctx; EditBuf *eb = &e.buf[e.bufi]; vui_clear(); int lmarg = 0; int x = lmarg, y = 0; TxtLoc start, end; find_view_window(eb->cur, &start, &end, LINES - 1); VuiAttr norm = FG_CYAN | BG_BLACK; VuiAttr sel = FG_BLACK | BG_CYAN; VuiAttr txt = FG_WHITE | BG_BLACK; vui_fill(' ', txt); { int x = 0; int y = LINES-1; x += vui_aprintf(x, y, norm, " %s ", mode_str[e.mode]); for (u32 i = 0; i < e.bufn; i++) { EditBuf *b = &e.buf[i]; VuiAttr a = i == e.bufi ? sel : norm; x += vui_aprintf(x, y, a, " %.*s", (int)b->path.n, b->path.s); if (b->type == ED_BUF_FILE && b->txt->dirty) x += vui_putsa(x, y, "* ", a); else vui_chra(x++, y, ' ', a); } int n = COLS; while (x < n) vui_chra(x++, y, ' ', norm); } TxtLoc l = eb->cur; int cur_found = 0; while (txt_before(start, end)) { if (l.p == start.p && l.i == start.i) { cur_found = 1; if (txt_chr(start) == '\t') { vui_curs_pos(x + (-(x+1) & 7), y); } else { vui_curs_pos(x, y); } } u32 c = txt_chr_next(&start); if (c == '\n') { x = lmarg; y++; } else if (is_space(c)) { VuiAttr a = txt_chr(start) == '\n' ? norm : txt; if (c == '\t') { u32 n = 1 + (-(x+1) & 7); while (n--) vui_chra(x++, y, ' ', a); } else { vui_chra(x++, y, ' ', a); } } else if (c) { vui_chra(x++, y, c, txt); } } ASSERT(start.i <= eb->txt->ptbl.v[start.p].n); if (!cur_found) vui_curs_pos(x, y); if (e.input_line.n > 0 || e.input_prompt.n > 0) { u32 x = 0; x += vui_putsn(0, 0, e.input_prompt.s, e.input_prompt.n); x += vui_putsn(x, 0, e.input_line.s, e.input_line.n); vui_curs_pos(x, 0); vui_curs_shape(VUI_CURS_BAR); u32 c = COLS; while (x < c) vui_chr(x++, 0, ' '); } } TxtLoc logical_line_start(TxtLoc l) { l = start_of_line(l); while (txt_chr(l) == '\t') l = cnext(l); return l; } Str get_input_line(Str prompt) { e.input_prompt = prompt; DYNARR(char) s = { 0 }; for (;;) { draw(NULL); vui_blit(); u32 c = vui_key(); switch (c) { case '\r': goto done; case KEY_ESC: e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return (Str) { 0, 0 }; case KEY_BKSP: if (s.n > 0) s.n--; break; case 0x17 /* ^W */: while (s.n > 0 && is_space(s.v[s.n-1])) s.n--; while (s.n > 0 && !is_space(s.v[s.n-1])) s.n--; break; default: if (c > 0 && c <= KEY_UTF8_MAX && c >= ' ') { u32 n = utf8_encode_len(&c, 1); DA_AFIT(&s, &e.scratch, s.n + n); utf8_encode(&s.v[s.n], &c, n); s.n += n; } break; } e.input_line = (Str) { s.v, s.n }; } done:; e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return (Str) { s.v, s.n }; } int search_next_regex(TxtLoc l, Str src, TxtLoc *out) { RegEx re = { 0 }; ReSearch s = { 0 }; if (re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS)) { /* TODO: report parse error */ return 0; } TxtLoc t = l; int match_found = 0; search_from_start: re_search_start(&s, &re, &e.scratch); while (!at_end(t)) { TxtLoc p = t; Str chnk = txt_next_chunk(&t); re_search_chunk(&s, chnk.s, chnk.n); if (at_end(t)) re_search_last_chunk(&s); ReMatch m; if (re_search_match(&s, &m)) { *out = (TxtLoc) { p.t, p.p, p.i + m.extent.start }; return 1; } } if (match_found == 0) { match_found = -1; t = txt_start(t.t); goto search_from_start; } return 0; } int search_prev_regex(TxtLoc start, Str src, TxtLoc *out) { RegEx re = { 0 }; ReSearch s = { 0 }; if (re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS)) { /* TODO: report parse error */ return 0; } int match_found = 0; TxtLoc end = txt_end(start.t); TxtLoc l = start; search_from_end: for (;;) { ReMatch m; Arena a = e.scratch; re_search_start(&s, &re, &e.scratch); TxtLoc t = l; while (!at_end(t)) { TxtLoc p = t; Str chnk = txt_next_chunk(&t); re_search_chunk(&s, chnk.s, chnk.n); if (at_end(t)) re_search_last_chunk(&s); if (re_search_match_at_start(&s, &m) && m.extent.start == 0) { *out = (TxtLoc) { p.t, p.p, p.i + m.extent.start }; return 1; } if (~s.flags & RE_SEARCH_MID_MATCH) { if (chnk.n > 0) break; if (txt_after(t, end)) break; } } e.scratch = a; if (at_start(l)) break; l = cprev(l); } if (match_found == 0) { match_found = -1; l = txt_end(l.t); end = start; goto search_from_end; } return 0; } int read_search(void) { Str src = get_input_line(S("Search: ")); if (!src.n) return 0; e.search.s = malloc(src.n); if (!e.search.s) FAIL_WITH_MSG("failed to allocate search"); memcpy(e.search.s, src.s, src.n); e.search.n = src.n; return 1; } int motion(TxtLoc *lp, u32 c) { TxtLoc l = *lp; TxtLoc last_loc = l; for (;;) { switch (c) { case '0': if (!e.count) goto loop; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': e.count = (e.count % 100000000) * 10 + c - '0'; break; case KEY_ESC: e.count = 0; return 0; default: goto loop; } draw(NULL); vui_blit(); c = vui_key(); } loop: switch (c) { case KEY_LEFT: case 'h': l = cprev(l); break; case KEY_RIGHT: case 'l': l = cnext(l); break; case KEY_DOWN: case 'j': l = next_line(l); break; case KEY_UP: case 'k': l = prev_line(l); break; case KEY_LEFT | KEY_CTRL_BIT: case 'b': l = prev_word(l); break; case KEY_RIGHT | KEY_CTRL_BIT: case 'w': l = next_word(l); break; case KEY_UP | KEY_CTRL_BIT: case '{': l = prev_par(l); break; case KEY_DOWN | KEY_CTRL_BIT: case '}': l = next_par(l); break; case '[': l = prev_func(l); break; case ']': l = next_func(l); break; case '%': if (!match_bracket(l, &l)) return 0; break; case KEY_PGUP: for (u32 i = 0; i < LINES; i += 3) l = prev_line(l); break; case KEY_PGDN: for (u32 i = 0; i < LINES; i += 3) l = next_line(l); break; case KEY_HOME: case '^': l = logical_line_start(l); break; case '0': l = start_of_line(l); break; case KEY_END: case '$': l = end_of_line(l); break; case KEY_HOME | KEY_CTRL_BIT: case 'g': if (e.count) { l = txt_at_line(l.t, e.count); e.count = 0; } else { l = txt_start(l.t); } break; case KEY_END | KEY_CTRL_BIT: case 'G': if (e.count) { l = txt_at_line(l.t, e.count); e.count = 0; } else { l = txt_end(l.t); } break; case 'f': e.search_char_incl = 1; e.search_char_dir = 1; e.search_char = vui_key(); goto repeat_char_search; case 'F': e.search_char_incl = 1; e.search_char_dir = -1; e.search_char = vui_key(); goto repeat_char_search; case 't': e.search_char_incl = 0; e.search_char_dir = 1; e.search_char = vui_key(); goto repeat_char_search; case 'T': e.search_char_incl = 0; e.search_char_dir = -1; e.search_char = vui_key(); goto repeat_char_search; case ';': { repeat_char_search:; int found = 0; TxtLoc t = l; if (!e.search_char_incl) { if (e.search_char_dir < 0) { if (at_start(t)) break; t = cprev(t); } else { if (at_end(t)) break; t = cnext(t); } } for (;;) { TxtLoc p = t; if (e.search_char_dir < 0) { if (at_start(t)) break; t = cprev(t); } else { if (at_end(t)) break; t = cnext(t); } u32 x = txt_chr(t); if (x == '\n' || x == 0) break; if (x == e.search_char) { found = 1; if (e.search_char_incl) l = t; else l = p; break; } } if (!found) return 0; } break; case '/': if (read_search()) { TxtLoc r; if (search_next_regex(l, e.search, &r)) l = r; e.search_dir = 1; } break; case '?': if (read_search()) { TxtLoc r; if (search_prev_regex(l, e.search, &r)) l = r; e.search_dir = -1; } break; case 'n': if (!e.search.n) return 0; if (e.search_dir > 0 && !search_next_regex(cnext(l), e.search, &l)) return 0; if (e.search_dir < 0 && !search_prev_regex(cprev(l), e.search, &l)) return 0; break; case 'N': if (!e.search.n) return 0; if (e.search_dir < 0 && !search_next_regex(cnext(l), e.search, &l)) return 0; if (e.search_dir > 0 && !search_prev_regex(cprev(l), e.search, &l)) return 0; break; default: return 0; } if (e.count > 1 && (txt_before(l, last_loc) || txt_after(l, last_loc))) { e.count--; goto loop; } e.count = 0; *lp = l; return 1; } TxtLoc ins_newline(TxtLoc l) { u32 tabs = 0; TxtLoc start = start_of_line(l); for (TxtLoc t = start; txt_byte(t) == '\t'; t = bnext(t)) tabs++; while (txt_byte(cprev(l)) == '\t') l = txt_delete(l, 1); l = txt_insert_c(l, '\n'); while (tabs--) l = txt_insert_c(l, '\t'); return l; } static Str normalize_path(Str s) { char pwd[8192]; if (s.n > 0 && s.s[0] != '/') { if (!getcwd(pwd, sizeof(pwd))) { return (Str) { 0, 0 }; } Str d = str_from_cstr(pwd); str_catc(&d, '/', &e.scratch); str_cat(&d, s, &e.scratch); s = d; } DYNARR(Str) bits = { 0 }; while (s.n > 0) { Cut c = str_cut(s, '/'); if (str_eql(c.head, S(".."))) { if (bits.n > 0) bits.n--; } else if (str_eql(c.head, S("."))) { /* do nothing */ } else if (c.head.n > 0) { DA_APUSH(&bits, &e.scratch, c.head); } s = c.tail; } Str path = { 0 }; for (u32 i = 0; i < bits.n; i++) { str_catc(&path, '/', &e.scratch); str_cat(&path, bits.v[i], &e.scratch); } return path; } static void build_file(Str path, int debugp) { (void)path; shell_run(debugp ? "make DEBUG=1" : "make"); } #define WED_DEV_DIR "/mnt/user/wrmr/src/wed/" #define WED_DEV_EXE WED_DEV_DIR "wed" int is_wed_path(Str path) { return str_starts(normalize_path(path), S(WED_DEV_DIR)); } void str_cat_u32(Str *out, u32 c, Arena *a) { char buf[32]; u32 n = 0; do { buf[32 - ++n] = (c % 10) + '0'; c /= 10; } while (c); str_cat(out, (Str) { &buf[sizeof(buf) - n], n }, a); } int str_to_u32(Str s, u32 *out) { u32 x = 0; for (u32 i = 0; i < s.n; i++) { char c = s.s[i]; if (!(c >= '0' && c <= '9')) return -1; x = (x * 10) + c - '0'; } *out = x; return 0; } void make_edit_args(Str *a) { for (u32 i = 0; i < e.bufn; i++) { if (e.buf[i].type != ED_BUF_FILE) continue; if (i > 0) str_catc(a, ' ', &e.scratch); str_cat(a, e.buf[i].path, &e.scratch); str_cat(a, S("@"), &e.scratch); str_cat_u32(a, txt_ofs(e.buf[i].cur), &e.scratch); } } static void run_file(Str path, int debugp) { path = normalize_path(path); if (is_wed_path(path)) { vui_disable(); if (system(debugp ? "make DEBUG=1" : "make")) { press_any_key(); } else { vui_fini(); Str cmd = S("exec " WED_DEV_EXE " "); make_edit_args(&cmd); execl("/bin/sh", "sh", "-c", str_to_cstr(cmd, &e.scratch), 0); } } else { shell_run(debugp ? "make DEBUG=1 run" : "make run"); } } static void debug_file(Str path) { if (is_wed_path(path)) { vui_disable(); if (system("make DEBUG=1")) { press_any_key(); } else { vui_fini(); Str cmd = S("exec gdb -ex run --args " WED_DEV_EXE " "); make_edit_args(&cmd); execl("/bin/sh", "sh", "-c", str_to_cstr(cmd, &e.scratch), 0); } } else { Str cmd = S("gdb -ex run --args "); str_cat(&cmd, path, &e.scratch); shell_run(str_to_cstr(cmd, &e.scratch)); } } int main(int argc, const char **argv) { ed_init(&e); if (argc > 1) { ed_buf_close(&e, 0); /* close scratch buffer */ for (int i = 1; i < argc; i++) { Str s = str_from_cstr(argv[i]); Cut c = str_cut(s, '@'); e.bufi = ed_buf_open(&e, str_to_cstr(c.head, &e.scratch)); if (c.tail.n > 1) { EditBuf *eb = &e.buf[e.bufi]; u32 ofs; if (str_to_u32(c.tail, &ofs)) { fprintf(stderr, "invalid byte offset in %s\n", argv[i]); return 1; } eb->cur = txt_at(eb->txt, ofs); } } } vui_init(); vui_curs_vis(1); vui_redraw_fn(draw); while (e.bufn > 0) { arena_reset(&e.scratch); EditBuf *eb = &e.buf[e.bufi]; draw(NULL); switch (e.mode) { case MODE_NORMAL: vui_curs_shape(VUI_CURS_BLOCK); break; case MODE_INSERT: vui_curs_shape(VUI_CURS_BAR); break; case MODE_REPLACE_ONE: case MODE_REPLACE_MULT: vui_curs_shape(VUI_CURS_UNDERLINE); break; } vui_blit(); u32 c = vui_key(); switch (e.mode) { case MODE_NORMAL: switch (c) { case 'q': e.bufi = ed_buf_close(&e, e.bufi); break; case 'Q': while (e.bufn > 0) e.bufi = ed_buf_close(&e, e.bufi); break; case 'H': if (e.bufi > 0) e.bufi = e.bufi - 1; else e.bufi = e.bufn - 1; break; case 'L': e.bufi = (e.bufi + 1) % e.bufn; break; case 'z': case 'Z': case 0x13 /* ^S */: ed_buf_save(&e, e.bufi); //txt_load(&txt, "test.txt"); if (c == 'Z') e.bufi = ed_buf_close(&e, e.bufi); break; case 'i': e.mode = 1; break; case 'a': { e.mode = 1; TxtLoc l = eb->cur; TxtLoc e = end_of_line(l); l = txt_before(l, e) ? cnext(l) : e; eb->cur = l; } break; case 'I': e.mode = 1; eb->cur = logical_line_start(eb->cur); break; case 'A': e.mode = 1; eb->cur = end_of_line(eb->cur); break; case 'o': e.mode = 1; eb->cur = ins_newline(end_of_line(eb->cur)); break; case 'O': e.mode = 1; eb->cur = ins_newline(bprev(start_of_line(eb->cur))); if (at_start(bprev(eb->cur))) eb->cur = bprev(eb->cur); break; case 'd': { TxtLoc before = eb->cur; if (motion(&eb->cur, vui_key())) { eb->cur = txt_delete_range(before, eb->cur); } } break; case 'D': eb->cur = txt_delete_range(eb->cur, end_of_line(eb->cur)); break; case 'C': eb->cur = txt_delete_range(eb->cur, end_of_line(eb->cur)); e.mode = 1; break; case 'S': { TxtLoc start = start_of_line(eb->cur); TxtLoc end = end_of_line(eb->cur); eb->cur = txt_delete_range(start, end); e.mode = 1; } break; case 'c': { TxtLoc before = eb->cur; if (motion(&eb->cur, vui_key())) { eb->cur = txt_delete_range(before, eb->cur); e.mode = 1; } break; } case 'x': eb->cur = txt_delete_c(cnext(eb->cur)); break; case 's': eb->cur = txt_delete_c(cnext(eb->cur)); e.mode = 1; break; case 'r': e.mode = MODE_REPLACE_ONE; break; case 'R': e.mode = MODE_REPLACE_MULT; break; case 'M': { TxtLoc start = prev_par(eb->cur); TxtLoc end = next_par(eb->cur); if (shell_replace(start, end, "fmt -w80 -u")) { err(1, "shell_replace"); } } break; case ' ': switch ((u32)vui_key()) { case 'm': build_file(eb->path, 1); break; case 'M': build_file(eb->path, 0); break; case 'r': run_file(eb->path, 1); break; case 'R': run_file(eb->path, 0); break; case 'D': debug_file(eb->path); break; case 'e': vui_disable(); system("make clean"); vui_enable(); vui_redraw(); break; default: /* TODO: flash */ break; } break; case '>': { TxtLoc end = eb->cur; VuiKey k = vui_key(); if (k == '>' || motion(&end, k)) { if (k == '>') end = next_line_start(end); u32 ofs = txt_ofs(eb->cur); TxtLoc start = start_of_line(eb->cur); u32 lines = 0; for (TxtLoc t = start; txt_before(t, end); t = next_line_start(t)) lines++; while (lines--) { if (!empty_line(start)) start = txt_insert_c(start, '\t'); start = next_line_start(start); } eb->cur = txt_at(eb->txt, ofs); } } break; case '<': { TxtLoc end = eb->cur; VuiKey k = vui_key(); if (k == '<' || motion(&end, k)) { if (k == '<') end = next_line_start(end); u32 ofs = txt_ofs(eb->cur); TxtLoc start = start_of_line(eb->cur); u32 lines = 0; for (TxtLoc t = start; txt_before(t, end); t = next_line_start(t)) lines++; while (lines--) { if (txt_byte(start) == '\t') start = txt_delete_c(cnext(start)); start = next_line_start(start); } eb->cur = txt_at(eb->txt, ofs); } } break; case '=': { Str s = get_input_line(S("File to open: ")); if (s.n > 0) e.bufi = ed_buf_open(&e, str_to_cstr(s, &e.scratch)); } break; default: motion(&eb->cur, c); break; } break; case MODE_INSERT: switch (c) { case KEY_ESC: if (txt_after(eb->cur, start_of_line(eb->cur))) { eb->cur = cprev(eb->cur); } e.mode = 0; break; case KEY_BKSP: eb->cur = txt_delete_c(eb->cur); break; case 0x17 /* ^W */: eb->cur = txt_delete_range(prev_word(eb->cur), eb->cur); break; case 0x05 /* ^E */: eb->cur = txt_delete_range(eb->cur, next_word(eb->cur)); break; case 0x0c /* ^L */: vui_redraw(); break; case '\r': eb->cur = ins_newline(eb->cur); break; default: if ((c == '\t' || c >= ' ') && c <= KEY_UTF8_MAX) eb->cur = txt_insert_c(eb->cur, c); break; } break; case MODE_REPLACE_ONE: if (txt_byte(eb->cur) != '\n' && c != KEY_BKSP && c <= KEY_UTF8_MAX) { eb->cur = cprev(txt_insert_c(txt_delete_c(cnext(eb->cur)), c)); } e.mode = MODE_NORMAL; break; case MODE_REPLACE_MULT: if (c == '\x1b') { e.mode = MODE_NORMAL; eb->cur = cprev(eb->cur); } else if (c == KEY_BKSP) { if (txt_byte(bprev(eb->cur)) != '\n') eb->cur = cprev(eb->cur); } else if (c <= KEY_UTF8_MAX) { if (txt_byte(eb->cur) == '\n') { eb->cur = txt_insert_c(eb->cur, c); } else { eb->cur = txt_insert_c(txt_delete_c(cnext(eb->cur)), c); } } break; } if (e.mode == 1) e.count = 0; } vui_fini(); arena_free(&e.scratch); return 0; }