/* 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, ">1s:", TOYFLAG_USR|TOYFLAG_BIN)) config VI bool "vi" default n help usage: vi [-s script] FILE -s script: run script 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( char *s; int vi_mode, tabstop, list; int cur_col, cur_row, scr_row; int drawn_row, drawn_col; int count0, count1, vi_mov_flag; unsigned screen_height, screen_width; char vi_reg, *last_search; struct str_line { int alloc; int len; char *data; } *il; size_t screen, cursor; //offsets //yank buffer struct yank_buf { char reg; int alloc; char* data; } yank; int modified; size_t filesize; // mem_block contains RO data that is either original file as mmap // or heap allocated inserted data // // // struct block_list { struct block_list *next, *prev; struct mem_block { size_t size; size_t len; enum alloc_flag { MMAP, //can be munmap() before exit() HEAP, //can be free() before exit() STACK, //global or stack perhaps toybuf } alloc; const char *data; } *node; } *text; // slices do not contain actual allocated data but slices of data in mem_block // when file is first opened it has only one slice. // after inserting data into middle new mem_block is allocated for insert data // and 3 slices are created, where first and last slice are pointing to original // mem_block with offsets, and middle slice is pointing to newly allocated block // When deleting, data is not freed but mem_blocks are sliced more such way that // deleted data left between 2 slices struct slice_list { struct slice_list *next, *prev; struct slice { size_t len; const char *data; } *node; } *slices; ) static const char *blank = " \n\r\t"; static const char *specials = ",.:;=-+*/(){}<>[]!@#$%^&|\\?\"\'"; //get utf8 length and width at same time static int utf8_lnw(int *width, char *s, int bytes) { unsigned wc; int length = 1; if (*s == '\t') *width = TT.tabstop; else { length = utf8towc(&wc, s, bytes); if (length < 1) length = 0, *width = 0; else *width = wcwidth(wc); } return length; } 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 char* utf8_last(char* str, int size) { char* end = str+size; int pos = size, len, width = 0; for (;pos >= 0; end--, pos--) { len = utf8_lnw(&width, end, size-pos); if (len && width) return end; } return 0; } struct double_list *dlist_add_before(struct double_list **head, struct double_list **list, char *data) { struct double_list *new = xmalloc(sizeof(struct double_list)); new->data = data; if (*list == *head) *head = new; dlist_add_nomalloc(list, new); return new; } struct double_list *dlist_add_after(struct double_list **head, struct double_list **list, char *data) { struct double_list *new = xmalloc(sizeof(struct double_list)); new->data = data; if (*list) { new->prev = *list; new->next = (*list)->next; (*list)->next->prev = new; (*list)->next = new; } else *head = *list = new->next = new->prev = new; return new; } // str must be already allocated // ownership of allocated data is moved // data, pre allocated data // offset, offset in whole text // size, data allocation size of given data // len, length of the string // type, define allocation type for cleanup purposes at app exit static int insert_str(const char *data, size_t offset, size_t size, size_t len, enum alloc_flag type) { struct mem_block *b = xmalloc(sizeof(struct mem_block)); struct slice *next = xmalloc(sizeof(struct slice)); struct slice_list *s = TT.slices; b->size = size; b->len = len; b->alloc = type; b->data = data; next->len = len; next->data = data; //mem blocks can be just added unordered TT.text = (struct block_list *)dlist_add((struct double_list **)&TT.text, (char *)b); if (!s) { TT.slices = (struct slice_list *)dlist_add( (struct double_list **)&TT.slices, (char *)next); } else { size_t pos = 0; //search insertation point for slice do { if (pos<=offset && pos+s->node->len>offset) break; pos += s->node->len; s = s->next; if (s == TT.slices) return -1; //error out of bounds } while (1); //need to cut previous slice into 2 since insert is in middle if (pos+s->node->len>offset && pos!=offset) { struct slice *tail = xmalloc(sizeof(struct slice)); tail->len = s->node->len-(offset-pos); tail->data = s->node->data+(offset-pos); s->node->len = offset-pos; //pos = offset; s = (struct slice_list *)dlist_add_after( (struct double_list **)&TT.slices, (struct double_list **)&s, (char *)tail); s = (struct slice_list *)dlist_add_before( (struct double_list **)&TT.slices, (struct double_list **)&s, (char *)next); } else if (pos==offset) { // insert before s = (struct slice_list *)dlist_add_before( (struct double_list **)&TT.slices, (struct double_list **)&s, (char *)next); } else { // insert after s = (struct slice_list *)dlist_add_after((struct double_list **)&TT.slices, (struct double_list **)&s, (char *)next); } } return 0; } // this will not free any memory // will only create more slices depending on position static int cut_str(size_t offset, size_t len) { struct slice_list *e, *s = TT.slices; size_t end = offset+len; size_t epos, spos = 0; if (!s) return -1; //find start and end slices for (;;) { if (spos<=offset && spos+s->node->len>offset) break; spos += s->node->len; s = s->next; if (s == TT.slices) return -1; //error out of bounds } for (e = s, epos = spos; ; ) { if (epos<=end && epos+e->node->len>end) break; epos += e->node->len; e = e->next; if (e == TT.slices) return -1; //error out of bounds } for (;;) { if (spos == offset && ( end >= spos+s->node->len)) { //cut full spos += s->node->len; offset += s->node->len; s = dlist_pop(&s); if (s == TT.slices) TT.slices = s->next; } else if (spos < offset && ( end >= spos+s->node->len)) { //cut end size_t clip = s->node->len - (offset - spos); offset = spos+s->node->len; spos += s->node->len; s->node->len -= clip; } else if (spos == offset && s == e) { //cut begin size_t clip = end - offset; s->node->len -= clip; s->node->data += clip; break; } else { //cut middle struct slice *tail = xmalloc(sizeof(struct slice)); size_t clip = end-offset; tail->len = s->node->len-(offset-spos)-clip; tail->data = s->node->data+(offset-spos)+clip; s->node->len = offset-spos; //wrong? s = (struct slice_list *)dlist_add_after( (struct double_list **)&TT.slices, (struct double_list **)&s, (char *)tail); break; } if (s == e) break; s = s->next; } return 0; } //find offset position in slices static struct slice_list *slice_offset(size_t *start, size_t offset) { struct slice_list *s = TT.slices; size_t spos = 0; //find start for ( ;s ; ) { if (spos<=offset && spos+s->node->len>offset) break; spos += s->node->len; s = s->next; if (s == TT.slices) s = 0; //error out of bounds } if (s) *start = spos; return s; } static size_t text_strchr(size_t offset, char c) { struct slice_list *s = TT.slices; size_t epos, spos = 0; int i = 0; //find start if (!(s = slice_offset(&spos, offset))) return SIZE_MAX; i = offset-spos; epos = spos+i; do { for (; i < s->node->len; i++, epos++) if (s->node->data[i] == c) return epos; s = s->next; i = 0; } while (s != TT.slices); return SIZE_MAX; } static size_t text_strrchr(size_t offset, char c) { struct slice_list *s = TT.slices; size_t epos, spos = 0; int i = 0; //find start if (!(s = slice_offset(&spos, offset))) return SIZE_MAX; i = offset-spos; epos = spos+i; do { for (; i >= 0; i--, epos--) if (s->node->data[i] == c) return epos; s = s->prev; i = s->node->len-1; } while (s != TT.slices->prev); //tail return SIZE_MAX; } static size_t text_filesize() { struct slice_list *s = TT.slices; size_t pos = 0; if (s) do { pos += s->node->len; s = s->next; } while (s != TT.slices); return pos; } static int text_count(size_t start, size_t end, char c) { struct slice_list *s = TT.slices; size_t i, count = 0, spos = 0; if (!(s = slice_offset(&spos, start))) return 0; i = start-spos; if (s) do { for (; i < s->node->len && spos+inode->data[i] == c) count++; if (spos+i>=end) return count; spos += s->node->len; i = 0; s = s->next; } while (s != TT.slices); return count; } static char text_byte(size_t offset) { struct slice_list *s = TT.slices; size_t spos = 0; //find start if (!(s = slice_offset(&spos, offset))) return 0; return s->node->data[offset-spos]; } //utf-8 codepoint -1 if not valid, 0 if out_of_bounds, len if valid //copies data to dest if dest is not 0 static int text_codepoint(char *dest, size_t offset) { char scratch[8] = {0}; int state = 0, finished = 0; for (;!(finished = utf8_dec(text_byte(offset), scratch, &state)); offset++) if (!state) return -1; if (!finished && !state) return -1; if (dest) memcpy(dest, scratch, 8); return strlen(scratch); } static size_t text_sol(size_t offset) { size_t pos; if (!TT.filesize || !offset) return 0; else if (TT.filesize <= offset) return TT.filesize-1; else if ((pos = text_strrchr(offset-1, '\n')) == SIZE_MAX) return 0; else if (pos < offset) return pos+1; return offset; } static size_t text_eol(size_t offset) { if (!TT.filesize) offset = 1; else if (TT.filesize <= offset) return TT.filesize-1; else if ((offset = text_strchr(offset, '\n')) == SIZE_MAX) return TT.filesize-1; return offset; } static size_t text_nsol(size_t offset) { offset = text_eol(offset); if (text_byte(offset) == '\n') offset++; if (offset >= TT.filesize) offset--; return offset; } static size_t text_psol(size_t offset) { offset = text_sol(offset); if (offset) offset--; if (offset && text_byte(offset-1) != '\n') offset = text_sol(offset-1); return offset; } static size_t text_getline(char *dest, size_t offset, size_t max_len) { struct slice_list *s = TT.slices; size_t end, spos = 0; int i, j = 0; if (dest) *dest = 0; if (!s) return 0; if ((end = text_strchr(offset, '\n')) == SIZE_MAX) if ((end = TT.filesize) > offset+max_len) return 0; //find start if (!(s = slice_offset(&spos, offset))) return 0; i = offset-spos; j = end-offset+1; if (dest) do { for (; i < s->node->len && j; i++, j--, dest++) *dest = s->node->data[i]; s = s->next; i = 0; } while (s != TT.slices && j); if (dest) *dest = 0; return end-offset; } //copying is needed when file has lot of inserts that are //just few char long, but not always. Advanced search should //check big slices directly and just copy edge cases. //Also this is only line based search multiline //and regexec should be done instead. static size_t text_strstr(size_t offset, char *str) { size_t bytes, pos = offset; char *s = 0; do { bytes = text_getline(toybuf, pos, ARRAY_LEN(toybuf)); if (!bytes) pos++; //empty line else if ((s = strstr(toybuf, str))) return pos+(s-toybuf); else pos += bytes; } while (pos < TT.filesize); return SIZE_MAX; } static void block_list_free(void *node) { struct block_list *d = node; if (d->node->alloc == HEAP) free((void *)d->node->data); else if (d->node->alloc == MMAP) munmap((void *)d->node->data, d->node->size); free(d->node); free(d); } static void linelist_unload() { llist_traverse((void *)TT.slices, llist_free_double); llist_traverse((void *)TT.text, block_list_free); TT.slices = 0, TT.text = 0; } static int linelist_load(char *filename) { if (!filename) filename = (char*)*toys.optargs; if (filename) { int fd = open(filename, O_RDONLY); size_t size; char *data; if (fd == -1) return 0; data = xmmap(0, size = fdlength(fd), PROT_READ, MAP_SHARED, fd, 0); xclose(fd); insert_str(data, 0, size, size, MMAP); TT.filesize = text_filesize(); } return 1; } static void write_file(char *filename) { struct slice_list *s = TT.slices; struct stat st; int fd = 0; if (!s) return; if (!filename) filename = (char*)*toys.optargs; sprintf(toybuf, "%s.swp", filename); if ( (fd = xopen(toybuf, O_WRONLY | O_CREAT | O_TRUNC)) <0) return; do { xwrite(fd, (void *)s->node->data, s->node->len ); s = s->next; } while (s != TT.slices); linelist_unload(); xclose(fd); if (!stat(filename, &st)) chmod(toybuf, st.st_mode); else chmod(toybuf, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH); xrename(toybuf, filename); linelist_load(filename); } //jump into valid offset index //and valid utf8 codepoint static void check_cursor_bounds() { char buf[8] = {0}; int len, width = 0; if (!TT.filesize) TT.cursor = 0; for (;;) { if (TT.cursor < 1) { TT.cursor = 0; return; } else if (TT.cursor >= TT.filesize-1) { TT.cursor = TT.filesize-1; return; } if ((len = text_codepoint(buf, TT.cursor)) < 1) { TT.cursor--; //we are not in valid data try jump over continue; } if (utf8_lnw(&width, buf, len) && width) break; else TT.cursor--; //combine char jump over } } // TT.vi_mov_flag is used for special cases when certain move // acts differently depending is there DELETE/YANK or NOP // Also commands such as G does not default to count0=1 // 0x1 = Command needs argument (f,F,r...) // 0x2 = Move 1 right on yank/delete/insert (e, $...) // 0x4 = yank/delete last line fully // 0x10000000 = redraw after cursor needed // 0x20000000 = full redraw needed // 0x40000000 = count0 not given // 0x80000000 = move was reverse //TODO rewrite the logic, difficulties counting lines //and with big files scroll should not rely in knowing //absoluteline numbers static void adjust_screen_buffer() { size_t c, s; TT.cur_row = 0, TT.scr_row = 0; if (!TT.cursor) { TT.screen = 0; TT.vi_mov_flag = 0x20000000; return; } else if (TT.screen > (1<<18) || TT.cursor > (1<<18)) { //give up, file is big, do full redraw TT.screen = text_strrchr(TT.cursor-1, '\n')+1; TT.vi_mov_flag = 0x20000000; return; } s = text_count(0, TT.screen, '\n'); c = text_count(0, TT.cursor, '\n'); if (s >= c) { TT.screen = text_strrchr(TT.cursor-1, '\n')+1; s = c; TT.vi_mov_flag = 0x20000000; //TODO I disabled scroll } else { int distance = c-s+1; if (distance > (int)TT.screen_height) { int n, adj = distance-TT.screen_height; TT.vi_mov_flag = 0x20000000; //TODO I disabled scroll for (;adj; adj--, s++) if ((n = text_strchr(TT.screen, '\n'))+1 > TT.screen) TT.screen = n+1; } } TT.scr_row = s; TT.cur_row = c; } //TODO search yank buffer by register //TODO yanks could be separate slices so no need to copy data //now only supports default register static int vi_yank(char reg, size_t from, int flags) { size_t start = from, end = TT.cursor; char *str; memset(TT.yank.data, 0, TT.yank.alloc); if (TT.vi_mov_flag&0x80000000) start = TT.cursor, end = from; else TT.cursor = start; //yank moves cursor to left pos always? if (TT.yank.alloc < end-from) { size_t new_bounds = (1+end-from)/1024; new_bounds += ((1+end-from)%1024) ? 1 : 0; new_bounds *= 1024; TT.yank.data = xrealloc(TT.yank.data, new_bounds); TT.yank.alloc = new_bounds; } //this is naive copy for (str = TT.yank.data ; start 0) TT.cursor += len; else TT.cursor++; for (;TT.cursor < TT.filesize;) { if ((len = text_codepoint(buf, TT.cursor)) < 1) { TT.cursor++; //we are not in valid data try jump over continue; } if (utf8_lnw(&width, buf, len) && width) break; else TT.cursor += len; } } check_cursor_bounds(); return 1; } //TODO column shift static int cur_up(int count0, int count1, char *unused) { int count = count0*count1; for (;count--;) TT.cursor = text_psol(TT.cursor); TT.vi_mov_flag |= 0x80000000; check_cursor_bounds(); return 1; } //TODO column shift static int cur_down(int count0, int count1, char *unused) { int count = count0*count1; for (;count--;) TT.cursor = text_nsol(TT.cursor); check_cursor_bounds(); return 1; } static int vi_H(int count0, int count1, char *unused) { TT.cursor = text_sol(TT.screen); return 1; } static int vi_L(int count0, int count1, char *unused) { TT.cursor = text_sol(TT.screen); cur_down(TT.screen_height-1, 1, 0); return 1; } static int vi_M(int count0, int count1, char *unused) { TT.cursor = text_sol(TT.screen); cur_down(TT.screen_height/2, 1, 0); return 1; } static int search_str(char *s) { size_t pos = text_strstr(TT.cursor+1, s); if (TT.last_search != s) { free(TT.last_search); TT.last_search = xstrdup(s); } if (pos != SIZE_MAX) TT.cursor = pos; check_cursor_bounds(); return 0; } static int vi_yy(char reg, int count0, int count1) { size_t history = TT.cursor; size_t pos = text_sol(TT.cursor); //go left to first char on line TT.vi_mov_flag |= 0x4; for (;count0; count0--) TT.cursor = text_nsol(TT.cursor); vi_yank(reg, pos, 0); TT.cursor = history; return 1; } static int vi_dd(char reg, int count0, int count1) { size_t pos = text_sol(TT.cursor); //go left to first char on line TT.vi_mov_flag |= 0x30000000; for (;count0; count0--) TT.cursor = text_nsol(TT.cursor); if (pos == TT.cursor && TT.filesize) pos--; vi_delete(reg, pos, 0); check_cursor_bounds(); return 1; } static int vi_x(char reg, int count0, int count1) { size_t from = TT.cursor; if (text_byte(TT.cursor) == '\n') { cur_left(count0-1, 1, 0); } else { cur_right(count0-1, 1, 0); if (text_byte(TT.cursor) == '\n') TT.vi_mov_flag |= 2; else cur_right(1, 1, 0); } vi_delete(reg, from, 0); check_cursor_bounds(); return 1; } static int vi_movw(int count0, int count1, char *unused) { int count = count0*count1; while (count--) { char c = text_byte(TT.cursor); do { if (TT.cursor > TT.filesize-1) break; //if at empty jump to non empty if (c == '\n') { if (++TT.cursor > TT.filesize-1) break; if ((c = text_byte(TT.cursor)) == '\n') break; continue; } else if (strchr(blank, c)) do { if (++TT.cursor > TT.filesize-1) break; c = text_byte(TT.cursor); } while (strchr(blank, c)); //if at special jump to non special else if (strchr(specials, c)) do { if (++TT.cursor > TT.filesize-1) break; c = text_byte(TT.cursor); } while (strchr(specials, c)); //else jump to empty or spesial else do { if (++TT.cursor > TT.filesize-1) break; c = text_byte(TT.cursor); } while (c && !strchr(blank, c) && !strchr(specials, c)); } while (strchr(blank, c) && c != '\n'); //never stop at empty } check_cursor_bounds(); return 1; } static int vi_movb(int count0, int count1, char *unused) { int count = count0*count1; int type = 0; char c; while (count--) { c = text_byte(TT.cursor); do { if (!TT.cursor) break; //if at empty jump to non empty if (strchr(blank, c)) do { if (!--TT.cursor) break; c = text_byte(TT.cursor); } while (strchr(blank, c)); //if at special jump to non special else if (strchr(specials, c)) do { if (!--TT.cursor) break; type = 0; c = text_byte(TT.cursor); } while (strchr(specials, c)); //else jump to empty or spesial else do { if (!--TT.cursor) break; type = 1; c = text_byte(TT.cursor); } while (!strchr(blank, c) && !strchr(specials, c)); } while (strchr(blank, c)); //never stop at empty } //find first for (;TT.cursor; TT.cursor--) { c = text_byte(TT.cursor-1); if (type && !strchr(blank, c) && !strchr(specials, c)) break; else if (!type && !strchr(specials, c)) break; } TT.vi_mov_flag |= 0x80000000; check_cursor_bounds(); return 1; } static int vi_move(int count0, int count1, char *unused) { int count = count0*count1; int type = 0; char c; if (count>1) vi_movw(count-1, 1, unused); c = text_byte(TT.cursor); if (strchr(specials, c)) type = 1; TT.cursor++; for (;TT.cursor < TT.filesize-1; TT.cursor++) { c = text_byte(TT.cursor+1); if (!type && (strchr(blank, c) || strchr(specials, c))) break; else if (type && !strchr(specials, c)) break; } TT.vi_mov_flag |= 2; check_cursor_bounds(); return 1; } static void i_insert(char *str, int len) { if (!str || !len) return; insert_str(xstrdup(str), TT.cursor, len, len, HEAP); TT.cursor += len; TT.filesize = text_filesize(); TT.vi_mov_flag |= 0x30000000; } static int vi_zero(int count0, int count1, char *unused) { TT.cursor = text_sol(TT.cursor); TT.cur_col = 0; TT.vi_mov_flag |= 0x80000000; return 1; } static int vi_dollar(int count0, int count1, char *unused) { size_t new = text_strchr(TT.cursor, '\n'); if (new != TT.cursor) { TT.cursor = new - 1; TT.vi_mov_flag |= 2; check_cursor_bounds(); } return 1; } static void vi_eol() { TT.cursor = text_strchr(TT.cursor, '\n'); check_cursor_bounds(); } static void ctrl_b() { int i; for (i=0; i TT.cursor) TT.cursor = TT.screen; } static void ctrl_e() { TT.screen = text_nsol(TT.screen); // TODO: real vi keeps the x position. if (TT.screen > TT.cursor) TT.cursor = TT.screen; } static void ctrl_y() { TT.screen = text_psol(TT.screen); // TODO: only if we're on the bottom line TT.cursor = text_psol(TT.cursor); // TODO: real vi keeps the x position. } //TODO check register where to push from static int vi_push(char reg, int count0, int count1) { //if row changes during push original cursor position is kept //vi inconsistancy //if yank ends with \n push is linemode else push in place+1 size_t history = TT.cursor; char *start = TT.yank.data; char *eol = strchr(start, '\n'); if (start[strlen(start)-1] == '\n') { if ((TT.cursor = text_strchr(TT.cursor, '\n')) == SIZE_MAX) TT.cursor = TT.filesize; else TT.cursor = text_nsol(TT.cursor); } else cur_right(1, 1, 0); i_insert(start, strlen(start)); if (eol) { TT.vi_mov_flag |= 0x10000000; TT.cursor = history; } return 1; } static int vi_find_c(int count0, int count1, char *symbol) { //// int count = count0*count1; size_t pos = text_strchr(TT.cursor, *symbol); if (pos != SIZE_MAX) TT.cursor = pos; return 1; } static int vi_find_cb(int count0, int count1, char *symbol) { //do backward search size_t pos = text_strrchr(TT.cursor, *symbol); if (pos != SIZE_MAX) TT.cursor = pos; return 1; } //if count is not spesified should go to last line static int vi_go(int count0, int count1, char *symbol) { size_t prev_cursor = TT.cursor; int count = count0*count1-1; TT.cursor = 0; if (TT.vi_mov_flag&0x40000000 && (TT.cursor = TT.filesize) > 0) TT.cursor = text_sol(TT.cursor-1); else if (count) { size_t next = 0; for ( ;count && (next = text_strchr(next+1, '\n')) != SIZE_MAX; count--) TT.cursor = next; TT.cursor++; } check_cursor_bounds(); //adjusts cursor column if (prev_cursor > TT.cursor) TT.vi_mov_flag |= 0x80000000; return 1; } static int vi_o(char reg, int count0, int count1) { TT.cursor = text_eol(TT.cursor); insert_str(xstrdup("\n"), TT.cursor++, 1, 1, HEAP); TT.vi_mov_flag |= 0x30000000; TT.vi_mode = 2; return 1; } static int vi_O(char reg, int count0, int count1) { TT.cursor = text_psol(TT.cursor); return vi_o(reg, count0, count1); } static int vi_D(char reg, int count0, int count1) { size_t pos = TT.cursor; if (!count0) return 1; vi_eol(); vi_delete(reg, pos, 0); if (--count0) vi_dd(reg, count0, 1); check_cursor_bounds(); return 1; } static int vi_I(char reg, int count0, int count1) { TT.cursor = text_sol(TT.cursor); TT.vi_mode = 2; return 1; } static int vi_join(char reg, int count0, int count1) { size_t next; while (count0--) { //just strchr(/n) and cut_str(pos, 1); if ((next = text_strchr(TT.cursor, '\n')) == SIZE_MAX) break; TT.cursor = next+1; vi_delete(reg, TT.cursor-1, 0); } return 1; } static int vi_find_next(char reg, int count0, int count1) { if (TT.last_search) search_str(TT.last_search); return 1; } //NOTES //vi-mode cmd syntax is //("[REG])[COUNT0]CMD[COUNT1](MOV) //where: //------------------------------------------------------------- //"[REG] is optional buffer where deleted/yanked text goes REG can be // atleast 0-9, a-z or default " //[COUNT] is optional multiplier for cmd execution if there is 2 COUNT // operations they are multiplied together //CMD is operation to be executed //(MOV) is movement operation, some CMD does not require MOV and some // have special cases such as dd, yy, also movements can work without // CMD //ex commands can be even more complicated than this.... // struct vi_cmd_param { const char* cmd; unsigned flags; int (*vi_cmd)(char, size_t, int);//REG,from,FLAGS }; struct vi_mov_param { const char* mov; unsigned flags; int (*vi_mov)(int, int, char*);//COUNT0,COUNT1,params }; //special cases without MOV and such struct vi_special_param { const char *cmd; int (*vi_special)(char, int, int);//REG,COUNT0,COUNT1 }; struct vi_special_param vi_special[] = { {"D", &vi_D}, {"I", &vi_I}, {"J", &vi_join}, {"O", &vi_O}, {"n", &vi_find_next}, {"o", &vi_o}, {"p", &vi_push}, {"x", &vi_x}, {"dd", &vi_dd}, {"yy", &vi_yy}, }; //there is around ~47 vi moves //some of them need extra params //such as f and ' struct vi_mov_param vi_movs[] = { {"0", 0, &vi_zero}, {"b", 0, &vi_movb}, {"e", 0, &vi_move}, {"G", 0, &vi_go}, {"H", 0, &vi_H}, {"h", 0, &cur_left}, {"j", 0, &cur_down}, {"k", 0, &cur_up}, {"L", 0, &vi_L}, {"l", 0, &cur_right}, {"M", 0, &vi_M}, {"w", 0, &vi_movw}, {"$", 0, &vi_dollar}, {"f", 1, &vi_find_c}, {"F", 1, &vi_find_cb}, }; //change and delete unfortunately behave different depending on move command, //such as ce cw are same, but dw and de are not... //also dw stops at w position and cw seem to stop at e pos+1... //so after movement we need to possibly set up some flags before executing //command, and command needs to adjust... struct vi_cmd_param vi_cmds[] = { {"c", 1, &vi_change}, {"d", 1, &vi_delete}, {"y", 1, &vi_yank}, }; static int run_vi_cmd(char *cmd) { int i = 0, val = 0; char *cmd_e; int (*vi_cmd)(char, size_t, int) = 0; int (*vi_mov)(int, int, char*) = 0; TT.count0 = 0, TT.count1 = 0, TT.vi_mov_flag = 0; TT.vi_reg = '"'; if (*cmd == '"') { cmd++; TT.vi_reg = *cmd; //TODO check validity cmd++; } errno = 0; val = strtol(cmd, &cmd_e, 10); if (errno || val == 0) val = 1, TT.vi_mov_flag |= 0x40000000; else cmd = cmd_e; TT.count0 = val; for (i = 0; i < ARRAY_LEN(vi_special); i++) { if (strstr(cmd, vi_special[i].cmd)) { return vi_special[i].vi_special(TT.vi_reg, TT.count0, TT.count1); } } for (i = 0; i < ARRAY_LEN(vi_cmds); i++) { if (!strncmp(cmd, vi_cmds[i].cmd, strlen(vi_cmds[i].cmd))) { vi_cmd = vi_cmds[i].vi_cmd; cmd += strlen(vi_cmds[i].cmd); break; } } errno = 0; val = strtol(cmd, &cmd_e, 10); if (errno || val == 0) val = 1; else cmd = cmd_e; TT.count1 = val; for (i = 0; i < ARRAY_LEN(vi_movs); i++) { if (!strncmp(cmd, vi_movs[i].mov, strlen(vi_movs[i].mov))) { vi_mov = vi_movs[i].vi_mov; TT.vi_mov_flag |= vi_movs[i].flags; cmd++; if (TT.vi_mov_flag&1 && !(*cmd)) return 0; break; } } if (vi_mov) { int prev_cursor = TT.cursor; if (vi_mov(TT.count0, TT.count1, cmd)) { if (vi_cmd) return (vi_cmd(TT.vi_reg, prev_cursor, TT.vi_mov_flag)); else return 1; } else return 0; //return some error } return 0; } static int run_ex_cmd(char *cmd) { if (cmd[0] == '/') { search_str(&cmd[1]); } else if (cmd[0] == '?') { // TODO: backwards search. } else if (cmd[0] == ':') { if (!strcmp(&cmd[1], "q") || !strcmp(&cmd[1], "q!")) { // TODO: if no !, check whether file modified. //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; } else if (strstr(&cmd[1], "set list")) { TT.list = 1; TT.vi_mov_flag |= 0x30000000; return 1; } else if (strstr(&cmd[1], "set nolist")) { TT.list = 0; TT.vi_mov_flag |= 0x30000000; return 1; } } return 0; } static int vi_crunch(FILE *out, int cols, int wc) { int ret = 0; if (wc < 32 && TT.list) { tty_esc("1m"); ret = crunch_escape(out,cols,wc); tty_esc("m"); } else if (wc == 0x09) { if (out) { int i = TT.tabstop; for (;i--;) fputs(" ", out); } ret = TT.tabstop; } else if (wc == '\n') return 0; return ret; } //crunch_str with n bytes restriction for printing substrings or //non null terminated strings static int crunch_nstr(char **str, int width, int n, FILE *out, char *escmore, int (*escout)(FILE *out, int cols, int wc)) { int columns = 0, col, bytes; char *start, *end; unsigned wc; for (end = start = *str; *end && n>0; columns += col, end += bytes, n -= bytes) { if ((bytes = utf8towc(&wc, end, 4))>0 && (col = wcwidth(wc))>=0) { if (!escmore || wc>255 || !strchr(escmore, wc)) { if (width-columns>28; scroll = TT.drawn_row-TT.scr_row; if (TT.drawn_row<0 || TT.cur_row<0 || TT.scr_row<0) redraw = 3; else if (abs(scroll)>TT.screen_height/2) redraw = 3; tty_jump(0, 0); if (redraw&2) tty_esc("2J"), tty_esc("H"); //clear screen else if (scroll>0) printf("\033[%dL", scroll); //scroll up else if (scroll<0) printf("\033[%dM", -scroll); //scroll down SOL = text_sol(TT.cursor); bytes = text_getline(toybuf, SOL, ARRAY_LEN(toybuf)); line = toybuf; for (SSOL = TT.screen, y = 0; SSOL < SOL; y++) SSOL = text_nsol(SSOL); cy_scr = y; //draw cursor row ///////////////////////////////////////////////////////////// //for long lines line starts to scroll when cursor hits margin bytes = TT.cursor-SOL; // TT.cur_col; end = line; tty_jump(0, y); tty_esc("2K"); //find cursor position aw = crunch_nstr(&end, INT_MAX, bytes, 0, "\t\n", vi_crunch); //if we need to render text that is not inserted to buffer yet if (TT.vi_mode == 2 && TT.il->len) { char* iend = TT.il->data; //input end x = 0; //find insert end position iw = crunch_str(&iend, INT_MAX, 0, "\t\n", vi_crunch); clip = (aw+iw) - TT.screen_width+margin; //if clipped area is bigger than text before insert if (clip > aw) { clip -= aw; iend = TT.il->data; iw -= crunch_str(&iend, clip, 0, "\t\n", vi_crunch); x = crunch_str(&iend, iw, stdout, "\t\n", vi_crunch); } else { iend = TT.il->data; end = line; //if clipped area is substring from cursor row start aw -= crunch_nstr(&end, clip, bytes, 0, "\t\n", vi_crunch); x = crunch_str(&end, aw, stdout, "\t\n", vi_crunch); x += crunch_str(&iend, iw, stdout, "\t\n", vi_crunch); } } //when not inserting but still need to keep cursor inside screen //margin area else if ( aw+margin > TT.screen_width) { clip = aw-TT.screen_width+margin; end = line; aw -= crunch_nstr(&end, clip, bytes, 0, "\t\n", vi_crunch); x = crunch_str(&end, aw, stdout, "\t\n", vi_crunch); } else { end = line; x = crunch_nstr(&end, aw, bytes, stdout, "\t\n", vi_crunch); } cx_scr = x; cy_scr = y; x += crunch_str(&end, TT.screen_width-x, stdout, "\t\n", vi_crunch); //start drawing all other rows that needs update /////////////////////////////////////////////////////////////////// y = 0, SSOL = TT.screen, line = toybuf; bytes = text_getline(toybuf, SSOL, ARRAY_LEN(toybuf)); //if we moved around in long line might need to redraw everything if (clip != TT.drawn_col) redraw = 3; for (; y < TT.screen_height; y++ ) { int draw_line = 0; if (SSOL == SOL) { line = toybuf; SSOL += bytes+1; bytes = text_getline(line, SSOL, ARRAY_LEN(toybuf)); continue; } else if (redraw) draw_line++; else if (scroll<0 && TT.screen_height-y-1<-scroll) scroll++, draw_line++; else if (scroll>0) scroll--, draw_line++; tty_jump(0, y); if (draw_line) { tty_esc("2K"); if (line && strlen(line)) { aw = crunch_nstr(&line, clip, bytes, 0, "\t\n", vi_crunch); crunch_str(&line, TT.screen_width-1, stdout, "\t\n", vi_crunch); if ( *line ) printf("@"); } else printf("\033[2m~\033[m"); } if (SSOL+bytes < TT.filesize) { line = toybuf; SSOL += bytes+1; bytes = text_getline(line, SSOL, ARRAY_LEN(toybuf)); } else line = 0; } TT.drawn_row = TT.scr_row, TT.drawn_col = clip; // Finished updating visual area, show status line. tty_jump(0, TT.screen_height); tty_esc("2K"); if (TT.vi_mode == 2) printf("\033[1m-- INSERT --\033[m"); if (!TT.vi_mode) { cx_scr = printf("%s", TT.il->data); cy_scr = TT.screen_height; *toybuf = 0; } else { // TODO: the row,col display doesn't show the cursor column // TODO: real vi shows the percentage by lines, not bytes sprintf(toybuf, "%zu/%zuC %zu%% %d,%d", TT.cursor, TT.filesize, (100*TT.cursor)/TT.filesize, TT.cur_row+1, TT.cur_col+1); if (TT.cur_col != cx_scr) sprintf(toybuf+strlen(toybuf),"-%d", cx_scr+1); } tty_jump(TT.screen_width-strlen(toybuf), TT.screen_height); printf("%s", toybuf); tty_jump(cx_scr, cy_scr); xflush(1); } void vi_main(void) { char stdout_buf[BUFSIZ]; char keybuf[16] = {0}; char vi_buf[16] = {0}; char utf8_code[8] = {0}; int utf8_dec_p = 0, vi_buf_pos = 0; FILE *script = FLAG(s) ? xfopen(TT.s, "r") : 0; TT.il = xzalloc(sizeof(struct str_line)); TT.il->data = xzalloc(80); TT.yank.data = xzalloc(128); TT.il->alloc = 80, TT.yank.alloc = 128; linelist_load(0); TT.screen = TT.cursor = 0; TT.vi_mov_flag = 0x20000000; TT.vi_mode = 1, TT.tabstop = 8; TT.screen_width = 80, TT.screen_height = 24; terminal_size(&TT.screen_width, &TT.screen_height); TT.screen_height -= 1; // Avoid flicker. setbuf(stdout, stdout_buf); xsignal(SIGWINCH, generic_signal); 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"); for (;;) { int key = 0; draw_page(); if (script) { key = fgetc(script); if (key == EOF) { fclose(script); script = 0; key = scan_key(keybuf, -1); } } else key = scan_key(keybuf, -1); if (key == -1) goto cleanup_vi; else if (key == -3) { toys.signal = 0; terminal_size(&TT.screen_width, &TT.screen_height); TT.screen_height -= 1; //TODO this is hack fix visual alignment continue; } // TODO: support cursor keys in ex mode too. if (TT.vi_mode && key>=256) { key -= 256; if (key==KEY_UP) cur_up(1, 1, 0); else if (key==KEY_DOWN) cur_down(1, 1, 0); else if (key==KEY_LEFT) cur_left(1, 1, 0); else if (key==KEY_RIGHT) cur_right(1, 1, 0); else if (key==KEY_HOME) vi_zero(1, 1, 0); else if (key==KEY_END) vi_dollar(1, 1, 0); else if (key==KEY_PGDN) ctrl_f(); else if (key==KEY_PGUP) ctrl_b(); continue; } if (TT.vi_mode == 1) { //NORMAL switch (key) { case '/': case '?': case ':': TT.vi_mode = 0; TT.il->data[0]=key; TT.il->len++; break; case 'A': vi_eol(); TT.vi_mode = 2; break; case 'a': cur_right(1, 1, 0); // FALLTHROUGH case 'i': TT.vi_mode = 2; break; case 'B'-'@': ctrl_b(); break; case 'E'-'@': ctrl_e(); break; case 'F'-'@': ctrl_f(); break; case 'Y'-'@': ctrl_y(); break; case 27: vi_buf[0] = 0; vi_buf_pos = 0; break; default: if (key > 0x20 && key < 0x7B) { vi_buf[vi_buf_pos] = key;//TODO handle input better 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; memset(vi_buf, 0, 16); } } break; } } else if (TT.vi_mode == 0) { //EX MODE switch (key) { case 0x7F: case 0x08: if (TT.il->len > 1) { TT.il->data[--TT.il->len] = 0; break; } // FALLTHROUGH case 27: TT.vi_mode = 1; TT.il->len = 0; memset(TT.il->data, 0, TT.il->alloc); break; case 0x0A: case 0x0D: if (run_ex_cmd(TT.il->data) == -1) goto cleanup_vi; TT.vi_mode = 1; TT.il->len = 0; memset(TT.il->data, 0, TT.il->alloc); break; default: //add chars to ex command until ENTER if (key >= 0x20 && key < 0x7F) { //might be utf? if (TT.il->len == TT.il->alloc) { TT.il->data = realloc(TT.il->data, TT.il->alloc*2); TT.il->alloc *= 2; } TT.il->data[TT.il->len] = key; TT.il->len++; } break; } } else if (TT.vi_mode == 2) {//INSERT MODE switch (key) { case 27: i_insert(TT.il->data, TT.il->len); cur_left(1, 1, 0); TT.vi_mode = 1; TT.il->len = 0; memset(TT.il->data, 0, TT.il->alloc); break; case 0x7F: case 0x08: if (TT.il->len) { char *last = utf8_last(TT.il->data, TT.il->len); int shrink = strlen(last); memset(last, 0, shrink); TT.il->len -= shrink; } break; case 0x0A: case 0x0D: //insert newline // TT.il->data[TT.il->len++] = '\n'; i_insert(TT.il->data, TT.il->len); TT.il->len = 0; memset(TT.il->data, 0, TT.il->alloc); break; default: if ((key >= 0x20 || key == 0x09) && utf8_dec(key, utf8_code, &utf8_dec_p)) { if (TT.il->len+utf8_dec_p+1 >= TT.il->alloc) { TT.il->data = realloc(TT.il->data, TT.il->alloc*2); TT.il->alloc *= 2; } strcpy(TT.il->data+TT.il->len, utf8_code); TT.il->len += utf8_dec_p; utf8_dec_p = 0; *utf8_code = 0; } break; } } } cleanup_vi: linelist_unload(); free(TT.il->data), free(TT.il), free(TT.yank.data); tty_reset(); tty_esc("?1049l"); }