/* $OpenBSD: vi.c,v 1.60 2021/03/12 02:10:25 millert Exp $ */ /* * vi command editing * written by John Rochester (initially for nsh) * bludgeoned to fit pdksh by Larry Bouzane, Jeff Sparkes & Eric Gisin * */ #include "config.h" #ifdef VI #include /* completion */ #include #include #include #ifndef SMALL # include # include #endif #include "sh.h" #include "edit.h" #undef CTRL #define CTRL(x) ((x) & 0x1F) /* ASCII */ struct edstate { char *cbuf; /* main buffer to build the command line */ int cbufsize; /* number of bytes allocated for cbuf */ int linelen; /* current number of bytes in cbuf */ int winleft; /* first byte# in cbuf to be displayed */ int cursor; /* byte# in cbuf having the cursor */ }; static int vi_hook(int); static void vi_reset(char *, size_t); static int nextstate(int); static int vi_insert(int); static int vi_cmd(int, const char *); static int domove(int, const char *, int); static int redo_insert(int); static void yank_range(int, int); static int bracktype(int); static void save_cbuf(void); static void restore_cbuf(void); static void edit_reset(char *, size_t); static int putbuf(const char *, int, int); static void del_range(int, int); static int findch(int, int, int, int); static int forwword(int); static int backword(int); static int endword(int); static int Forwword(int); static int Backword(int); static int Endword(int); static int grabhist(int, int); static int grabsearch(int, int, int, char *); static void do_clear_screen(void); static void redraw_line(int, int); static void refresh_line(int); static int outofwin(void); static void rewindow(void); static int newcol(int, int); static void display(char *, char *, int); static void ed_mov_opt(int, char *); static int expand_word(int); static int complete_word(int, int); static int print_expansions(struct edstate *); static int char_len(int); static void x_vi_zotc(int); static void vi_pprompt(int); static void vi_error(void); static void vi_macro_reset(void); static int x_vi_putbuf(const char *, size_t); static int isu8cont(unsigned char); #define C_ 0x1 /* a valid command that isn't a M_, E_, U_ */ #define M_ 0x2 /* movement command (h, l, etc.) */ #define E_ 0x4 /* extended command (c, d, y) */ #define X_ 0x8 /* long command (@, f, F, t, T, etc.) */ #define U_ 0x10 /* an UN-undoable command (that isn't a M_) */ #define B_ 0x20 /* bad command (^@) */ #define Z_ 0x40 /* repeat count defaults to 0 (not 1) */ #define S_ 0x80 /* search (/, ?) */ #define is_bad(c) (classify[(c)&0x7f]&B_) #define is_cmd(c) (classify[(c)&0x7f]&(M_|E_|C_|U_)) #define is_move(c) (classify[(c)&0x7f]&M_) #define is_extend(c) (classify[(c)&0x7f]&E_) #define is_long(c) (classify[(c)&0x7f]&X_) #define is_undoable(c) (!(classify[(c)&0x7f]&U_)) #define is_srch(c) (classify[(c)&0x7f]&S_) #define is_zerocount(c) (classify[(c)&0x7f]&Z_) const unsigned char classify[128] = { /* 0 1 2 3 4 5 6 7 */ /* 0 ^@ ^A ^B ^C ^D ^E ^F ^G */ B_, 0, 0, 0, 0, C_|U_, C_|Z_, 0, /* 01 ^H ^I ^J ^K ^L ^M ^N ^O */ M_, C_|Z_, 0, 0, C_|U_, 0, C_, 0, /* 02 ^P ^Q ^R ^S ^T ^U ^V ^W */ C_, 0, C_|U_, 0, 0, 0, C_, 0, /* 03 ^X ^Y ^Z ^[ ^\ ^] ^^ ^_ */ C_, 0, 0, C_|Z_, 0, 0, 0, 0, /* 04 ! " # $ % & ' */ M_, 0, 0, C_, M_, M_, 0, 0, /* 05 ( ) * + , - . / */ 0, 0, C_, C_, M_, C_, 0, C_|S_, /* 06 0 1 2 3 4 5 6 7 */ M_, 0, 0, 0, 0, 0, 0, 0, /* 07 8 9 : ; < = > ? */ 0, 0, 0, M_, 0, C_, 0, C_|S_, /* 010 @ A B C D E F G */ C_|X_, C_, M_, C_, C_, M_, M_|X_, C_|U_|Z_, /* 011 H I J K L M N O */ 0, C_, 0, 0, 0, 0, C_|U_, 0, /* 012 P Q R S T U V W */ C_, 0, C_, C_, M_|X_, C_, 0, M_, /* 013 X Y Z [ \ ] ^ _ */ C_, C_|U_, 0, 0, C_|Z_, 0, M_, C_|Z_, /* 014 ` a b c d e f g */ 0, C_, M_, E_, E_, M_, M_|X_, C_|Z_, /* 015 h i j k l m n o */ M_, C_, C_|U_, C_|U_, M_, 0, C_|U_, 0, /* 016 p q r s t u v w */ C_, 0, X_, C_, M_|X_, C_|U_, C_|U_|Z_,M_, /* 017 x y z { | } ~ ^? */ C_, E_|U_, 0, 0, M_|Z_, 0, C_, 0 }; #define MAXVICMD 3 #define SRCHLEN 40 #define INSERT 1 #define REPLACE 2 #define VNORMAL 0 /* command, insert or replace mode */ #define VARG1 1 /* digit prefix (first, eg, 5l) */ #define VEXTCMD 2 /* cmd + movement (eg, cl) */ #define VARG2 3 /* digit prefix (second, eg, 2c3l) */ #define VXCH 4 /* f, F, t, T, @ */ #define VFAIL 5 /* bad command */ #define VCMD 6 /* single char command (eg, X) */ #define VREDO 7 /* . */ #define VLIT 8 /* ^V */ #define VSEARCH 9 /* /, ? */ static char undocbuf[LINE]; static struct edstate *save_edstate(struct edstate *old); static void restore_edstate(struct edstate *old, struct edstate *new); static void free_edstate(struct edstate *old); static struct edstate ebuf; static struct edstate undobuf = { undocbuf, LINE, 0, 0, 0 }; static struct edstate *es; /* current editor state */ static struct edstate *undo; static char ibuf[LINE]; /* input buffer */ static int first_insert; /* set when starting in insert mode */ static int saved_inslen; /* saved inslen for first insert */ static int inslen; /* length of input buffer */ static int srchlen; /* number of bytes in search pattern */ static char ybuf[LINE]; /* yank buffer */ static int yanklen; /* length of yank buffer */ static int fsavecmd = ' '; /* last find command */ static int fsavech; /* character to find */ static char lastcmd[MAXVICMD]; /* last non-move command */ static int lastac; /* argcnt for lastcmd */ static int lastsearch = ' '; /* last search command */ static char srchpat[SRCHLEN]; /* last search pattern */ static int insert; /* mode: INSERT, REPLACE, or 0 */ static int hnum; /* position in history */ static int ohnum; /* history line copied (after mod) */ static int hlast; /* 1 past last position in history */ static int modified; /* buffer has been "modified" */ static int state; /* Information for keeping track of macros that are being expanded. * The format of buf is the alias contents followed by a null byte followed * by the name (letter) of the alias. The end of the buffer is marked by * a double null. The name of the alias is stored so recursive macros can * be detected. */ struct macro_state { unsigned char *p; /* current position in buf */ unsigned char *buf; /* pointer to macro(s) being expanded */ int len; /* how much data in buffer */ }; static struct macro_state macro; enum expand_mode { NONE, EXPAND, COMPLETE, PRINT }; static enum expand_mode expanded = NONE;/* last input was expanded */ int x_vi(char *buf, size_t len) { int c; vi_reset(buf, len > LINE ? LINE : len); vi_pprompt(1); x_flush(); while (1) { if (macro.p) { c = (unsigned char)*macro.p++; /* end of current macro? */ if (!c) { /* more macros left to finish? */ if (*macro.p++) continue; /* must be the end of all the macros */ vi_macro_reset(); c = x_getc(); } } else c = x_getc(); if (c == -1) break; if (state != VLIT) { if (c == edchars.intr || c == edchars.quit) { /* pretend we got an interrupt */ x_vi_zotc(c); x_flush(); trapsig(c == edchars.intr ? SIGINT : SIGQUIT); x_mode(false); unwind(LSHELL); } else if (c == edchars.eof) { if (es->linelen == 0) { x_vi_zotc(edchars.eof); c = -1; break; } continue; } } if (vi_hook(c)) break; x_flush(); } x_putc('\r'); x_putc('\n'); x_flush(); if (c == -1 || len <= (size_t)es->linelen) return -1; if (es->cbuf != buf) memmove(buf, es->cbuf, es->linelen); buf[es->linelen++] = '\n'; return es->linelen; } static int vi_hook(int ch) { static char curcmd[MAXVICMD], locpat[SRCHLEN]; static int cmdlen, argc1, argc2; switch (state) { case VNORMAL: if (insert != 0) { if (ch == CTRL('v')) { state = VLIT; ch = '^'; } switch (vi_insert(ch)) { case -1: vi_error(); state = VNORMAL; break; case 0: if (state == VLIT) { es->cursor--; refresh_line(0); } else refresh_line(insert != 0); break; case 1: return 1; } } else { if (ch == '\r' || ch == '\n') return 1; cmdlen = 0; argc1 = 0; if (ch >= '1' && ch <= '9') { argc1 = ch - '0'; state = VARG1; } else { curcmd[cmdlen++] = ch; state = nextstate(ch); if (state == VSEARCH) { save_cbuf(); es->cursor = 0; es->linelen = 0; if (ch == '/') { if (putbuf("/", 1, 0) != 0) return -1; } else if (putbuf("?", 1, 0) != 0) return -1; refresh_line(0); } } } break; case VLIT: if (is_bad(ch)) { del_range(es->cursor, es->cursor + 1); vi_error(); } else es->cbuf[es->cursor++] = ch; refresh_line(1); state = VNORMAL; break; case VARG1: if (isdigit(ch)) argc1 = argc1 * 10 + ch - '0'; else { curcmd[cmdlen++] = ch; state = nextstate(ch); } break; case VEXTCMD: argc2 = 0; if (ch >= '1' && ch <= '9') { argc2 = ch - '0'; state = VARG2; return 0; } else { curcmd[cmdlen++] = ch; if (ch == curcmd[0]) state = VCMD; else if (is_move(ch)) state = nextstate(ch); else state = VFAIL; } break; case VARG2: if (isdigit(ch)) argc2 = argc2 * 10 + ch - '0'; else { if (argc1 == 0) argc1 = argc2; else argc1 *= argc2; curcmd[cmdlen++] = ch; if (ch == curcmd[0]) state = VCMD; else if (is_move(ch)) state = nextstate(ch); else state = VFAIL; } break; case VXCH: if (ch == CTRL('[')) state = VNORMAL; else { curcmd[cmdlen++] = ch; state = VCMD; } break; case VSEARCH: if (ch == '\r' || ch == '\n' /*|| ch == CTRL('[')*/ ) { restore_cbuf(); /* Repeat last search? */ if (srchlen == 0) { if (!srchpat[0]) { vi_error(); state = VNORMAL; refresh_line(0); return 0; } } else { locpat[srchlen] = '\0'; (void) strlcpy(srchpat, locpat, sizeof srchpat); } state = VCMD; } else if (ch == edchars.erase || ch == CTRL('h')) { if (srchlen != 0) { do { srchlen--; es->linelen -= char_len( (unsigned char)locpat[srchlen]); } while (srchlen > 0 && isu8cont(locpat[srchlen])); es->cursor = es->linelen; refresh_line(0); return 0; } restore_cbuf(); state = VNORMAL; refresh_line(0); } else if (ch == edchars.kill) { srchlen = 0; es->linelen = 1; es->cursor = 1; refresh_line(0); return 0; } else if (ch == edchars.werase) { struct edstate new_es, *save_es; int i; int n = srchlen; new_es.cursor = n; new_es.cbuf = locpat; save_es = es; es = &new_es; n = backword(1); es = save_es; for (i = srchlen; --i >= n; ) es->linelen -= char_len((unsigned char)locpat[i]); srchlen = n; es->cursor = es->linelen; refresh_line(0); return 0; } else { if (srchlen == SRCHLEN - 1) vi_error(); else { locpat[srchlen++] = ch; if ((ch & 0x80) && Flag(FVISHOW8)) { if (es->linelen + 2 > es->cbufsize) vi_error(); es->cbuf[es->linelen++] = 'M'; es->cbuf[es->linelen++] = '-'; ch &= 0x7f; } if (ch < ' ' || ch == 0x7f) { if (es->linelen + 2 > es->cbufsize) vi_error(); es->cbuf[es->linelen++] = '^'; es->cbuf[es->linelen++] = ch ^ '@'; } else { if (es->linelen >= es->cbufsize) vi_error(); es->cbuf[es->linelen++] = ch; } es->cursor = es->linelen; refresh_line(0); } return 0; } break; } switch (state) { case VCMD: state = VNORMAL; switch (vi_cmd(argc1, curcmd)) { case -1: vi_error(); refresh_line(0); break; case 0: if (insert != 0) inslen = 0; refresh_line(insert != 0); break; case 1: refresh_line(0); return 1; case 2: /* back from a 'v' command - don't redraw the screen */ return 1; } break; case VREDO: state = VNORMAL; if (argc1 != 0) lastac = argc1; switch (vi_cmd(lastac, lastcmd)) { case -1: vi_error(); refresh_line(0); break; case 0: if (insert != 0) { if (lastcmd[0] == 's' || lastcmd[0] == 'c' || lastcmd[0] == 'C') { if (redo_insert(1) != 0) vi_error(); } else { if (redo_insert(lastac) != 0) vi_error(); } } refresh_line(0); break; case 1: refresh_line(0); return 1; case 2: /* back from a 'v' command - can't happen */ break; } break; case VFAIL: state = VNORMAL; vi_error(); break; } return 0; } static void vi_reset(char *buf, size_t len) { state = VNORMAL; ohnum = hnum = hlast = histnum(-1) + 1; insert = INSERT; saved_inslen = inslen; first_insert = 1; inslen = 0; modified = 1; vi_macro_reset(); edit_reset(buf, len); } static int nextstate(int ch) { if (is_extend(ch)) return VEXTCMD; else if (is_srch(ch)) return VSEARCH; else if (is_long(ch)) return VXCH; else if (ch == '.') return VREDO; else if (is_cmd(ch)) return VCMD; else return VFAIL; } static int vi_insert(int ch) { int tcursor; if (ch == edchars.erase || ch == CTRL('h')) { if (insert == REPLACE) { if (es->cursor == undo->cursor) { vi_error(); return 0; } } else { if (es->cursor == 0) { /* x_putc(BEL); no annoying bell here */ return 0; } } tcursor = es->cursor - 1; while(tcursor > 0 && isu8cont(es->cbuf[tcursor])) tcursor--; if (insert == INSERT) memmove(es->cbuf + tcursor, es->cbuf + es->cursor, es->linelen - es->cursor); if (insert == REPLACE && es->cursor < undo->linelen) memcpy(es->cbuf + tcursor, undo->cbuf + tcursor, es->cursor - tcursor); else es->linelen -= es->cursor - tcursor; if (inslen < es->cursor - tcursor) inslen = 0; else inslen -= es->cursor - tcursor; es->cursor = tcursor; expanded = NONE; return 0; } if (ch == edchars.kill) { if (es->cursor != 0) { inslen = 0; memmove(es->cbuf, &es->cbuf[es->cursor], es->linelen - es->cursor); es->linelen -= es->cursor; es->cursor = 0; } expanded = NONE; return 0; } if (ch == edchars.werase) { if (es->cursor != 0) { tcursor = backword(1); memmove(&es->cbuf[tcursor], &es->cbuf[es->cursor], es->linelen - es->cursor); es->linelen -= es->cursor - tcursor; if (inslen < es->cursor - tcursor) inslen = 0; else inslen -= es->cursor - tcursor; es->cursor = tcursor; } expanded = NONE; return 0; } /* If any chars are entered before escape, trash the saved insert * buffer (if user inserts & deletes char, ibuf gets trashed and * we don't want to use it) */ if (first_insert && ch != CTRL('[')) saved_inslen = 0; switch (ch) { case '\0': return -1; case '\r': case '\n': return 1; case CTRL('['): expanded = NONE; if (first_insert) { first_insert = 0; if (inslen == 0) { inslen = saved_inslen; return redo_insert(0); } lastcmd[0] = 'a'; lastac = 1; } if (lastcmd[0] == 's' || lastcmd[0] == 'c' || lastcmd[0] == 'C') return redo_insert(0); else return redo_insert(lastac - 1); /* { Begin nonstandard vi commands */ case CTRL('x'): expand_word(0); break; case CTRL('f'): complete_word(0, 0); break; case CTRL('e'): print_expansions(es); break; case CTRL('l'): do_clear_screen(); break; case CTRL('r'): redraw_line(1, 0); break; case CTRL('i'): if (Flag(FVITABCOMPLETE)) { complete_word(0, 0); break; } /* FALLTHROUGH */ /* End nonstandard vi commands } */ default: if (es->linelen >= es->cbufsize - 1) return -1; ibuf[inslen++] = ch; if (insert == INSERT) { memmove(&es->cbuf[es->cursor+1], &es->cbuf[es->cursor], es->linelen - es->cursor); es->linelen++; } es->cbuf[es->cursor++] = ch; if (insert == REPLACE && es->cursor > es->linelen) es->linelen++; expanded = NONE; } return 0; } static int vi_cmd(int argcnt, const char *cmd) { int ncursor; int cur, c1, c2, c3 = 0; int any; struct edstate *t; if (argcnt == 0 && !is_zerocount(*cmd)) argcnt = 1; if (is_move(*cmd)) { if ((cur = domove(argcnt, cmd, 0)) >= 0) { if (cur == es->linelen && cur != 0) while (isu8cont(es->cbuf[--cur])) continue; es->cursor = cur; } else return -1; } else { /* Don't save state in middle of macro.. */ if (is_undoable(*cmd) && !macro.p) { undo->winleft = es->winleft; memmove(undo->cbuf, es->cbuf, es->linelen); undo->linelen = es->linelen; undo->cursor = es->cursor; lastac = argcnt; memmove(lastcmd, cmd, MAXVICMD); } switch (*cmd) { case CTRL('l'): do_clear_screen(); break; case CTRL('r'): redraw_line(1, 0); break; case '@': { static char alias[] = "_\0"; struct tbl *ap; int olen, nlen; char *p, *nbuf; /* lookup letter in alias list... */ alias[1] = cmd[1]; ap = ktsearch(&aliases, alias, hash(alias)); if (!cmd[1] || !ap || !(ap->flag & ISSET)) return -1; /* check if this is a recursive call... */ if ((p = (char *) macro.p)) while ((p = strchr(p, '\0')) && p[1]) if (*++p == cmd[1]) return -1; /* insert alias into macro buffer */ nlen = strlen(ap->val.s) + 1; olen = !macro.p ? 2 : macro.len - (macro.p - macro.buf); nbuf = alloc(nlen + 1 + olen, APERM); memcpy(nbuf, ap->val.s, nlen); nbuf[nlen++] = cmd[1]; if (macro.p) { memcpy(nbuf + nlen, macro.p, olen); afree(macro.buf, APERM); nlen += olen; } else { nbuf[nlen++] = '\0'; nbuf[nlen++] = '\0'; } macro.p = macro.buf = (unsigned char *) nbuf; macro.len = nlen; } break; case 'a': modified = 1; hnum = hlast; if (es->linelen != 0) while (isu8cont(es->cbuf[++es->cursor])) continue; insert = INSERT; break; case 'A': modified = 1; hnum = hlast; del_range(0, 0); es->cursor = es->linelen; insert = INSERT; break; case 'S': es->cursor = domove(1, "^", 1); del_range(es->cursor, es->linelen); modified = 1; hnum = hlast; insert = INSERT; break; case 'Y': cmd = "y$"; /* ahhhhhh... */ case 'c': case 'd': case 'y': if (*cmd == cmd[1]) { c1 = *cmd == 'c' ? domove(1, "^", 1) : 0; c2 = es->linelen; } else if (!is_move(cmd[1])) return -1; else { if ((ncursor = domove(argcnt, &cmd[1], 1)) < 0) return -1; if (*cmd == 'c' && (cmd[1]=='w' || cmd[1]=='W') && !isspace((unsigned char)es->cbuf[es->cursor])) { while (isspace( (unsigned char)es->cbuf[--ncursor])) ; ncursor++; } if (ncursor > es->cursor) { c1 = es->cursor; c2 = ncursor; } else { c1 = ncursor; c2 = es->cursor; if (cmd[1] == '%') c2++; } } if (*cmd != 'c' && c1 != c2) yank_range(c1, c2); if (*cmd != 'y') { del_range(c1, c2); es->cursor = c1; } if (*cmd == 'c') { modified = 1; hnum = hlast; insert = INSERT; } break; case 'p': modified = 1; hnum = hlast; if (es->linelen != 0) es->cursor++; while (putbuf(ybuf, yanklen, 0) == 0 && --argcnt > 0) ; if (es->cursor != 0) es->cursor--; if (argcnt != 0) return -1; break; case 'P': modified = 1; hnum = hlast; any = 0; while (putbuf(ybuf, yanklen, 0) == 0 && --argcnt > 0) any = 1; if (any && es->cursor != 0) es->cursor--; if (argcnt != 0) return -1; break; case 'C': modified = 1; hnum = hlast; del_range(es->cursor, es->linelen); insert = INSERT; break; case 'D': yank_range(es->cursor, es->linelen); del_range(es->cursor, es->linelen); if (es->cursor != 0) es->cursor--; break; case 'g': if (!argcnt) argcnt = hlast; /* FALLTHROUGH */ case 'G': if (!argcnt) argcnt = 1; else argcnt = hlast - (source->line - argcnt); if (grabhist(modified, argcnt - 1) < 0) return -1; else { modified = 0; hnum = argcnt - 1; } break; case 'i': modified = 1; hnum = hlast; insert = INSERT; break; case 'I': modified = 1; hnum = hlast; es->cursor = domove(1, "^", 1); insert = INSERT; break; case 'j': case '+': case CTRL('n'): if (grabhist(modified, hnum + argcnt) < 0) return -1; else { modified = 0; hnum += argcnt; } break; case 'k': case '-': case CTRL('p'): if (grabhist(modified, hnum - argcnt) < 0) return -1; else { modified = 0; hnum -= argcnt; } break; case 'r': if (es->linelen == 0) return -1; modified = 1; hnum = hlast; if (cmd[1] == 0) vi_error(); else { c1 = 0; for (cur = es->cursor; cur < es->linelen; cur++) { if (!isu8cont(es->cbuf[cur])) c1++; if (c1 > argcnt) break; } if (argcnt > c1) return -1; del_range(es->cursor, cur); while (argcnt-- > 0) putbuf(&cmd[1], 1, 0); while (es->cursor > 0) if (!isu8cont(es->cbuf[--es->cursor])) break; es->cbuf[es->linelen] = '\0'; } break; case 'R': modified = 1; hnum = hlast; insert = REPLACE; break; case 's': if (es->linelen == 0) return -1; modified = 1; hnum = hlast; for (cur = es->cursor; cur < es->linelen; cur++) if (!isu8cont(es->cbuf[cur])) if (argcnt-- == 0) break; del_range(es->cursor, cur); insert = INSERT; break; case 'v': if (es->linelen == 0 && argcnt == 0) return -1; if (!argcnt) { if (modified) { es->cbuf[es->linelen] = '\0'; source->line++; histsave(source->line, es->cbuf, 1); } else argcnt = source->line + 1 - (hlast - hnum); } shf_snprintf(es->cbuf, es->cbufsize, argcnt ? "%s %d" : "%s", "fc -e ${VISUAL:-${EDITOR:-vi}} --", argcnt); es->linelen = strlen(es->cbuf); return 2; case 'x': if (es->linelen == 0) return -1; modified = 1; hnum = hlast; for (cur = es->cursor; cur < es->linelen; cur++) if (!isu8cont(es->cbuf[cur])) if (argcnt-- == 0) break; yank_range(es->cursor, cur); del_range(es->cursor, cur); break; case 'X': if (es->cursor == 0) return -1; modified = 1; hnum = hlast; for (cur = es->cursor; cur > 0; cur--) if (!isu8cont(es->cbuf[cur])) if (argcnt-- == 0) break; yank_range(cur, es->cursor); del_range(cur, es->cursor); es->cursor = cur; break; case 'u': t = es; es = undo; undo = t; break; case 'U': if (!modified) return -1; if (grabhist(modified, ohnum) < 0) return -1; modified = 0; hnum = ohnum; break; case '?': if (hnum == hlast) hnum = -1; /* ahhh */ case '/': c3 = 1; srchlen = 0; lastsearch = *cmd; /* FALLTHROUGH */ case 'n': case 'N': if (lastsearch == ' ') return -1; if (lastsearch == '?') c1 = 1; else c1 = 0; if (*cmd == 'N') c1 = !c1; if ((c2 = grabsearch(modified, hnum, c1, srchpat)) < 0) { if (c3) { restore_cbuf(); refresh_line(0); } return -1; } else { modified = 0; hnum = c2; ohnum = hnum; } break; case '_': { int inspace; char *p, *sp; if (histnum(-1) < 0) return -1; p = *histpos(); #define issp(c) (isspace((unsigned char)(c)) || (c) == '\n') if (argcnt) { while (*p && issp(*p)) p++; while (*p && --argcnt) { while (*p && !issp(*p)) p++; while (*p && issp(*p)) p++; } if (!*p) return -1; sp = p; } else { sp = p; inspace = 0; while (*p) { if (issp(*p)) inspace = 1; else if (inspace) { inspace = 0; sp = p; } p++; } p = sp; } modified = 1; hnum = hlast; if (es->cursor != es->linelen) es->cursor++; while (*p && !issp(*p)) { argcnt++; p++; } if (putbuf(" ", 1, 0) != 0) argcnt = -1; else if (putbuf(sp, argcnt, 0) != 0) argcnt = -1; if (argcnt < 0) { if (es->cursor != 0) es->cursor--; return -1; } insert = INSERT; } break; case '~': { char *p; unsigned char c; int i; if (es->linelen == 0) return -1; for (i = 0; i < argcnt; i++) { p = &es->cbuf[es->cursor]; c = (unsigned char)*p; if (islower(c)) { modified = 1; hnum = hlast; *p = toupper(c); } else if (isupper(c)) { modified = 1; hnum = hlast; *p = tolower(c); } if (es->cursor < es->linelen - 1) es->cursor++; } break; } case '#': { int ret = x_do_comment(es->cbuf, es->cbufsize, &es->linelen); if (ret >= 0) es->cursor = 0; return ret; } case '=': /* at&t ksh */ case CTRL('e'): /* Nonstandard vi/ksh */ print_expansions(es); break; case CTRL('i'): /* Nonstandard vi/ksh */ if (!Flag(FVITABCOMPLETE)) return -1; complete_word(1, argcnt); break; case CTRL('['): /* some annoying at&t ksh's */ if (!Flag(FVIESCCOMPLETE)) return -1; case '\\': /* at&t ksh */ case CTRL('f'): /* Nonstandard vi/ksh */ complete_word(1, argcnt); break; case '*': /* at&t ksh */ case CTRL('x'): /* Nonstandard vi/ksh */ expand_word(1); break; } if (insert == 0 && es->cursor >= es->linelen) while (es->cursor > 0) if (!isu8cont(es->cbuf[--es->cursor])) break; } return 0; } static int domove(int argcnt, const char *cmd, int sub) { int bcount, i = 0, t; int ncursor = 0; switch (*cmd) { case 'b': case 'B': if (!sub && es->cursor == 0) return -1; ncursor = (*cmd == 'b' ? backword : Backword)(argcnt); break; case 'e': case 'E': if (!sub && es->cursor + 1 >= es->linelen) return -1; ncursor = (*cmd == 'e' ? endword : Endword)(argcnt); if (!sub) while (isu8cont((unsigned char)es->cbuf[--ncursor])) continue; break; case 'f': case 'F': case 't': case 'T': fsavecmd = *cmd; fsavech = cmd[1]; /* drop through */ case ',': case ';': if (fsavecmd == ' ') return -1; i = fsavecmd == 'f' || fsavecmd == 'F'; t = fsavecmd > 'a'; if (*cmd == ',') t = !t; if ((ncursor = findch(fsavech, argcnt, t, i)) < 0) return -1; if (sub && t) ncursor++; break; case 'h': case CTRL('h'): if (!sub && es->cursor == 0) return -1; for (ncursor = es->cursor; ncursor > 0; ncursor--) if (!isu8cont(es->cbuf[ncursor])) if (argcnt-- == 0) break; break; case ' ': case 'l': if (!sub && es->cursor + 1 >= es->linelen) return -1; for (ncursor = es->cursor; ncursor < es->linelen; ncursor++) if (!isu8cont(es->cbuf[ncursor])) if (argcnt-- == 0) break; break; case 'w': case 'W': if (!sub && es->cursor + 1 >= es->linelen) return -1; ncursor = (*cmd == 'w' ? forwword : Forwword)(argcnt); break; case '0': ncursor = 0; break; case '^': ncursor = 0; while (ncursor < es->linelen - 1 && isspace((unsigned char)es->cbuf[ncursor])) ncursor++; break; case '|': ncursor = argcnt; if (ncursor > es->linelen) ncursor = es->linelen; if (ncursor) ncursor--; while (isu8cont(es->cbuf[ncursor])) ncursor--; break; case '$': ncursor = es->linelen; break; case '%': ncursor = es->cursor; while (ncursor < es->linelen && (i = bracktype(es->cbuf[ncursor])) == 0) ncursor++; if (ncursor == es->linelen) return -1; bcount = 1; do { if (i > 0) { if (++ncursor >= es->linelen) return -1; } else { if (--ncursor < 0) return -1; } t = bracktype(es->cbuf[ncursor]); if (t == i) bcount++; else if (t == -i) bcount--; } while (bcount != 0); if (sub && i > 0) ncursor++; break; default: return -1; } return ncursor; } static int redo_insert(int count) { while (count-- > 0) if (putbuf(ibuf, inslen, insert==REPLACE) != 0) return -1; if (es->cursor > 0) while (isu8cont(es->cbuf[--es->cursor])) continue; insert = 0; return 0; } static void yank_range(int a, int b) { yanklen = b - a; if (yanklen != 0) memmove(ybuf, &es->cbuf[a], yanklen); } static int bracktype(int ch) { switch (ch) { case '(': return 1; case '[': return 2; case '{': return 3; case ')': return -1; case ']': return -2; case '}': return -3; default: return 0; } } /* * Non user interface editor routines below here */ static int cur_col; /* current display column */ static int pwidth; /* display columns needed for prompt */ static int prompt_trunc; /* how much of prompt to truncate */ static int prompt_skip; /* how much of prompt to skip */ static int winwidth; /* available column positions */ static char *wbuf[2]; /* current & previous window buffer */ static int wbuf_len; /* length of window buffers (x_cols-3)*/ static int win; /* number of window buffer in use */ static char morec; /* more character at right of window */ static char holdbuf[LINE]; /* place to hold last edit buffer */ static int holdlen; /* length of holdbuf */ static void save_cbuf(void) { memmove(holdbuf, es->cbuf, es->linelen); holdlen = es->linelen; holdbuf[holdlen] = '\0'; } static void restore_cbuf(void) { es->cursor = 0; es->linelen = holdlen; memmove(es->cbuf, holdbuf, holdlen); } /* return a new edstate */ static struct edstate * save_edstate(struct edstate *old) { struct edstate *new; new = alloc(sizeof(struct edstate), APERM); new->cbuf = alloc(old->cbufsize, APERM); memcpy(new->cbuf, old->cbuf, old->linelen); new->cbufsize = old->cbufsize; new->linelen = old->linelen; new->cursor = old->cursor; new->winleft = old->winleft; return new; } static void restore_edstate(struct edstate *new, struct edstate *old) { memcpy(new->cbuf, old->cbuf, old->linelen); new->linelen = old->linelen; new->cursor = old->cursor; new->winleft = old->winleft; free_edstate(old); } static void free_edstate(struct edstate *old) { afree(old->cbuf, APERM); afree(old, APERM); } static void edit_reset(char *buf, size_t len) { const char *p; es = &ebuf; es->cbuf = buf; es->cbufsize = len; undo = &undobuf; undo->cbufsize = len; es->linelen = undo->linelen = 0; es->cursor = undo->cursor = 0; es->winleft = undo->winleft = 0; cur_col = pwidth = promptlen(prompt, &p); prompt_skip = p - prompt; if (pwidth > x_cols - 3 - MIN_EDIT_SPACE) { cur_col = x_cols - 3 - MIN_EDIT_SPACE; prompt_trunc = pwidth - cur_col; pwidth -= prompt_trunc; } else prompt_trunc = 0; if (!wbuf_len || wbuf_len != x_cols - 3) { wbuf_len = x_cols - 3; wbuf[0] = aresize(wbuf[0], wbuf_len, APERM); wbuf[1] = aresize(wbuf[1], wbuf_len, APERM); } (void) memset(wbuf[0], ' ', wbuf_len); (void) memset(wbuf[1], ' ', wbuf_len); winwidth = x_cols - pwidth - 3; win = 0; morec = ' '; holdlen = 0; } /* * this is used for calling x_escape() in complete_word() */ static int x_vi_putbuf(const char *s, size_t len) { return putbuf(s, len, 0); } static int putbuf(const char *buf, int len, int repl) { if (len == 0) return 0; if (repl) { if (es->cursor + len >= es->cbufsize) return -1; if (es->cursor + len > es->linelen) es->linelen = es->cursor + len; } else { if (es->linelen + len >= es->cbufsize) return -1; memmove(&es->cbuf[es->cursor + len], &es->cbuf[es->cursor], es->linelen - es->cursor); es->linelen += len; } memmove(&es->cbuf[es->cursor], buf, len); es->cursor += len; return 0; } static void del_range(int a, int b) { if (es->linelen != b) memmove(&es->cbuf[a], &es->cbuf[b], es->linelen - b); es->linelen -= b - a; } static int findch(int ch, int cnt, int forw, int incl) { int ncursor; if (es->linelen == 0) return -1; ncursor = es->cursor; while (cnt--) { do { if (forw) { if (++ncursor == es->linelen) return -1; } else { if (--ncursor < 0) return -1; } } while (es->cbuf[ncursor] != ch); } if (!incl) { if (forw) ncursor--; else ncursor++; } return ncursor; } /* Move right one character, and then to the beginning of the next word. */ static int forwword(int argcnt) { int ncursor, skip_space, want_letnum; unsigned char uc; ncursor = es->cursor; while (ncursor < es->linelen && argcnt--) { skip_space = 0; want_letnum = -1; ncursor--; while (++ncursor < es->linelen) { uc = es->cbuf[ncursor]; if (isspace(uc)) { skip_space = 1; continue; } else if (skip_space) break; if (uc & 0x80) continue; if (want_letnum == -1) want_letnum = letnum(uc); else if (want_letnum != letnum(uc)) break; } } return ncursor; } /* Move left one character, and then to the beginning of the word. */ static int backword(int argcnt) { int ncursor, skip_space, want_letnum; unsigned char uc; ncursor = es->cursor; while (ncursor > 0 && argcnt--) { skip_space = 1; want_letnum = -1; while (ncursor-- > 0) { uc = es->cbuf[ncursor]; if (isspace(uc)) { if (skip_space) continue; else break; } skip_space = 0; if (uc & 0x80) continue; if (want_letnum == -1) want_letnum = letnum(uc); else if (want_letnum != letnum(uc)) break; } ncursor++; } return ncursor; } /* Move right one character, and then to the byte after the word. */ static int endword(int argcnt) { int ncursor, skip_space, want_letnum; unsigned char uc; ncursor = es->cursor; while (ncursor < es->linelen && argcnt--) { skip_space = 1; want_letnum = -1; while (++ncursor < es->linelen) { uc = es->cbuf[ncursor]; if (isspace(uc)) { if (skip_space) continue; else break; } skip_space = 0; if (uc & 0x80) continue; if (want_letnum == -1) want_letnum = letnum(uc); else if (want_letnum != letnum(uc)) break; } } return ncursor; } /* Move right one character, and then to the beginning of the next big word. */ static int Forwword(int argcnt) { int ncursor; ncursor = es->cursor; while (ncursor < es->linelen && argcnt--) { while (!isspace((unsigned char)es->cbuf[ncursor]) && ncursor < es->linelen) ncursor++; while (isspace((unsigned char)es->cbuf[ncursor]) && ncursor < es->linelen) ncursor++; } return ncursor; } /* Move left one character, and then to the beginning of the big word. */ static int Backword(int argcnt) { int ncursor; ncursor = es->cursor; while (ncursor > 0 && argcnt--) { while (--ncursor >= 0 && isspace((unsigned char)es->cbuf[ncursor])) ; while (ncursor >= 0 && !isspace((unsigned char)es->cbuf[ncursor])) ncursor--; ncursor++; } return ncursor; } /* Move right one character, and then to the byte after the big word. */ static int Endword(int argcnt) { int ncursor; ncursor = es->cursor; while (ncursor < es->linelen && argcnt--) { while (++ncursor < es->linelen && isspace((unsigned char)es->cbuf[ncursor])) ; while (ncursor < es->linelen && !isspace((unsigned char)es->cbuf[ncursor])) ncursor++; } return ncursor; } static int grabhist(int save, int n) { char *hptr; if (n < 0 || n > hlast) return -1; if (n == hlast) { restore_cbuf(); ohnum = n; return 0; } (void) histnum(n); if ((hptr = *histpos()) == NULL) { internal_warningf("%s: bad history array", __func__); return -1; } if (save) save_cbuf(); if ((es->linelen = strlen(hptr)) >= es->cbufsize) es->linelen = es->cbufsize - 1; memmove(es->cbuf, hptr, es->linelen); es->cursor = 0; ohnum = n; return 0; } static int grabsearch(int save, int start, int fwd, char *pat) { char *hptr; int hist; int anchored; if ((start == 0 && fwd == 0) || (start >= hlast-1 && fwd == 1)) return -1; if (fwd) start++; else start--; anchored = *pat == '^' ? (++pat, 1) : 0; if ((hist = findhist(start, fwd, pat, anchored)) < 0) { /* if (start != 0 && fwd && match(holdbuf, pat) >= 0) { */ /* XXX should strcmp be strncmp? */ if (start != 0 && fwd && strcmp(holdbuf, pat) >= 0) { restore_cbuf(); return 0; } else return -1; } if (save) save_cbuf(); histnum(hist); hptr = *histpos(); if ((es->linelen = strlen(hptr)) >= es->cbufsize) es->linelen = es->cbufsize - 1; memmove(es->cbuf, hptr, es->linelen); es->cursor = 0; return hist; } static void do_clear_screen(void) { int neednl = 1; #ifndef SMALL if (cur_term != NULL && clear_screen != NULL) { if (tputs(clear_screen, 1, x_putc) != ERR) neednl = 0; } #endif /* Only print the full prompt if we cleared the screen. */ redraw_line(neednl, !neednl); } static void redraw_line(int neednl, int full) { (void) memset(wbuf[win], ' ', wbuf_len); if (neednl) { x_putc('\r'); x_putc('\n'); } vi_pprompt(full); cur_col = pwidth; morec = ' '; } static void refresh_line(int leftside) { if (outofwin()) rewindow(); display(wbuf[1 - win], wbuf[win], leftside); win = 1 - win; } static int outofwin(void) { int cur, col; if (es->cursor < es->winleft) return 1; col = 0; cur = es->winleft; while (cur < es->cursor) col = newcol((unsigned char) es->cbuf[cur++], col); if (col >= winwidth) return 1; return 0; } static void rewindow(void) { int tcur, tcol; int holdcur1, holdcol1; int holdcur2, holdcol2; holdcur1 = holdcur2 = tcur = 0; holdcol1 = holdcol2 = tcol = 0; while (tcur < es->cursor) { if (tcol - holdcol2 > winwidth / 2) { holdcur1 = holdcur2; holdcol1 = holdcol2; holdcur2 = tcur; holdcol2 = tcol; } tcol = newcol((unsigned char) es->cbuf[tcur++], tcol); } while (tcol - holdcol1 > winwidth / 2) holdcol1 = newcol((unsigned char) es->cbuf[holdcur1++], holdcol1); es->winleft = holdcur1; } /* Printing the byte ch at display column col moves to which column? */ static int newcol(int ch, int col) { if (ch == '\t') return (col | 7) + 1; if (isu8cont(ch)) return col; return col + char_len(ch); } /* Display wb1 assuming that wb2 is currently displayed. */ static void display(char *wb1, char *wb2, int leftside) { char *twb1; /* pointer into the buffer to display */ char *twb2; /* pointer into the previous display buffer */ static int lastb = -1; /* last byte# written from wb1, if UTF-8 */ int cur; /* byte# in the main command line buffer */ int col; /* display column loop variable */ int ncol; /* display column of the cursor */ int cnt; /* remaining display columns to fill */ int moreright; char mc; /* new "more character" at the right of window */ unsigned char ch; /* * Fill the current display buffer with data from cbuf. * In this first loop, col does not include the prompt. */ ncol = col = 0; cur = es->winleft; moreright = 0; twb1 = wb1; while (col < winwidth && cur < es->linelen) { if (cur == es->cursor && leftside) ncol = col + pwidth; if ((ch = es->cbuf[cur]) == '\t') { do { *twb1++ = ' '; } while (++col < winwidth && (col & 7) != 0); } else { if ((ch & 0x80) && Flag(FVISHOW8)) { *twb1++ = 'M'; if (++col < winwidth) { *twb1++ = '-'; col++; } ch &= 0x7f; } if (col < winwidth) { if (ch < ' ' || ch == 0x7f) { *twb1++ = '^'; if (++col < winwidth) { *twb1++ = ch ^ '@'; col++; } } else { *twb1++ = ch; if (!isu8cont(ch)) col++; } } } if (cur == es->cursor && !leftside) ncol = col + pwidth - 1; cur++; } if (cur == es->cursor) ncol = col + pwidth; /* Pad the current display buffer to the right margin. */ if (col < winwidth) { while (col < winwidth) { *twb1++ = ' '; col++; } } else moreright++; *twb1 = ' '; /* * Update the terminal display with data from wb1. * In this final loop, col includes the prompt. */ col = pwidth; cnt = winwidth; for (twb1 = wb1, twb2 = wb2; cnt; twb1++, twb2++) { if (*twb1 != *twb2) { /* * When a byte changes in the middle of a UTF-8 * character, back up to the start byte, unless * the previous byte was the last one written. */ if (col > 0 && isu8cont(*twb1)) { col--; if (lastb >= 0 && twb1 == wb1 + lastb + 1) cur_col = col; else while (twb1 > wb1 && isu8cont(*twb1)) { twb1--; twb2--; } } if (cur_col != col) ed_mov_opt(col, wb1); /* * Always write complete characters, and * advance all pointers accordingly. */ x_putc(*twb1); while (isu8cont(twb1[1])) { x_putc(*++twb1); twb2++; } lastb = *twb1 & 0x80 ? twb1 - wb1 : -1; cur_col++; } else if (isu8cont(*twb1)) continue; /* * For changed continuation bytes, we backed up. * For unchanged ones, we jumped to the next byte. * So, getting here, we had a real column. */ col++; cnt--; } /* Update the "more character". */ if (es->winleft > 0 && moreright) /* POSIX says to use * for this but that is a globbing * character and may confuse people; + is more innocuous */ mc = '+'; else if (es->winleft > 0) mc = '<'; else if (moreright) mc = '>'; else mc = ' '; if (mc != morec) { ed_mov_opt(pwidth + winwidth + 1, wb1); x_putc(mc); cur_col++; morec = mc; lastb = -1; } /* Move the cursor to its new position. */ if (cur_col != ncol) { ed_mov_opt(ncol, wb1); lastb = -1; } } /* Move the display cursor to display column number col. */ static void ed_mov_opt(int col, char *wb) { int ci; /* The cursor is already at the right place. */ if (cur_col == col) return; /* The cursor is too far right. */ if (cur_col > col) { if (cur_col > 2 * col + 1) { /* Much too far right, redraw from scratch. */ x_putc('\r'); vi_pprompt(0); cur_col = pwidth; } else { /* Slightly too far right, back up. */ do { x_putc('\b'); } while (--cur_col > col); return; } } /* Advance the cursor. */ for (ci = pwidth; ci < col || isu8cont(*wb); ci = newcol((unsigned char)*wb++, ci)) if (ci > cur_col || (ci == cur_col && !isu8cont(*wb))) x_putc(*wb); cur_col = ci; } /* replace word with all expansions (ie, expand word*) */ static int expand_word(int command) { static struct edstate *buf; int rval = 0; int nwords; int start, end; char **words; int i; /* Undo previous expansion */ if (command == 0 && expanded == EXPAND && buf) { restore_edstate(es, buf); buf = NULL; expanded = NONE; return 0; } if (buf) { free_edstate(buf); buf = NULL; } nwords = x_cf_glob(XCF_COMMAND_FILE|XCF_FULLPATH, es->cbuf, es->linelen, es->cursor, &start, &end, &words, NULL); if (nwords == 0) { vi_error(); return -1; } buf = save_edstate(es); expanded = EXPAND; del_range(start, end); es->cursor = start; for (i = 0; i < nwords; ) { if (x_escape(words[i], strlen(words[i]), x_vi_putbuf) != 0) { rval = -1; break; } if (++i < nwords && putbuf(" ", 1, 0) != 0) { rval = -1; break; } } i = buf->cursor - end; if (rval == 0 && i > 0) es->cursor += i; modified = 1; hnum = hlast; insert = INSERT; lastac = 0; refresh_line(0); return rval; } static int complete_word(int command, int count) { static struct edstate *buf; int rval = 0; int nwords; int start, end; char **words; char *match; int match_len; int is_unique; int is_command; /* Undo previous completion */ if (command == 0 && expanded == COMPLETE && buf) { print_expansions(buf); expanded = PRINT; return 0; } if (command == 0 && expanded == PRINT && buf) { restore_edstate(es, buf); buf = NULL; expanded = NONE; return 0; } if (buf) { free_edstate(buf); buf = NULL; } /* XCF_FULLPATH for count 'cause the menu printed by print_expansions() * was done this way. */ nwords = x_cf_glob(XCF_COMMAND_FILE | (count ? XCF_FULLPATH : 0), es->cbuf, es->linelen, es->cursor, &start, &end, &words, &is_command); if (nwords == 0) { vi_error(); return -1; } if (count) { int i; count--; if (count >= nwords) { vi_error(); x_print_expansions(nwords, words, is_command); x_free_words(nwords, words); redraw_line(0, 0); return -1; } /* * Expand the count'th word to its basename */ if (is_command) { match = words[count] + x_basename(words[count], NULL); /* If more than one possible match, use full path */ for (i = 0; i < nwords; i++) if (i != count && strcmp(words[i] + x_basename(words[i], NULL), match) == 0) { match = words[count]; break; } } else match = words[count]; match_len = strlen(match); is_unique = 1; /* expanded = PRINT; next call undo */ } else { match = words[0]; match_len = x_longest_prefix(nwords, words); expanded = COMPLETE; /* next call will list completions */ is_unique = nwords == 1; } buf = save_edstate(es); del_range(start, end); es->cursor = start; /* escape all shell-sensitive characters and put the result into * command buffer */ rval = x_escape(match, match_len, x_vi_putbuf); if (rval == 0 && is_unique) { /* If exact match, don't undo. Allows directory completions * to be used (ie, complete the next portion of the path). */ expanded = NONE; /* If not a directory, add a space to the end... */ if (match_len > 0 && match[match_len - 1] != '/') rval = putbuf(" ", 1, 0); } x_free_words(nwords, words); modified = 1; hnum = hlast; insert = INSERT; lastac = 0; /* prevent this from being redone... */ refresh_line(0); return rval; } static int print_expansions(struct edstate *e) { int nwords; int start, end; char **words; int is_command; nwords = x_cf_glob(XCF_COMMAND_FILE|XCF_FULLPATH, e->cbuf, e->linelen, e->cursor, &start, &end, &words, &is_command); if (nwords == 0) { vi_error(); return -1; } x_print_expansions(nwords, words, is_command); x_free_words(nwords, words); redraw_line(0, 0); return 0; } /* * The number of bytes needed to encode byte c. * Control bytes get "M-" or "^" prepended. * This function does not handle tabs. */ static int char_len(int c) { int len = 1; if ((c & 0x80) && Flag(FVISHOW8)) { len += 2; c &= 0x7f; } if (c < ' ' || c == 0x7f) len++; return len; } /* Similar to x_zotc(emacs.c), but no tab weirdness */ static void x_vi_zotc(int c) { if (Flag(FVISHOW8) && (c & 0x80)) { x_puts("M-"); c &= 0x7f; } if (c < ' ' || c == 0x7f) { x_putc('^'); c ^= '@'; } x_putc(c); } static void vi_pprompt(int full) { pprompt(prompt + (full ? 0 : prompt_skip), prompt_trunc); } static void vi_error(void) { /* Beem out of any macros as soon as an error occurs */ vi_macro_reset(); x_putc(BEL); x_flush(); } static void vi_macro_reset(void) { if (macro.p) { afree(macro.buf, APERM); memset((char *) ¯o, 0, sizeof(macro)); } } static int isu8cont(unsigned char c) { return !Flag(FVISHOW8) && (c & (0x80 | 0x40)) == 0x80; } #endif /* VI */