/* vi.c - You can't spell "evil" without "vi". * * Copyright 2015 Rob Landley * Copyright 2019 Jarno Mäkipää * * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html USE_VI(NEWTOY(vi, "<1>1", TOYFLAG_USR|TOYFLAG_BIN)) config VI bool "vi" default n help usage: vi FILE Visual text editor. Predates the existence of standardized cursor keys, so the controls are weird and historical. */ #define FOR_vi #include "toys.h" GLOBALS( int cur_col; int cur_row; unsigned screen_height; unsigned screen_width; int vi_mode; ) /* * * TODO: * BUGS: screen pos adjust does not cover "widelines" * * * REFACTOR: use dllist functions where possible. * draw_page dont draw full page at time if nothing changed... * ex callbacks * * FEATURE: ex: / ? % //atleast easy cases * vi: x dw d$ d0 * vi: yw yy (y0 y$) * vi+ex: gg G //line movements * ex: r * ex: !external programs * ex: w filename //only writes to same file now * big file support? */ struct linestack_show { struct linestack_show *next; long top, left; int x, width, y, height; }; static void draw_page(); static int draw_str_until(int *drawn, char *str, int width, int bytes); static void draw_char(char c, int x, int y, int highlight); //utf8 support static int utf8_lnw(int* width, char* str, int bytes); static int utf8_dec(char key, char *utf8_scratch, int *sta_p) ; static int utf8_len(char *str); static int utf8_width(char *str, int bytes); static int draw_rune(char *c, int x, int y, int highlight); static void cur_left(); static void cur_right(); static void cur_up(); static void cur_down(); static void check_cursor_bounds(); static void adjust_screen_buffer(); struct str_line { int alloc_len; int str_len; char *str_data; }; //lib dllist uses next and prev kinda opposite what im used to so I just //renamed both ends to up and down struct linelist { struct linelist *up;//next struct linelist *down;//prev struct str_line *line; }; //inserted line not yet pushed to buffer struct str_line *il; struct linelist *text; //file loaded into buffer struct linelist *scr_r;//current screen coord 0 row struct linelist *c_r;//cursor position row int modified; void dlist_insert_nomalloc(struct double_list **list, struct double_list *new) { if (*list) { new->next = *list; new->prev = (*list)->prev; if ((*list)->prev) (*list)->prev->next = new; (*list)->prev = new; } else *list = new->next = new->prev = new; } // Add an entry to the end of a doubly linked list struct double_list *dlist_insert(struct double_list **list, char *data) { struct double_list *new = xmalloc(sizeof(struct double_list)); new->data = data; dlist_insert_nomalloc(list, new); return new; } void linelist_unload() { } void write_file(char *filename) { struct linelist *lst = text; FILE *fp = 0; if (!filename) filename = (char*)*toys.optargs; fp = fopen(filename, "w"); if (!fp) return ; while (lst) { fprintf(fp, "%s\n", lst->line->str_data); lst = lst->down; } fclose(fp); } int linelist_load(char *filename) { struct linelist *lst = c_r;//cursor position or 0 FILE *fp = 0; if (!filename) filename = (char*)*toys.optargs; fp = fopen(filename, "r"); if (!fp) { char *line = xzalloc(80); ssize_t alc = 80; lst = (struct linelist*)dlist_add((struct double_list**)&lst, xzalloc(sizeof(struct str_line))); lst->line->alloc_len = alc; lst->line->str_len = 0; lst->line->str_data = line; text = lst; dlist_terminate(text->up); return 1; } for (;;) { char *line = xzalloc(80); ssize_t alc = 80; ssize_t len; if ((len = getline(&line, (void *)&alc, fp)) == -1) { if (errno == EINVAL || errno == ENOMEM) { printf("error %d\n", errno); } free(line); break; } lst = (struct linelist*)dlist_add((struct double_list**)&lst, xzalloc(sizeof(struct str_line))); lst->line->alloc_len = alc; lst->line->str_len = len; lst->line->str_data = line; if (lst->line->str_data[len-1] == '\n') { lst->line->str_data[len-1] = 0; lst->line->str_len--; } if (text == 0) { text = lst; } } if (text) { dlist_terminate(text->up); } fclose(fp); return 1; } //TODO this is overly complicated refactor with lib dllist int ex_dd(int count) { struct linelist *lst = c_r; if (c_r == text && text == scr_r) { if (!text->down && !text->up && text->line) { text->line->str_len = 1; sprintf(text->line->str_data, " "); goto success_exit; } if (text->down) { text = text->down; text->up = 0; c_r = text; scr_r = text; free(lst->line->str_data); free(lst->line); free(lst); } goto recursion_exit; } //TODO use lib dllist stuff if (lst) { if (lst->down) { lst->down->up = lst->up; } if (lst->up) { lst->up->down = lst->down; } if (scr_r == c_r) { scr_r = c_r->down ? c_r->down : c_r->up; } if (c_r->down) c_r = c_r->down; else { c_r = c_r->up; count = 1; } free(lst->line->str_data); free(lst->line); free(lst); } recursion_exit: count--; //make this recursive if (count) return ex_dd(count); success_exit: check_cursor_bounds(); adjust_screen_buffer(); return 1; } int ex_dw(int count) { return 1; } int ex_deol(int count) { return 1; } //does not work with utf8 yet int vi_x(int count) { char *s; int *l; int *p; if (!c_r) return 0; s = c_r->line->str_data; l = &c_r->line->str_len; p = &TT.cur_col; if (!(*l)) return 0; if ((*p) == (*l)-1) { s[*p] = 0; if (*p) (*p)--; (*l)--; } else { memmove(s+(*p), s+(*p)+1, (*l)-(*p)); s[*l] = 0; (*l)--; } count--; return (count) ? vi_x(count) : 1; } //move commands does not behave correct way yet. //only jump to next space for now. int vi_movw(int count) { if (!c_r) return 0; //could we call moveend first while (c_r->line->str_data[TT.cur_col] > ' ') TT.cur_col++; while (c_r->line->str_data[TT.cur_col] <= ' ') { TT.cur_col++; if (!c_r->line->str_data[TT.cur_col]) { //we could call j and g0 if (!c_r->down) return 0; c_r = c_r->down; TT.cur_col = 0; } } count--; if (count>1) return vi_movw(count); check_cursor_bounds(); adjust_screen_buffer(); return 1; } int vi_movb(int count) { if (!c_r) return 0; if (!TT.cur_col) { if (!c_r->up) return 0; c_r = c_r->up; TT.cur_col = (c_r->line->str_len) ? c_r->line->str_len-1 : 0; goto exit_function; } if (TT.cur_col) TT.cur_col--; while (c_r->line->str_data[TT.cur_col] <= ' ') { if (TT.cur_col) TT.cur_col--; else goto exit_function; } while (c_r->line->str_data[TT.cur_col] > ' ') { if (TT.cur_col)TT.cur_col--; else goto exit_function; } TT.cur_col++; exit_function: count--; if (count>1) return vi_movb(count); check_cursor_bounds(); adjust_screen_buffer(); return 1; } int vi_move(int count) { if (!c_r) return 0; if (TT.cur_col < c_r->line->str_len) TT.cur_col++; if (c_r->line->str_data[TT.cur_col] <= ' ' || count > 1) vi_movw(count); //find next word; while (c_r->line->str_data[TT.cur_col] > ' ') TT.cur_col++; if (TT.cur_col) TT.cur_col--; check_cursor_bounds(); adjust_screen_buffer(); return 1; } void i_insert() { char *t = xzalloc(c_r->line->alloc_len); char *s = c_r->line->str_data; int sel = c_r->line->str_len-TT.cur_col; strncpy(t, &s[TT.cur_col], sel); t[sel+1] = 0; if (c_r->line->alloc_len < c_r->line->str_len+il->str_len+5) { c_r->line->str_data = xrealloc(c_r->line->str_data, c_r->line->alloc_len*2+il->alloc_len*2); c_r->line->alloc_len = c_r->line->alloc_len*2+2*il->alloc_len; memset(&c_r->line->str_data[c_r->line->str_len], 0, c_r->line->alloc_len-c_r->line->str_len); s = c_r->line->str_data; } strcpy(&s[TT.cur_col], il->str_data); strcpy(&s[TT.cur_col+il->str_len], t); TT.cur_col += il->str_len; if (TT.cur_col) TT.cur_col--; c_r->line->str_len += il->str_len; free(t); } //new line at split pos; void i_split() { struct str_line *l = xmalloc(sizeof(struct str_line)); int l_a = c_r->line->alloc_len; int l_len = c_r->line->str_len-TT.cur_col; l->str_data = xzalloc(l_a); l->alloc_len = l_a; l->str_len = l_len; strncpy(l->str_data, &c_r->line->str_data[TT.cur_col], l_len); l->str_data[l_len] = 0; c_r->line->str_len -= l_len; c_r->line->str_data[c_r->line->str_len] = 0; c_r = (struct linelist*)dlist_insert((struct double_list**)&c_r, (char*)l); c_r->line = l; TT.cur_col = 0; check_cursor_bounds(); adjust_screen_buffer(); } struct vi_cmd_param { const char *cmd; int (*vi_cmd_ptr)(int); }; struct vi_cmd_param vi_cmds[7] = { {"dd", &ex_dd}, {"dw", &ex_dw}, {"d$", &ex_deol}, {"w", &vi_movw}, {"b", &vi_movb}, {"e", &vi_move}, {"x", &vi_x}, }; int run_vi_cmd(char *cmd) { int val = 0; char *cmd_e; errno = 0; int i = 0; val = strtol(cmd, &cmd_e, 10); if (errno || val == 0) { val = 1; } else { cmd = cmd_e; } for (; i<7; i++) { if (strstr(cmd, vi_cmds[i].cmd)) { return vi_cmds[i].vi_cmd_ptr(val); } } return 0; } int search_str(char *s) { struct linelist *lst = c_r; char *c = strstr(&c_r->line->str_data[TT.cur_col], s); if (c) { TT.cur_col = c_r->line->str_data-c; TT.cur_col = c-c_r->line->str_data; } else for (; !c;) { lst = lst->down; if (!lst) return 1; c = strstr(&lst->line->str_data[TT.cur_col], s); } c_r = lst; TT.cur_col = c-c_r->line->str_data; return 0; } int run_ex_cmd(char *cmd) { if (cmd[0] == '/') { //search pattern if (!search_str(&cmd[1]) ) { check_cursor_bounds(); adjust_screen_buffer(); } } else if (cmd[0] == '?') { } else if (cmd[0] == ':') { if (strstr(&cmd[1], "q!")) { //exit_application; return -1; } else if (strstr(&cmd[1], "wq")) { write_file(0); return -1; } else if (strstr(&cmd[1], "w")) { write_file(0); return 1; } } return 0; } void vi_main(void) { char keybuf[16]; char utf8_code[8]; int utf8_dec_p = 0; int key = 0; char vi_buf[16]; int vi_buf_pos = 0; il = xzalloc(sizeof(struct str_line)); il->str_data = xzalloc(80); il->alloc_len = 80; keybuf[0] = 0; memset(vi_buf, 0, 16); memset(utf8_code, 0, 8); linelist_load(0); scr_r = text; c_r = text; TT.cur_row = 0; TT.cur_col = 0; TT.screen_width = 80; TT.screen_height = 24; TT.vi_mode = 1; terminal_size(&TT.screen_width, &TT.screen_height); TT.screen_height -= 2; //TODO this is hack fix visual alignment set_terminal(0, 1, 0, 0); //writes stdout into different xterm buffer so when we exit //we dont get scroll log full of junk tty_esc("?1049h"); tty_esc("H"); xflush(); draw_page(); while(1) { key = scan_key(keybuf, -1); printf("key %d\n", key); switch (key) { case -1: case 3: case 4: goto cleanup_vi; } if (TT.vi_mode == 1) { //NORMAL switch (key) { case 'h': cur_left(); break; case 'j': cur_down(); break; case 'k': cur_up(); break; case 'l': cur_right(); break; case '/': case '?': case ':': TT.vi_mode = 0; il->str_data[0]=key; il->str_len++; break; case 'a': if (c_r && c_r->line->str_len) TT.cur_col++; case 'i': TT.vi_mode = 2; break; case 27: vi_buf[0] = 0; vi_buf_pos = 0; break; default: if (key > 0x20 && key < 0x7B) { vi_buf[vi_buf_pos] = key; vi_buf_pos++; if (run_vi_cmd(vi_buf)) { memset(vi_buf, 0, 16); vi_buf_pos = 0; } else if (vi_buf_pos == 16) { vi_buf_pos = 0; } } break; } } else if (TT.vi_mode == 0) { //EX MODE switch (key) { case 27: TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; case 0x7F: case 0x08: if (il->str_len) { il->str_data[il->str_len] = 0; if (il->str_len > 1) il->str_len--; } break; case 0x0D: if (run_ex_cmd(il->str_data) == -1) goto cleanup_vi; TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; default: //add chars to ex command until ENTER if (key >= 0x20 && key < 0x7F) { //might be utf? if (il->str_len == il->alloc_len) { il->str_data = realloc(il->str_data, il->alloc_len*2); il->alloc_len *= 2; } il->str_data[il->str_len] = key; il->str_len++; } break; } } else if (TT.vi_mode == 2) {//INSERT MODE switch (key) { case 27: i_insert(); TT.vi_mode = 1; il->str_len = 0; memset(il->str_data, 0, il->alloc_len); break; case 0x7F: case 0x08: if (il->str_len) il->str_data[il->str_len--] = 0; break; case 0x09: //TODO implement real tabs il->str_data[il->str_len++] = ' '; il->str_data[il->str_len++] = ' '; break; case 0x0D: //insert newline // i_insert(); il->str_len = 0; memset(il->str_data, 0, il->alloc_len); i_split(); break; default: if (key >= 0x20 && utf8_dec(key, utf8_code, &utf8_dec_p)) { if (il->str_len+utf8_dec_p+1 >= il->alloc_len) { il->str_data = realloc(il->str_data, il->alloc_len*2); il->alloc_len *= 2; } strcpy(il->str_data+il->str_len, utf8_code); il->str_len += utf8_dec_p; utf8_dec_p = 0; *utf8_code = 0; } break; } } draw_page(); } cleanup_vi: tty_reset(); tty_esc("?1049l"); } static void draw_page() { unsigned y = 0; int cy_scr = 0; int cx_scr = 0; int utf_l = 0; char* line = 0; int bytes = 0; int drawn = 0; int x = 0; struct linelist *scr_buf= scr_r; //clear screen tty_esc("2J"); tty_esc("H"); tty_jump(0, 0); //draw lines until cursor row for (; y < TT.screen_height; ) { if (line && bytes) { draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); } else if (scr_buf && scr_buf->line->str_data && scr_buf->line->str_len) { if (scr_buf == c_r) break; line = scr_buf->line->str_data; bytes = scr_buf->line->str_len; scr_buf = scr_buf->down; } else { if (scr_buf == c_r) break; y++; tty_jump(0, y); //printf(" \n"); if (scr_buf) scr_buf = scr_buf->down; } } //draw cursor row until cursor //this is to calculate cursor position on screen and possible insert line = scr_buf->line->str_data; bytes = TT.cur_col; for (; y < TT.screen_height; ) { if (bytes) { x = draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; } if (!bytes) break; y++; tty_jump(0, y); } if (TT.vi_mode == 2 && il->str_len) { line = il->str_data; bytes = il->str_len; cx_scr = x; cy_scr = y; x = draw_str_until(&drawn, line, TT.screen_width-x, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; cx_scr += x; for (; y < TT.screen_height; ) { if (bytes) { x = draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; cx_scr = x; } if (!bytes) break; y++; cy_scr = y; tty_jump(0, y); } } else { cy_scr = y; cx_scr = x; } line = scr_buf->line->str_data+TT.cur_col; bytes = scr_buf->line->str_len-TT.cur_col; scr_buf = scr_buf->down; x = draw_str_until(&drawn,line, TT.screen_width-x, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); //draw until end for (; y < TT.screen_height; ) { if (line && bytes) { draw_str_until(&drawn, line, TT.screen_width, bytes); bytes = drawn ? (bytes-drawn) : 0; line = bytes ? (line+drawn) : 0; y++; tty_jump(0, y); } else if (scr_buf && scr_buf->line->str_data && scr_buf->line->str_len) { line = scr_buf->line->str_data; bytes = scr_buf->line->str_len; scr_buf = scr_buf->down; } else { y++; tty_jump(0, y); if (scr_buf) scr_buf = scr_buf->down; } } tty_jump(0, TT.screen_height); switch (TT.vi_mode) { case 0: tty_esc("30;44m"); printf("COMMAND|"); break; case 1: tty_esc("30;42m"); printf("NORMAL|"); break; case 2: tty_esc("30;41m"); printf("INSERT|"); break; } //DEBUG tty_esc("47m"); tty_esc("30m"); utf_l = utf8_len(&c_r->line->str_data[TT.cur_col]); if (utf_l) { char t[5] = {0, 0, 0, 0, 0}; strncpy(t, &c_r->line->str_data[TT.cur_col], utf_l); printf("utf: %d %s", utf_l, t); } printf("| %d, %d\n", cx_scr, cy_scr); //screen coord tty_jump(TT.screen_width-12, TT.screen_height); printf("| %d, %d\n", TT.cur_row, TT.cur_col); tty_esc("37m"); tty_esc("40m"); if (!TT.vi_mode) { tty_esc("1m"); tty_jump(0, TT.screen_height+1); printf("%s", il->str_data); tty_esc("0m"); } else tty_jump(cx_scr, cy_scr); xflush(); } static void draw_char(char c, int x, int y, int highlight) { tty_jump(x, y); if (highlight) { tty_esc("30m"); //foreground black tty_esc("47m"); //background white } printf("%c", c); } //utf rune draw //printf and useless copy could be replaced by direct write() to stdout static int draw_rune(char *c, int x, int y, int highlight) { int l = utf8_len(c); char t[5] = {0, 0, 0, 0, 0}; if (!l) return 0; tty_jump(x, y); tty_esc("0m"); if (highlight) { tty_esc("30m"); //foreground black tty_esc("47m"); //background white } strncpy(t, c, 5); printf("%s", t); tty_esc("0m"); return l; } static void check_cursor_bounds() { if (c_r->line->str_len-1 < TT.cur_col) { if (c_r->line->str_len == 0) TT.cur_col = 0; else TT.cur_col = c_r->line->str_len-1; } } static void adjust_screen_buffer() { //search cursor and screen TODO move this perhaps struct linelist *t = text; int c = -1; int s = -1; int i = 0; for (;;) { i++; if (t == c_r) c = i; if (t == scr_r) s = i; t = t->down; if ( ((c != -1) && (s != -1)) || t == 0) break; } if (c <= s) { scr_r = c_r; } else if ( c > s ) { //should count multiline long strings! int distance = c - s + 1; //TODO instead iterate scr_r up and check strlen%screen_width //for each iteration if (distance >= (int)TT.screen_height) { int adj = distance - TT.screen_height; while(adj--) { scr_r = scr_r->down; } } } TT.cur_row = c; } //return 0 if not ASCII nor UTF-8 //this is not fully tested //naive implementation with branches //there is better branchless lookup table versions out there //1 0xxxxxxx //2 110xxxxx 10xxxxxx //3 1110xxxx 10xxxxxx 10xxxxxx //4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx static int utf8_len(char *str) { int len = 0; int i = 0; uint8_t *c = (uint8_t*)str; if (!c || !(*c)) return 0; if (*c < 0x7F) return 1; if ((*c & 0xE0) == 0xc0) len = 2; else if ((*c & 0xF0) == 0xE0 ) len = 3; else if ((*c & 0xF8) == 0xF0 ) len = 4; else return 0; c++; for (i = len-1; i > 0; i--) { if ((*c++ & 0xc0) != 0x80) return 0; } return len; } //get utf8 length and width at same time static int utf8_lnw(int* width, char* str, int bytes) { wchar_t wc; int length = 1; *width = 1; // if (str < 0x7F) return length; length = mbtowc(&wc, str, bytes); switch (length) { case -1: mbtowc(0,0,4); case 0: *width = 0; length = 0; break; default: *width = wcwidth(wc); } return length; } //try to estimate width of next "glyph" in terminal buffer //combining chars 0x300-0x36F shall be zero width static int utf8_width(char *str, int bytes) { wchar_t wc; switch (mbtowc(&wc, str, bytes)) { case -1: mbtowc(0,0,4); case 0: return -1; default: return wcwidth(wc); } return 0; } static int utf8_dec(char key, char *utf8_scratch, int *sta_p) { int len = 0; char *c = utf8_scratch; c[*sta_p] = key; if (!(*sta_p)) *c = key; if (*c < 0x7F) { *sta_p = 1; return 1; } if ((*c & 0xE0) == 0xc0) len = 2; else if ((*c & 0xF0) == 0xE0 ) len = 3; else if ((*c & 0xF8) == 0xF0 ) len = 4; else {*sta_p = 0; return 0; } (*sta_p)++; if (*sta_p == 1) return 0; if ((c[*sta_p-1] & 0xc0) != 0x80) {*sta_p = 0; return 0; } if (*sta_p == len) { c[(*sta_p)] = 0; return 1; } return 0; } static int draw_str_until(int *drawn, char *str, int width, int bytes) { int rune_width = 0; int rune_bytes = 0; int max_bytes = bytes; int max_width = width; char* end = str; for (;width && bytes;) { rune_bytes = utf8_lnw(&rune_width, end, 4); if (!rune_bytes) break; if (width - rune_width < 0) goto write_bytes; width -= rune_width; bytes -= rune_bytes; end += rune_bytes; } for (;bytes;) { rune_bytes = utf8_lnw(&rune_width, end, 4); if (!rune_bytes) break; if (rune_width) break; bytes -= rune_bytes; end += rune_bytes; } write_bytes: fwrite(str, max_bytes-bytes, 1, stdout); *drawn = max_bytes-bytes; return max_width-width; } static void cur_left() { if (!TT.cur_col) return; TT.cur_col--; if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left(); } static void cur_right() { if (c_r->line->str_len <= 1) return; if (TT.cur_col == c_r->line->str_len-1) return; TT.cur_col++; if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_right(); } static void cur_up() { if (c_r->up != 0) c_r = c_r->up; if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left(); check_cursor_bounds(); adjust_screen_buffer(); } static void cur_down() { if (c_r->down != 0) c_r = c_r->down; if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left(); check_cursor_bounds(); adjust_screen_buffer(); }