#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" #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; typedef struct { Arena scratch; EditBuf buf[ED_BUF_MAX]; u32 bufn, bufi; Txt *txt_free; EditMode mode; u32 count; } 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]); } } 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 '('; default: return c; } } static inline int bracket_dir(u32 c) { switch (c) { case '{': case '[': case '(': return 1; case '}': case ']': case ')': return -1; default: return 0; } } TxtLoc match_bracket(TxtLoc l) { 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--; } } return l; } 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) { return match_bracket(prev_func_end(l)); } TxtLoc next_func(TxtLoc l) { return match_bracket(next_func_end(match_bracket(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 = 1; TxtLoc start, end; find_view_window(eb->cur, &start, &end, LINES - 1); vui_aprintf(-1, 0, ODD_ATTR, "%u piece(s)", eb->txt->ptbl.n); for (u32 i = 0; i < eb->txt->ptbl.n; i++) { TxtPiece *p = &eb->txt->ptbl.v[i]; VuiAttr a = i&1 ? ODD_ATTR : EVEN_ATTR; vui_aprintf(-1, i+1, a, "%u, %u (%s)", p->ofs, p->n, p->buf == TXT_ADD ? "add" : "src"); } { VuiAttr norm = FG_WHITE | BG_BLACK; VuiAttr sel = FG_BLACK | BG_WHITE; int x = 0; for (u32 i = 0; i < e.bufn; i++) { EditBuf *b = &e.buf[i]; VuiAttr a = i == e.bufi ? sel : norm; x += vui_aprintf(x, 0, a, " %.*s", (int)b->path.n, b->path.s); if (b->type == ED_BUF_FILE && b->txt->dirty) x += vui_putsa(x, 0, "* ", a); else vui_chra(x++, 0, ' ', a); } int n = COLS; while (x < n) vui_chra(x++, 0, ' ', 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' ? (FG_WHITE | BG_BLUE) : (FG_WHITE | BG_BLACK); 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_chr(x++, y, c); } } ASSERT(start.i <= eb->txt->ptbl.v[start.p].n); if (!cur_found) vui_curs_pos(x, y); /* u32 c = txt_chr(l); vui_printf(-1, -4, "%u", e.count); vui_aprintf(-1, -3, e.mode ? FG_BCYAN : FG_CYAN, "%s", e.mode ? "INSERT" : "NORMAL"); vui_printf(-1, -1, "%u - %u.%u - %02x (%c)", txt_ofs(eb->cur), l.p, l.i, c, (c < 0x20 || c > 0x7e) ? ' ' : c); u32 used = e.scratch.beg - e.scratch.start, max = e.scratch.end - e.scratch.start; vui_printf(-1, -2, "e.scratch %.02f/%.02fk", used/1024.0, max/1024.0);*/ } TxtLoc logical_line_start(TxtLoc l) { l = start_of_line(l); while (txt_chr(l) == '\t') l = cnext(l); return l; } 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_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: case 'k': l = prev_line(l); break; case KEY_DOWN: case 'j': l = next_line(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 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': { u32 k = vui_key(); TxtLoc t = cnext(l); for (TxtLoc n = l;;) { u32 x = txt_chr_next(&n); if (x == '\n' || x == 0) break; if (x == k) { l = t; break; } t = n; } } break; case 'F': { u32 k = vui_key(); TxtLoc t = cprev(l); for (;;) { u32 x = txt_chr(l); if (x == '\n') break; if (x == k) { l = t; break; } t = bprev(t); } } 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) { 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); } } } else { e.bufi = ed_buf_open(&e, "main.c"); } 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; 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; }