#define _POSIX_C_SOURCE 202511L #include #include #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; /* these unfortunately have to be global so that draw() will know what * to do when a window resizes during input */ Str msg; Str input_line, input_prompt; u32 optc, opti; Str *optv; u32 *optvi; 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 (path) { Str s = str_from_cstr(path); for (u32 i = 0; i < e->bufn; i++) { if (str_eql(e->buf[i].path, s)) return i; } } 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_buf_change_path(Editor *e, u32 i, Str s) { EditBuf *eb = &e->buf[i]; eb->path = str_dup(s, &eb->arena); eb->type = ED_BUF_FILE; } 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; } } Str str_printf(Arena *a, const char *fmt, ...) { va_list ap; va_start(ap, fmt); int n = vsnprintf(NULL, 0, fmt, ap); va_end(ap); va_start(ap, fmt); char *buf = new_arr(a, char, n + 1); vsnprintf(buf, n + 1, fmt, ap); va_end(ap); return (Str) { buf, n }; } 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 c) { u32 depth = 1; u32 o = bracket_opp(c); if (txt_chr(l) != c) return 0; 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(); } const char *expand_cmd(const char *src) { DYNARR(char) s = { 0 }; u32 n = strlen(src); for (u32 i = 0; i < n; i++) { if (src[i] == '%') { i++; if (src[i] == '%') { DA_APUSH(&s, &e.scratch, '%'); } else { DA_APUSH_MULT(&s, &e.scratch, e.buf[e.bufi].path.s, e.buf[e.bufi].path.n); if (i < n) { DA_APUSH(&s, &e.scratch, src[i]); } } } else { DA_APUSH(&s, &e.scratch, src[i]); } } DA_APUSH(&s, &e.scratch, '\0'); return s.v; } int shell_run(const char *cmd) { vui_disable(); int r = system(expand_cmd(cmd)); press_any_key(); return r; } int shell_run_no_prompt(const char *cmd) { vui_disable(); int r = system(expand_cmd(cmd)); vui_enable(); vui_redraw(); return r; } /* main */ 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_buf(EditBuf *eb) { int lmarg = 0; int x = lmarg, y = 0; TxtLoc start, end; find_view_window(eb->cur, &start, &end, LINES - 1); VuiAttr norm = A_DEFAULT; VuiAttr sel = norm | A_REVERSE; VuiAttr txt = A_DEFAULT; VuiAttr trailing_whitespace = BG_BLUE | FG_DEFAULT; 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; if (e.msg.n) { x += vui_aprintf(x, y, norm, " %.*s", (int)e.msg.n, e.msg.s); e.msg = (Str) { 0, 0 }; } 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' ? trailing_whitespace : 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) { VuiAttr a = norm; u32 x = 0; u32 y = LINES - 1; x += vui_putsna(0, y, e.input_prompt.s, e.input_prompt.n, a); x += vui_putsna(x, y, e.input_line.s, e.input_line.n, a); vui_curs_pos(x, y); vui_curs_shape(VUI_CURS_BAR); u32 c = COLS; while (x < c) vui_chra(x++, y, ' ', a); } } void draw_opts(Str *optv, u32 optc, u32 *optvi, u32 opti) { u32 x = 0; u32 y = 0; VuiAttr a = A_DEFAULT | A_ITALIC; vui_clear(); x += vui_putsna(0, y, e.input_prompt.s, e.input_prompt.n, a); x += vui_putsna(x, y, e.input_line.s, e.input_line.n, a); vui_curs_pos(x, y); vui_curs_shape(VUI_CURS_BAR); y++; u32 n = LINES; u32 tn = n / 2; u32 top = opti > tn ? opti - tn : 0; u32 bot = optc - top > n ? n : optc - top; for (u32 i = top; i < bot; i++) { VuiAttr a = A_DEFAULT; if (i == opti) a |= A_REVERSE; vui_putsna(2, y++, optv[optvi[i]].s, optv[optvi[i]].n, a); } } void draw(void *ctx) { (void)ctx; if (e.optv) { draw_opts(e.optv, e.optc, e.optvi, e.opti); } else { draw_buf(&e.buf[e.bufi]); } } TxtLoc logical_line_start(TxtLoc l) { l = start_of_line(l); while (txt_chr(l) == '\t') l = cnext(l); return l; } typedef struct { Arena *a; Str prompt; DYNARR(char) s; } LineEditor; typedef enum { LEDIT_EMPTY, LEDIT_DONE, LEDIT_CONT, LEDIT_UP, LEDIT_DOWN } LineEditStatus; static LineEditor line_editor(Str prompt, Arena *a) { e.input_line = (Str) { 0, 0 }; e.input_prompt = prompt; return (LineEditor) { a, prompt, { 0 }}; } static LineEditStatus line_edit(LineEditor *le, u32 c) { switch (c) { case '\r': e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return LEDIT_DONE; case KEY_UP: return LEDIT_UP; case KEY_DOWN: return LEDIT_DOWN; case KEY_ESC: e.input_line = (Str) { 0, 0 }; e.input_prompt = (Str) { 0, 0 }; return LEDIT_EMPTY; case KEY_BKSP: if (le->s.n > 0) le->s.n--; break; case 0x17 /* ^W */: while (le->s.n > 0 && is_space(le->s.v[le->s.n-1])) le->s.n--; while (le->s.n > 0 && !is_space(le->s.v[le->s.n-1])) le->s.n--; break; default: if (c > 0 && c <= KEY_UTF8_MAX && c >= ' ') { u32 n = utf8_encode_len(&c, 1); DA_AFIT(&le->s, le->a, le->s.n + n); utf8_encode(&le->s.v[le->s.n], &c, n); le->s.n += n; } e.opti = 0; break; } e.input_line = (Str) { le->s.v, le->s.n }; return LEDIT_CONT; } Str get_input_line(Str prompt) { LineEditor le = line_editor(prompt, &e.scratch); for (;;) { draw(NULL); vui_blit(); LineEditStatus st = line_edit(&le, vui_key()); if (st == LEDIT_EMPTY) return (Str) { 0, 0 }; if (st == LEDIT_DONE) return (Str) { le.s.v, le.s.n }; } } int search_next_regex(TxtLoc l, Str src, TxtLoc *out) { RegEx re = { 0 }; ReSearch s = { 0 }; int err = re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS); if (err) { e.msg = str_printf(&e.scratch, "Regex error: %s", re_comp_strerror(err)); 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)) { while (m.extent.start--) p = bnext(p); *out = p; 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 }; int err = re_comp_ex(&re, src, &e.scratch, &e.scratch, RE_COMP_NO_GROUPS); if (err) { e.msg = str_printf(&e.scratch, "Regex error: %s", re_comp_strerror(err)); 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)) { *out = p; 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, txt_chr(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)); } } static TxtLoc indent_dedent_lines(TxtLoc start, TxtLoc end, int n) { if (txt_before(end, start)) { TxtLoc t = start; start = end; end = t; } u32 ofs = txt_ofs(start); start = start_of_line(start); u32 lines = 0; for (TxtLoc t = start; txt_before(t, end); t = next_line_start(t)) lines++; while (lines--) { if (n > 0) { if (!empty_line(start)) for (u32 j = n; j--;) start = txt_insert_c(start, '\t'); } else { for (u32 j = n; j++;) if (txt_byte(start) == '\t') start = txt_delete_c(cnext(start)); } start = next_line_start(start); } return txt_at(start.t, ofs); } int *opt_sort_scr; static inline char to_lower(char c) { /* TODO: consider utf-8 support */ if (c >= 'A' && c <= 'Z') return c + ('a' - 'A'); return c; } int fz_score(Str s, Str p) { if (!p.n || str_eql(s, p)) return 0; int n = 1; u32 pi = 0, si = 0; for (;;) { if (to_lower(p.s[pi]) == to_lower(s.s[si])) { si++; pi++; if (si == s.n) { return pi < p.n ? -1 : n; } else if (pi == p.n) { return n; } } else { si++; n++; if (si == s.n) return -1; } } } void qsort_opt(u32 *dest, int *scr, int n) { int i = 0, j = n - 1; int x = scr[n / 2]; do { while (scr[i] < x) i++; while (x < scr[j]) j--; if (i <= j) { int t = dest[i]; dest[i] = dest[j]; dest[j] = t; t = scr[i]; scr[i] = scr[j]; scr[j] = t; i++; j--; } } while (i <= j); if (0 < j) qsort_opt(dest, scr, j + 1); if (i+1 < n) qsort_opt(dest + i, scr + i, n - i); } u32 sort_opt_idx(u32 *dest, Str *src, u32 n, Str pat, Arena *scratch) { int *scr = new_arr(scratch, int, n); u32 scrn = 0; for (u32 i = 0; i < n; i++) { int s = fz_score(src[i], pat); if (s != -1) { scr[scrn] = s; dest[scrn] = i; scrn++; } } qsort_opt(dest, scr, scrn); return scrn; } int select_opt(Str *optv, u32 optc, Str prompt) { if (!optc) return -1; e.optc = optc; e.optvi = new_arr(&e.scratch, u32, optc); e.optv = optv; LineEditor le = line_editor(prompt, &e.scratch); int r = -1; for (;;) { Arena s = e.scratch; e.optc = sort_opt_idx(e.optvi, optv, optc, e.input_line, &e.scratch); draw(NULL); vui_blit(); e.scratch = s; LineEditStatus st = line_edit(&le, vui_key()); if (st == LEDIT_EMPTY) break; if (st == LEDIT_DONE) { r = e.optvi[e.opti]; break; } if (st == LEDIT_DOWN) e.opti = (e.opti + 1) % e.optc; if (st == LEDIT_UP) e.opti = e.opti ? e.opti - 1 : e.optc - 1; } e.optc = 0; e.optv = NULL; return r; } #include Str select_file_in(const char *path) { DIR *d = opendir(path); if (!d) return (Str) { 0, 0 }; struct dirent *de; DYNARR(Str) opt = { 0 }; DA_APUSH(&opt, &e.scratch, S("..")); while ((de = readdir(d))) { if (de->d_name[0] == '.') continue; Str n = str_dup(str_from_cstr(de->d_name), &e.scratch); DA_APUSH(&opt, &e.scratch, n); } int o = select_opt(opt.v, opt.n, S("File: ")); if (o == -1) return (Str) { 0, 0 }; return opt.v[o]; } Str select_file(void) { Str s = select_file_in("."); return s; } 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) { EditBuf *eb = &e.buf[e.bufi]; draw(NULL); arena_reset(&e.scratch); /* must happen after draw, so e.msg can persist */ 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); e.msg = str_printf(&e.scratch, "%.02fk written", eb->txt->len / 1024.0); 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 'J': { TxtLoc l = end_of_line(eb->cur); if (txt_byte(l) == '\n') { do l = txt_delete_c(cnext(l)); while (is_space(txt_chr(l))); l = cprev(txt_insert_c(l, ' ')); eb->cur = l; } } 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 'f': { Str s = select_file(); if (s.n > 0) e.bufi = ed_buf_open(&e, str_to_cstr(s, &e.scratch)); } break; case 'F': { Str s = select_file(); if (s.n > 0) { e.bufi = ed_buf_close(&e, e.bufi); e.bufi = ed_buf_open(&e, str_to_cstr(s, &e.scratch)); } } break; 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': shell_run_no_prompt("make clean"); break; case 'l': shell_run_no_prompt("git log --oneline %"); break; case 'd': shell_run_no_prompt("git diff --color=always % | less -cr"); break; case 'c': shell_run_no_prompt("git add % && git commit %"); break; default: /* TODO: flash */ break; } break; case '<': case '>': { TxtLoc end = eb->cur; VuiKey k = vui_key(); if (k == '<' || k == '>' || motion(&end, k)) { if (k == '<' || k == '>') end = next_line_start(end); eb->cur = indent_dedent_lines(eb->cur, end, c == '<' ? -1 : 1); } } 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; case '+': { Str s = get_input_line(S("Replace file with: ")); if (s.n > 0) { e.bufi = ed_buf_close(&e, e.bufi); e.bufi = ed_buf_open(&e, str_to_cstr(s, &e.scratch)); } } break; case '-': { Str s = get_input_line(S("Change path: ")); if (s.n > 0) ed_buf_change_path(&e, e.bufi, s); } 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; }