diff --git a/ANNOUNCE b/ANNOUNCE index 9b7830e2d..0183dec73 100644 --- a/ANNOUNCE +++ b/ANNOUNCE @@ -56,6 +56,15 @@ New features in built-in commands: Note that to use these options the operating system must support the corresponding resource limit. +New command line editor features: + +- Various keys on extended PC keyboards are now handled as expected in the + emacs and vi built-in line editors: Ctrl or Alt + left or right arrow (go + back or forward one word), Ctrl+G (cancel reverse search), Ctrl+Delete + (delete next word). In addition, the Insert key now escapes the next + character in emacs and enters insert mode in vi, and the arrow keys are + recognized on more terminals. + New features in shell options: - The new --histreedit and --histverify options modify history expansion diff --git a/NEWS b/NEWS index 2642fa28d..a501ec4fe 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,25 @@ For full details, see the git log at: https://github.com/ksh93/ksh/tree/1.0 Any uppercase BUG_* names are modernish shell bug IDs. +2022-01-31: + +- Improved keyboard support for the vi and emacs built-in line editors: + - Added support for Home key sequences ^[[1~ and ^[[7~ as well as End key + sequences ^[[4~ and ^[[8~. + - Added support for arrow key sequences ^[OA, ^[OB, ^[OC and ^[OD. + - Added support for the following keyboard shortcuts (if the platform + supports the expected escape sequence): + - Ctrl-Left Arrow: Go back one word + - Alt-Left Arrow: Go back one word (Not supported on Haiku) + - Ctrl-Right Arrow: Go forward one word + - Alt-Right Arrow: Go forward one word (Not supported on Haiku) + - Ctrl-G: Cancel reverse search + - Ctrl-Delete: Delete next word (Not supported on Haiku) + - Added a keybind for the Insert key, which differs in the emacs and vi + editing modes: + - In emacs mode, Insert escapes the next character. + - In vi mode, Insert will switch the editor to insert mode (like in vim). + 2022-01-28: - Fixed longstanding job control breakage that occurred on the interactive diff --git a/src/cmd/ksh93/edit/emacs.c b/src/cmd/ksh93/edit/emacs.c index 76302c419..27fd3bcdd 100644 --- a/src/cmd/ksh93/edit/emacs.c +++ b/src/cmd/ksh93/edit/emacs.c @@ -780,7 +780,7 @@ static void putstring(Emacs_t* ep,register char *sp) static int escape(register Emacs_t* ep,register genchar *out,int count) { register int i,value; - int digit,ch; + int digit,ch,c,d; digit = 0; value = 0; while ((i=ed_getchar(ep->ed,0)),digit(i)) @@ -827,6 +827,7 @@ static int escape(register Emacs_t* ep,register genchar *out,int count) case 'd': /* M-d == delete word */ case 'c': /* M-c == uppercase */ case 'f': /* M-f == move cursor forward one word */ + forward: { i = cur; while(value-- && i0) @@ -1079,6 +1081,7 @@ static int escape(register Emacs_t* ep,register genchar *out,int count) return(-1); #endif case '[': /* feature not in book */ + case 'O': /* after running top O instead of [ */ switch(i=ed_getchar(ep->ed,1)) { case 'A': @@ -1131,8 +1134,53 @@ static int escape(register Emacs_t* ep,register genchar *out,int count) /* VT220 End key */ ed_ungetchar(ep->ed,cntl('E')); return(-1); + case '1': + case '7': + /* + * ed_getchar() can only be run once on each character + * and shouldn't be used on non-existent characters. + */ + ch = ed_getchar(ep->ed,1); + if(ch == '~') + { /* Home key */ + ed_ungetchar(ep->ed,cntl('A')); + return(-1); + } + else if(i == '1' && ch == ';') + { + c = ed_getchar(ep->ed,1); + if(c == '3' || c == '5' || c == '9') /* 3 == Alt, 5 == Ctrl, 9 == iTerm2 Alt */ + { + d = ed_getchar(ep->ed,1); + switch(d) + { + case 'D': /* Ctrl/Alt-Left arrow (go back one word) */ + ch = 'b'; + goto backward; + case 'C': /* Ctrl/Alt-Right arrow (go forward one word) */ + ch = 'f'; + goto forward; + } + ed_ungetchar(ep->ed,d); + } + ed_ungetchar(ep->ed,c); + } + ed_ungetchar(ep->ed,ch); + ed_ungetchar(ep->ed,i); + return(-1); + case '2': /* Insert key */ + ch = ed_getchar(ep->ed,1); + if(ch == '~') + { + ed_ungetchar(ep->ed, cntl('V')); + return(-1); + } + ed_ungetchar(ep->ed,ch); + ed_ungetchar(ep->ed,i); + return(-1); case '3': - if((ch=ed_getchar(ep->ed,1))=='~') + ch = ed_getchar(ep->ed,1); + if(ch == '~') { /* * VT220 forward-delete key. * Since ERASECHAR and EOFCHAR are usually both mapped to ^D, we @@ -1143,6 +1191,47 @@ static int escape(register Emacs_t* ep,register genchar *out,int count) ed_ungetchar(ep->ed,ERASECHAR); return(-1); } + else if(ch == ';') + { + c = ed_getchar(ep->ed,1); + if(c == '5') + { + d = ed_getchar(ep->ed,1); + if(d == '~') + { + /* Ctrl-Delete (delete next word) */ + ch = 'd'; + goto forward; + } + ed_ungetchar(ep->ed,d); + } + ed_ungetchar(ep->ed,c); + } + ed_ungetchar(ep->ed,ch); + ed_ungetchar(ep->ed,i); + return(-1); + case '5': /* Haiku terminal Ctrl-Arrow key */ + ch = ed_getchar(ep->ed,1); + switch(ch) + { + case 'D': /* Ctrl-Left arrow (go back one word) */ + ch = 'b'; + goto backward; + case 'C': /* Ctrl-Right arrow (go forward one word) */ + ch = 'f'; + goto forward; + } + ed_ungetchar(ep->ed,ch); + ed_ungetchar(ep->ed,i); + return(-1); + case '4': + case '8': /* rxvt */ + ch = ed_getchar(ep->ed,1); + if(ch == '~') + { + ed_ungetchar(ep->ed,cntl('E')); /* End key */ + return(-1); + } ed_ungetchar(ep->ed,ch); /* FALLTHROUGH */ default: @@ -1288,7 +1377,7 @@ static void search(Emacs_t* ep,genchar *out,int direction) goto restore; continue; } - if(i == ep->ed->e_intr) /* end reverse search */ + if(i == ep->ed->e_intr || i == cntl('G')) /* end reverse search */ goto restore; if (i==usrkill) { diff --git a/src/cmd/ksh93/edit/vi.c b/src/cmd/ksh93/edit/vi.c index bd8b34506..00892cfb8 100644 --- a/src/cmd/ksh93/edit/vi.c +++ b/src/cmd/ksh93/edit/vi.c @@ -112,6 +112,7 @@ typedef struct _vi_ int U_saved; /* original virtual saved */ genchar *U_space; /* used for U command */ genchar *u_space; /* used for u command */ + unsigned char del_word; /* used for Ctrl-Delete */ #ifdef FIORDCHK clock_t typeahead; /* typeahead occurred */ #else @@ -753,7 +754,7 @@ static int cntlmode(Vi_t *vp) if(mvcursor(vp,c)) { sync_cursor(vp); - if( c != '[' ) + if( c != '[' && c != 'O' ) vp->repeat = 1; continue; } @@ -1036,7 +1037,7 @@ static int cntlmode(Vi_t *vp) if(lookahead) { ed_ungetchar(vp->ed,c=ed_getchar(vp->ed,1)); - if(c=='[') + if(c=='[' || c=='O') continue; } /* FALLTHROUGH */ @@ -1430,6 +1431,10 @@ static void getline(register Vi_t* vp,register int mode) } break; + case cntl('G'): + if(mode!=SEARCH) + goto fallback; + /* FALLTHROUGH */ case UINTR: first_virt = 0; cdelete(vp,cur_virt+1, BAD); @@ -1550,6 +1555,7 @@ static void getline(register Vi_t* vp,register int mode) } /* FALLTHROUGH */ default: + fallback: if( mode == REPLACE ) { if( cur_virt < last_virt ) @@ -1583,7 +1589,7 @@ static void getline(register Vi_t* vp,register int mode) static int mvcursor(register Vi_t* vp,register int motion) { - register int count; + register int count, c, d; register int tcur_virt; register int incr = -1; register int bound = 0; @@ -1612,7 +1618,8 @@ static int mvcursor(register Vi_t* vp,register int motion) tcur_virt = last_virt; break; - case '[': + case '[': /* feature not in book */ + case 'O': /* after running top O instead of [ */ switch(motion=ed_getchar(vp->ed,-1)) { case 'A': @@ -1659,6 +1666,47 @@ static int mvcursor(register Vi_t* vp,register int motion) /* VT220 End key */ ed_ungetchar(vp->ed,'$'); return(1); + case '1': + case '7': + bound = ed_getchar(vp->ed,-1); + if(bound=='~') + { /* Home key */ + ed_ungetchar(vp->ed,'0'); + return(1); + } + else if(motion=='1' && bound==';') + { + c = ed_getchar(vp->ed,-1); + if(c == '3' || c == '5' || c == '9') /* 3 == Alt, 5 == Ctrl, 9 == iTerm2 Alt */ + { + d = ed_getchar(vp->ed,-1); + switch(d) + { + case 'D': /* Ctrl/Alt-Left arrow (go back one word) */ + ed_ungetchar(vp->ed, 'b'); + return(1); + case 'C': /* Ctrl/Alt-Right arrow (go forward one word) */ + ed_ungetchar(vp->ed, 'w'); + return(1); + } + ed_ungetchar(vp->ed,d); + } + ed_ungetchar(vp->ed,c); + } + ed_ungetchar(vp->ed,bound); + ed_ungetchar(vp->ed,motion); + return(0); + case '2': + bound = ed_getchar(vp->ed,-1); + if(bound=='~') + { + /* VT220 insert key */ + ed_ungetchar(vp->ed,'i'); + return(1); + } + ed_ungetchar(vp->ed,bound); + ed_ungetchar(vp->ed,motion); + return(0); case '3': bound = ed_getchar(vp->ed,-1); if(bound=='~') @@ -1667,6 +1715,48 @@ static int mvcursor(register Vi_t* vp,register int motion) ed_ungetchar(vp->ed,'x'); return(1); } + else if(bound==';') + { + c = ed_getchar(vp->ed,-1); + if(c == '5') + { + d = ed_getchar(vp->ed,-1); + if(d == '~') + { + /* Ctrl-Delete */ + vp->del_word = 1; + ed_ungetchar(vp->ed,'d'); + return(1); + } + ed_ungetchar(vp->ed,d); + } + ed_ungetchar(vp->ed,c); + } + ed_ungetchar(vp->ed,bound); + ed_ungetchar(vp->ed,motion); + return(0); + case '5': /* Haiku terminal Ctrl-Arrow key */ + bound = ed_getchar(vp->ed,-1); + switch(bound) + { + case 'D': /* Ctrl-Left arrow (go back one word) */ + ed_ungetchar(vp->ed, 'b'); + return(1); + case 'C': /* Ctrl-Right arrow (go forward one word) */ + ed_ungetchar(vp->ed, 'w'); + return(1); + } + ed_ungetchar(vp->ed,bound); + ed_ungetchar(vp->ed,motion); + return(0); + case '4': + case '8': + bound = ed_getchar(vp->ed,-1); + if(bound=='~') + { /* End key */ + ed_ungetchar(vp->ed,'$'); + return(1); + } ed_ungetchar(vp->ed,bound); /* FALLTHROUGH */ default: @@ -2602,6 +2692,11 @@ chgeol: case 'd': /** delete **/ if( mode ) c = vp->lastmotion; + else if( vp->del_word ) + { + vp->del_word = 0; + c = 'w'; + } else c = getcount(vp,ed_getchar(vp->ed,-1)); deleol: diff --git a/src/cmd/ksh93/include/version.h b/src/cmd/ksh93/include/version.h index 55be850f8..2dd38c2bc 100644 --- a/src/cmd/ksh93/include/version.h +++ b/src/cmd/ksh93/include/version.h @@ -21,7 +21,7 @@ #define SH_RELEASE_FORK "93u+m" /* only change if you develop a new ksh93 fork */ #define SH_RELEASE_SVER "1.0.0-beta.2" /* semantic version number: https://semver.org */ -#define SH_RELEASE_DATE "2022-01-28" /* must be in this format for $((.sh.version)) */ +#define SH_RELEASE_DATE "2022-01-31" /* must be in this format for $((.sh.version)) */ #define SH_RELEASE_CPYR "(c) 2020-2022 Contributors to ksh " SH_RELEASE_FORK /* Scripts sometimes field-split ${.sh.version}, so don't change amount of whitespace. */ diff --git a/src/cmd/ksh93/sh.1 b/src/cmd/ksh93/sh.1 index 0f4d8f9b8..537b7b706 100644 --- a/src/cmd/ksh93/sh.1 +++ b/src/cmd/ksh93/sh.1 @@ -4922,6 +4922,15 @@ Move cursor forward one word. editor's idea of a word is a string of characters consisting of only letters, digits and underscores.) .TP 10 +.BI M-[1;3C +(Alt-Right arrow) Same as \fBM-f\fR. +.TP 10 +.BI M-[1;5C +(Ctrl-Right arrow) Same as \fBM-f\fR. +.TP 10 +.BI M-[1;9C +(iTerm2 Alt-Right arrow) Same as \fBM-f\fR. +.TP 10 .BI ^B Move cursor backward (left) one character. .TP 10 @@ -4931,21 +4940,60 @@ Move cursor backward (left) one character. .BI M-b Move cursor backward one word. .TP 10 +.BI M-[1;3D +(Alt-Left arrow) Same as \fBM-b\fR. +.TP 10 +.BI M-[1;5D +(Ctrl-Left arrow) Same as \fBM-b\fR. +.TP 10 +.BI M-[1;9D +(iTerm2 Alt-Left arrow) Same as \fBM-b\fR. +.TP 10 .BI ^A Move cursor to start of line. .TP 10 .BI M-[H (Home) Same as \fB^A\fR. .TP 10 +.BI M-[1~ +Same as \fB^A\fR. +.TP 10 +.BI M-[7~ +Same as \fB^A\fR. +.TP 10 .BI ^E Move cursor to end of line. .TP 10 .BI M-[F (End) Same as \fB^E\fR. .TP 10 +.BI M-[4~ +Same as \fB^E\fR. +.TP 10 +.BI M-[8~ +Same as \fB^E\fR. +.TP 10 .BI M-[Y Same as \fB^E\fR. .TP 10 +.BI M-OA +(Up Arrow) Same as \fBM-[A\fR. +.TP 10 +.BI M-OB +(Down Arrow) Same as \fBM-[B\fR. +.TP 10 +.BI M-OC +(Right Arrow) Same as \fBM-[C\fR. +.TP 10 +.BI M-OD +(Left Arrow) Same as \fBM-[D\fR. +.TP 10 +.BI M-O5C +(Ctrl-Right Arrow) Same as \fBM-f\fR. +.TP 10 +.BI M-O5D +(Ctrl-Left Arrow) Same as \fBM-b\fR. +.TP 10 .BI ^] char Move cursor forward to character .I char @@ -4987,6 +5035,9 @@ Delete current character. .BI M-d Delete current word. .TP 10 +.BI M-[3;5~ +(Ctrl-Delete) Same as \fBM-d\fR. +.TP 10 .BI M-^H (Meta-backspace) Delete previous word. .TP 10 @@ -5130,6 +5181,9 @@ is accessed. In this case a parameter of zero reverses the direction of the search. .TP 10 +.B ^G +Exit reverse search mode. +.TP 10 .B ^O Operate \- Execute the current line and fetch the next line relative to current line from the @@ -5247,6 +5301,9 @@ which is not subject to any shell option. .B M-^V Display version of the shell. .TP 10 +.BI M-[2~ +(Insert) Escape the next character. +.TP 10 .B M-# If the line does not begin with a .BR # , @@ -5437,6 +5494,54 @@ Cursor to start of line. .B ^[[H (Home) Same as \f30\fP. .TP 10 +.B ^[[1~ +Same as \f30\fP. +.TP 10 +.B ^[[7~ +Same as \f30\fP. +.TP 10 +.B ^[[1;3D +(Alt-Left arrow) Same as \f3b\fP. +.TP 10 +.B ^[[1;5D +(Ctrl-Left arrow) Same as \f3b\fP. +.TP 10 +.B ^[[1;9D +(iTerm2 Alt-Left arrow) Same as \f3b\fP. +.TP 10 +.B ^[[1;3C +(Alt-Right arrow) Same as \f3w\fP. +.TP 10 +.B ^[[1;5C +(Ctrl-Right arrow) Same as \f3w\fP. +.TP 10 +.B ^[[1;9C +(iTerm2 Alt-Right arrow) Same as \f3w\fP. +.TP 10 +.B ^[[2~ +(Insert) Same as \f3i\fP. +.TP 10 +.B ^[[3;5~ +(Ctrl-Delete) Same as \f3dw\fP. +.TP 10 +.B ^[OA +(Up Arrow) Same as \f3^[[A\fP. +.TP 10 +.B ^[OB +(Down Arrow) Same as \f3^[[B\fP. +.TP 10 +.B ^[OC +(Right Arrow) Same as \f3^[[C\fP. +.TP 10 +.B ^[OD +(Left Arrow) Same as \f3^[[D\fP. +.TP 10 +.B ^[O5C +(Ctrl-Right Arrow) Same as \f3w\fP. +.TP 10 +.B ^[O5D +(Ctrl-Left Arrow) Same as \f3b\fP. +.TP 10 .B ^ Cursor to first non-blank character in line. .TP 10 @@ -5446,9 +5551,18 @@ Cursor to end of line. .B ^[[F (End) Same as \f3$\fP. .TP 10 +.B ^[[4~ +Same as \f3$\fP. +.TP 10 +.B ^[[8~ +Same as \f3$\fP. +.TP 10 .B ^[[Y Same as \f3$\fP. .TP 10 +.B ^G +Exit reverse search mode. +.TP 10 .B % Moves to balancing .BR ( ,