Subject: PATCH: vi-mode case-manipulation
From: Oliver Kiddle <[email-masked]>
To: Zsh workers <[email-masked]>
Date: Tue, 28 Jun 2016 16:46:51 +0200
List-Id: Zsh Workers List <zsh-workers.zsh.org>

This adds vi upper/lowercase widgets (gU / gu) in C form. There isn't
much difference to the shell widgets but it is possible to have the keys
bound by default and they aren't afflicted by KEYTIMEOUT problems. That
means guu and gUU work as aliases for gugu and gUgU. For consistency
with the naming of the emacs widgets, they are named
vi-up-case and vi-down-case.

Perhaps select-bracketed and quoted would also be better in C form with
something simpler and less likely to be missed by a vim user serving as
a shell widget example.

I think there is some value in providing a shell widget example of how
to read a vi movement. This includes vi-pipe which is like ! in vi. It
uses vi-delete instead of vi-change followed by vi-cmd-mode. Perhaps
there was a reason for using vi-change in my original case change
widgets but if so, I've forgotten what it was.

Oliver

diff --git a/Doc/Zsh/contrib.yo b/Doc/Zsh/contrib.yo
index 53ae96d..f1208e8 100644
--- a/Doc/Zsh/contrib.yo
+++ b/Doc/Zsh/contrib.yo
@@ -2968,6 +2968,17 @@ and aliases `tt(globurl)' to `tt(noglob urlglobber)'.  This function takes
 a local URL apart, attempts to pattern-match the local file portion of the
 URL path, and then puts the results back into URL format again.
 )
+tindex(vi-pipe)
+item(tt(vi-pipe))(
+This function reads a movement command from the keyboard and then
+prompts for an external command. The part of the buffer covered by
+the movement is piped to the external command and then replaced by
+the command's output. If the movement command is bound to vi-pipe,
+the current line is used.
+
+The function serves as an example for reading a vi movement command
+from within a user-defined widget.
+)
 tindex(which-command)
 item(tt(which-command))(
 This function is a drop-in replacement for the builtin widget
diff --git a/Doc/Zsh/zle.yo b/Doc/Zsh/zle.yo
index 80d3f39..1bae0cc 100644
--- a/Doc/Zsh/zle.yo
+++ b/Doc/Zsh/zle.yo
@@ -1781,6 +1781,13 @@ tindex(down-case-word)
 item(tt(down-case-word) (tt(ESC-L ESC-l)) (unbound) (unbound))(
 Convert the current word to all lowercase and move past it.
 )
+tindex(vi-down-case)
+item(tt(vi-down-case)) ((unbound) (tt(gu)) (unbound))(
+Read a movement command from the keyboard, and convert all characters
+from the cursor position to the endpoint of the movement to lowercase.
+If the movement command is tt(vi-down-case), swap the case of all
+characters on the current line.
+)
 tindex(kill-word)
 item(tt(kill-word) (tt(ESC-D ESC-d)) (unbound) (unbound))(
 Kill the current word.
@@ -1946,6 +1953,13 @@ tindex(vi-unindent)
 item(tt(vi-unindent) (unbound) (tt(<)) (unbound))(
 Unindent a number of lines.
 )
+tindex(vi-up-case)
+item(tt(vi-up-case)) ((unbound) (tt(gU)) (unbound))(
+Read a movement command from the keyboard, and convert all characters
+from the cursor position to the endpoint of the movement to lowercase.
+If the movement command is tt(vi-up-case), swap the case of all
+characters on the current line.
+)
 tindex(up-case-word)
 item(tt(up-case-word) (tt(ESC-U ESC-u)) (unbound) (unbound))(
 Convert the current word to all caps and move past it.
diff --git a/Functions/Zle/vi-pipe b/Functions/Zle/vi-pipe
new file mode 100644
index 0000000..a28f211
--- /dev/null
+++ b/Functions/Zle/vi-pipe
@@ -0,0 +1,31 @@
+# Example of a widget that takes a vi motion
+
+# Filter part of buffer corresponding to a vi motion through an external
+# program.
+
+# To enable with vi compatible bindings use:
+#   autoload -Uz vi-pipe
+#   bindkey -a '!' vi-pipe
+
+autoload -Uz read-from-minibuffer
+local _save_cut="$CUTBUFFER" REPLY
+
+# Use the standard vi-delete to accept a vi motion.
+zle .vi-delete || return
+read-from-minibuffer "!"
+local _save_cur=$CURSOR
+
+# cut buffer contains the deleted text and can be modified
+CUTBUFFER="$(eval $REPLY <<<$CUTBUFFER)"
+
+# put the modified text back in position. 
+if [[ CURSOR -eq 0 || $BUFFER[CURSOR] = $'\n' ]]; then
+  # at the beginning of a line, vi-delete won't have moved the cursor
+  # back to a previous line
+  zle .vi-put-before -n 1
+else
+  zle .vi-put-after -n 1
+fi
+
+# restore cut buffer and cursor to the start of the range
+CUTBUFFER="$_save_cut" CURSOR="$_save_cur"
diff --git a/Src/Zle/iwidgets.list b/Src/Zle/iwidgets.list
index 2b2654c..58310cd 100644
--- a/Src/Zle/iwidgets.list
+++ b/Src/Zle/iwidgets.list
@@ -143,6 +143,7 @@
 "vi-delete", videlete, ZLE_KEEPSUFFIX | ZLE_LASTCOL | ZLE_VIOPER
 "vi-delete-char", videletechar, ZLE_KEEPSUFFIX
 "vi-digit-or-beginning-of-line", vidigitorbeginningofline, 0
+"vi-down-case", vidowncase, ZLE_LASTCOL | ZLE_VIOPER
 "vi-down-line-or-history", vidownlineorhistory, ZLE_LINEMOVE
 "vi-end-of-line", viendofline, ZLE_LASTCOL
 "vi-fetch-history", vifetchhistory, ZLE_LINEMOVE
@@ -188,6 +189,7 @@
 "vi-swap-case", viswapcase, ZLE_LASTCOL
 "vi-undo-change", viundochange, ZLE_KEEPSUFFIX
 "vi-unindent", viunindent, ZLE_LASTCOL | ZLE_VIOPER
+"vi-up-case", viupcase, ZLE_LASTCOL | ZLE_VIOPER
 "vi-up-line-or-history", viuplineorhistory, ZLE_LINEMOVE
 "vi-yank", viyank, ZLE_LASTCOL | ZLE_VIOPER
 "vi-yank-eol", viyankeol, 0
diff --git a/Src/Zle/zle_keymap.c b/Src/Zle/zle_keymap.c
index f547dbf..3db4207 100644
--- a/Src/Zle/zle_keymap.c
+++ b/Src/Zle/zle_keymap.c
@@ -1374,8 +1374,11 @@ default_bindings(void)
     bindkey(amap, "ge", refthingy(t_vibackwardwordend), NULL);
     bindkey(amap, "gE", refthingy(t_vibackwardblankwordend), NULL);
     bindkey(amap, "gg", refthingy(t_beginningofbufferorhistory), NULL);
-    bindkey(amap, "g~", refthingy(t_vioperswapcase), NULL);
+    bindkey(amap, "gu", refthingy(t_vidowncase), NULL);
+    bindkey(amap, "gU", refthingy(t_viupcase), NULL);
     bindkey(amap, "g~~", NULL, "g~g~");
+    bindkey(amap, "guu", NULL, "gugu");
+    bindkey(amap, "gUU", NULL, "gUgU");
 
     /* emacs mode: arrow keys */ 
     add_cursor_key(emap, TCUPCURSOR, t_uplineorhistory, 'A');
diff --git a/Src/Zle/zle_vi.c b/Src/Zle/zle_vi.c
index 953af24..baa2064 100644
--- a/Src/Zle/zle_vi.c
+++ b/Src/Zle/zle_vi.c
@@ -731,6 +731,52 @@ vioperswapcase(UNUSED(char **args))
 
 /**/
 int
+viupcase(UNUSED(char **args))
+{
+    int oldcs, c2, ret = 1;
+
+    /* get the range */
+    startvichange(1);
+    if ((c2 = getvirange(0)) != -1) {
+	oldcs = zlecs;
+	/* covert the case of all letters within range */
+	while (zlecs < c2) {
+	    zleline[zlecs] = ZC_toupper(zleline[zlecs]);
+	    INCCS();
+	}
+	/* go back to the first line of the range */
+	zlecs = oldcs;
+	ret = 0;
+    }
+    vichgflag = 0;
+    return ret;
+}
+
+/**/
+int
+vidowncase(UNUSED(char **args))
+{
+    int oldcs, c2, ret = 1;
+
+    /* get the range */
+    startvichange(1);
+    if ((c2 = getvirange(0)) != -1) {
+	oldcs = zlecs;
+	/* convert the case of all letters within range */
+	while (zlecs < c2) {
+	    zleline[zlecs] = ZC_tolower(zleline[zlecs]);
+	    INCCS();
+	}
+	/* go back to the first line of the range */
+	zlecs = oldcs;
+	ret = 0;
+    }
+    vichgflag = 0;
+    return ret;
+}
+
+/**/
+int
 virepeatchange(UNUSED(char **args))
 {
     /* make sure we have a change to repeat */