52520: add new features and improvements to the "incarg" ZLE widget

- Decrement integers without defining a new widget
- Preserve the number of leading zeros
- Increment binaries, octals, and hexadecimals
- Move the cursor to the end of the incremented integer
- Create a sequence of integers across terminal panes
- Add a Vim variant
- Also add tests
master
midchildan 4 months ago committed by Oliver Kiddle
parent 2d2086557e
commit fb9a7cc5dd

@ -1,5 +1,9 @@
2024-02-15 Oliver Kiddle <opk@zsh.org>
* 52520: midchildan: Doc/Zsh/contrib.yo, Functions/Zle/incarg,
Test/X05zleincarg.ztst: add new features and improvements to the
"incarg" ZLE widget
* github #112: Poncho: Completion/Unix/Command/_todo.sh:
Completion: todo.sh uses shorthelp and not showhelp

@ -2620,12 +2620,30 @@ zle -N history-pattern-search-forward history-pattern-search)
tindex(incarg)
vindex(incarg, use of)
item(tt(incarg))(
Typing the keystrokes for this widget with the cursor placed on or to the
left of an integer causes that integer to be incremented by one. With a
numeric argument, the number is incremented by the amount of the
argument (decremented if the numeric argument is negative). The shell
parameter tt(incarg) may be set to change the default increment to
something other than one.
This widget allows you to increment integers on the current line. In addition
to decimals, it can handle hexadecimals prefixed with tt(0x), binaries with
tt(0b), and octals with tt(0o).
By default, the target integer will be incremented by one. With a numeric
argument, the integer is incremented by the amount of the argument. The shell
parameter tt(incarg) may be set to change the default increment to something
other than one.
The behavior of this widget changes depending on the widget name.
When the widget is named tt(incarg), the widget will increment an integer
placed under the cursor placed or just to the left of it. tt(decarg), on the
other hand, decrements the integer. When the name is prefixed with tt(vim-),
the cursor will jump to the nearest integer after the cursor before incrementing
it.
There's also a tt(sync-) prefix that can be added to the widget name. This
variant is used for creating a sequence of numbers on split terminals with
synchronized key input. The first pane won't increment the integer at all, but
each pane after that will have the integer incremented once more than the
previous pane. It currently supports tmux and iTerm2.
The prefixes tt(vim-) and tt(sync-) can be combined into tt(vim-sync-).
example(bindkey '^X+' incarg)
)

@ -1,43 +1,254 @@
# Shell function to increment an integer either under the cursor or just
# to the left of it. Use
emulate -L zsh
# A ZLE widget to increment an integer.
#
# In addition to decimals, it can handle hexadecimals prefixed with "0x",
# binaries with "0b", and octals with "0o".
#
# By default, the target integer will be incremented by one. With a numeric
# argument, the integer is incremented by the amount of the argument. The shell
# parameter "incarg" may be set to change the default increment to something
# other than one.
#
# The behavior of this widget changes depending on how it is named.
#
# - incarg / decarg
#
# incarg will increment an integer either under the cursor or just to the left
# of it. decarg, on the other hand, will decrement it.
#
# For example,
#
# echo 41
# ^^^ cursor anywhere here
#
# with incarg gives
#
# echo 42
# ^ cursor will move here
#
# - sync-incarg / sync-decarg
#
# The sync- variant is used for creating a sequence of numbers on split
# terminals with synchronized key input. The first pane won't be incremented
# at all, but each pane after that will have the number incremented once more
# than the previous pane.
#
# Currently supports tmux and iTerm2.
#
# - vim-incarg / vim-decarg
#
# This behaves like Vim's CTRL-A / CTRL-X. It moves the cursor to the nearest
# number after the cursor and increments or decrements it.
#
# - vim-sync-incarg / vim-sync-decarg
#
# This combines the behavior of the vim- and sync- variants. It's inspired by
# Vim's g_CTRL-A / g_CTRL-X.
#
# Example Usage:
#
# autoload -Uz incarg
# zle -N incarg
# bindkey "..." incarg
# to define it. For example,
# echo 41
# ^^^ cursor anywhere here
# with incarg gives
# echo 42
# with the cursor in the same place.
#
# A numeric argument gives a number other than 1 to add (may be negative).
# If you're going to do it a lot with one particular number, you can set
# the parameter incarg to that number (a numeric argument still takes
# precedence).
# for widget in vim-{,sync-}{inc,dec}arg; do
# zle -N "$widget" incarg
# done
# bindkey -a \
# '^A' vim-incarg \
# '^X' vim-decarg \
# 'g^A' vim-sync-incarg \
# 'g^X' vim-sync-decarg
emulate -L zsh
setopt extendedglob
setopt localoptions extended_glob
local match mbegin mend MATCH MBEGIN MEND i
local rrest lrest num
# find the number and determine the base
integer pos=$(( CURSOR + 1 )) base=0
rrest=${RBUFFER##[0-9]#}
if [[ $RBUFFER = [0-9]* ]]; then
if [[ -z $rrest ]]; then
num=$RBUFFER
else
num=${RBUFFER[1,-$#rrest-1]}
# avoid miscalculating positions when cursor is at the end of the line
while (( pos > 0 )) && [[ "$BUFFER[pos]" == '' ]]; do
(( pos-- ))
done
# check for a prefix (e.g., 0x) before the cursor
for (( i = 0; i < 2; i++ )); do
case "$BUFFER[1,pos]" in
*0[xX][0-9a-fA-F]##) base=16 ;;
*0[oO][0-7]##) base=8 ;;
*0[bB][01]##) base=2 ;;
*[1-9]) base=10 ;;
*0) ;; # there may be a prefix right after the cursor
*)
# the non-Vim variant looks right before the cursor too, but not after it
if [[ "$WIDGET" != vi* ]]; then
if (( i == 0 )); then
(( pos-- ))
continue
else
return 1
fi
fi
;;
esac
break
done
# check for a prefix on the cursor
if (( base == 0 && pos < $#BUFFER )); then
case "$BUFFER[1,pos+1]" in
*0[xX][0-9a-fA-F]) base=16; (( pos++ )) ;;
*0[oO][0-7]) base=8; (( pos++ )) ;;
*0[bB][01]) base=2; (( pos++ )) ;;
esac
fi
if (( base == 0 )); then
if [[ "$WIDGET" == vi* ]]; then
# jump to the nearest number after the cursor
while [[ "$BUFFER[pos]" == [^0-9] ]]; do
(( pos++ ))
(( pos > $#BUFFER )) && return 1
done
fi
# check for a prefix right after the cursor and jump right after it, if any
if (( pos <= 1 )) || [[ "$BUFFER[pos-1]" == [^0-9] ]]; then
case "$BUFFER[pos,-1]" in
0[xX][0-9a-fA-F]*) base=16; (( pos += 2 )) ;;
0[oO][0-7]*) base=8; (( pos += 2 )) ;;
0[bB][01]*) base=2; (( pos += 2 )) ;;
esac
fi
fi
if (( base == 0 )); then
base=10
fi
lrest=${LBUFFER%%[0-9]#}
if [[ $LBUFFER = *[0-9] ]]; then
if [[ -z $lrest ]]; then
num="$LBUFFER$num"
# find the start of the number
integer first="$pos"
case "$base" in
10)
while [[ "$BUFFER[first-1]" == [0-9] ]]; do
(( first-- ))
done
if [[ $BUFFER[first-1] = - ]]; then
(( first-- ))
fi
;;
2)
while [[ "$BUFFER[first-1]" == [01] ]]; do
(( first-- ))
done
;;
8)
while [[ "$BUFFER[first-1]" == [0-7] ]]; do
(( first-- ))
done
;;
16)
while [[ "$BUFFER[first-1]" == [0-9a-fA-F] ]]; do
(( first-- ))
done
;;
esac
# find the end of the number
integer last="$pos"
case "$base" in
10)
while [[ "$BUFFER[last+1]" == [0-9] ]]; do
(( last++ ))
done
;;
2)
while [[ "$BUFFER[last+1]" == [01] ]]; do
(( last++ ))
done
;;
8)
while [[ "$BUFFER[last+1]" == [0-7] ]]; do
(( last++ ))
done
;;
16)
while [[ "$BUFFER[last+1]" == [0-9a-fA-F] ]]; do
(( last++ ))
done
;;
esac
# calculate the number of digits
integer ndigits=0
case "$BUFFER[first,first+1]" in
0*|-0) ndigits=$(( last - first + 1 )) ;;
esac
# determine the amount to increment
integer delta=${NUMERIC:-${incarg:-1}}
if [[ "$WIDGET" = *decarg ]]; then
(( delta = -delta ))
fi
if [[ "$WIDGET" = *sync-* ]]; then
integer pane_index=0
if [[ -n "$TMUX_PANE" ]]; then
pane_index="$(tmux display-message -pt "$TMUX_PANE" '#{pane_index}')"
elif [[ "$ITERM_SESSION_ID" =~ '^w[0-9]+t[0-9]+p([0-9]+)' ]]; then
pane_index="$match[1]"
else
num="${LBUFFER[$#lrest+1,-1]}$num"
zle -M "[$WIDGET] unsupported terminal"
return 1
fi
(( delta *= pane_index ))
fi
local old="$BUFFER[first,last]"
integer oldlen=$#BUFFER
local fmt1 fmt2
case "$base" in
10) fmt1=d; fmt2='#10' ;;
2) fmt1=s; fmt2='##2' ;;
8) fmt1=s; fmt2='##8' ;;
16) fmt1="$BUFFER[first-1]"; fmt2='#16' ;;
esac
local raw_result padded
raw_result="$( \
printf "%0$ndigits$fmt1" $(( [$fmt2] "$base#$old" + delta )) 2> /dev/null)"
padded="${raw_result// /0}"
integer oldnum="$base#$old" newnum="$base#$padded" 2> /dev/null
if (( base != 10 && newnum < 0
|| delta > 0 && newnum < oldnum
|| delta < 0 && newnum > oldnum )); then
zle -M "[$WIDGET] The resulting number is either too big or too small."
return 1
fi
# adjust the number of leading zeros if the sign of the integer changed
local new
if (( base == 10 && ndigits == $#padded )); then
if (( oldnum < 0 && newnum >= 0 )); then
new="${padded#0}"
elif (( oldnum >= 0 && newnum < 0 )); then
new="-0${padded#-}"
fi
fi
if [[ -z "$new" ]]; then
new="$padded"
fi
if zstyle -t ":zle:$WIDGET" debug; then
zle -M "[$WIDGET] base: $base delta: $delta old: '$old' new: '$new'"
fi
BUFFER[first,last]="$new"
[[ -n $num ]] && (( num += ${NUMERIC:-${incarg:-1}} ))
integer offset=0
if [[ "$WIDGET" == vi* ]]; then
offset=-1
fi
(( CURSOR = last + $#BUFFER - oldlen + offset ))
BUFFER="$lrest$num$rrest"
return 0

@ -0,0 +1,360 @@
# Tests the incarg ZLE widget
%prep
ZSH_TEST_LANG=$(ZTST_find_UTF8)
if ( zmodload zsh/zpty 2>/dev/null ); then
. $ZTST_srcdir/comptest
comptestinit -v -z $ZTST_testdir/../Src/zsh
else
ZTST_unimplemented="the zsh/zpty module is not available"
fi
zpty_run '
autoload -Uz incarg
for name in {,vim-}{,sync-}{inc,dec}arg; do
zle -N "$name" incarg
done
bindkey -v "^N" incarg
bindkey -v "^P" decarg
bindkey -v "^F" sync-incarg
bindkey -v "^B" sync-decarg
bindkey -a "^N" vim-incarg
bindkey -a "^P" vim-decarg
bindkey -a "^F" vim-sync-incarg
bindkey -a "^B" vim-sync-decarg
unset TMUX_PANE ITERM_SESSION_ID
tmux() {
echo "$TMUX_PANE"
}
'
%test
zletest $'0\C-n'
0:increment an integer with incarg
>BUFFER: 1
>CURSOR: 1
zletest $'0\C-p'
0:decrement an integer with decarg
>BUFFER: -1
>CURSOR: 2
zletest $'echo 0\e0\C-n'
0:increment an integer with vim-incarg
>BUFFER: echo 1
>CURSOR: 5
zletest $'echo 0\e0\C-p'
0:decrement an integer with vim-decarg
>BUFFER: echo -1
>CURSOR: 6
zletest $'0\C-f'
0:sync-incarg does nothing on unsupported terminals
>BUFFER: 0
>CURSOR: 1
zpty_run 'TMUX_PANE=0'
zletest $'0\C-f'
zpty_run 'unset TMUX_PANE'
0:sync-incarg on tmux in pane 0
>BUFFER: 0
>CURSOR: 1
zpty_run 'TMUX_PANE=1'
zletest $'0\C-f'
zpty_run 'unset TMUX_PANE'
0:sync-incarg on tmux in pane 1
>BUFFER: 1
>CURSOR: 1
zpty_run 'TMUX_PANE=2'
zletest $'0\C-f'
zpty_run 'unset TMUX_PANE'
0:sync-incarg on tmux in pane 2
>BUFFER: 2
>CURSOR: 1
zpty_run 'ITERM_SESSION_ID=w0t0p0:00000000-0000-0000-0000-000000000000'
zletest $'0\C-f'
zpty_run 'unset ITERM_SESSION_ID'
0:sync-incarg on tmux in pane 0
>BUFFER: 0
>CURSOR: 1
zpty_run 'ITERM_SESSION_ID=w0t0p1:00000000-0000-0000-0000-000000000000'
zletest $'0\C-f'
zpty_run 'unset ITERM_SESSION_ID'
0:sync-incarg on tmux in pane 1
>BUFFER: 1
>CURSOR: 1
zpty_run 'ITERM_SESSION_ID=w0t0p2:00000000-0000-0000-0000-000000000000'
zletest $'0\C-f'
zpty_run 'unset ITERM_SESSION_ID'
0:sync-incarg on tmux in pane 2
>BUFFER: 2
>CURSOR: 1
zpty_run 'TMUX_PANE=1'
zpty_run 'ITERM_SESSION_ID=w0t0p2:00000000-0000-0000-0000-000000000000'
zletest $'0\C-f'
zpty_run 'unset TMUX_PANE ITERM_SESSION_ID'
0:tmux pane number takes precedence over iTerm2's
>BUFFER: 1
>CURSOR: 1
zletest $'0\e2\C-n'
0:Providing a numeric argument will change the incremented amount
>BUFFER: 2
>CURSOR: 0
zpty_run 'incarg=3'
zletest $'0\e\C-n'
zpty_run 'unset incarg'
0:Setting the incarg variable will change the default incremented amount
>BUFFER: 3
>CURSOR: 0
zpty_run 'incarg=3'
zletest $'0\e2\C-n'
zpty_run 'unset incarg'
0:A numeric argument will take precedence over the incarg variable
>BUFFER: 2
>CURSOR: 0
zpty_run 'TMUX_PANE=2'
zletest $'0\e2\C-f'
zpty_run 'unset TMUX_PANE'
0:Providing a numeric argument will work for the sync- variants of incarg
>BUFFER: 4
>CURSOR: 0
zletest $'000\C-n'
0:Incrementing a decimal integer preserves leading zeros
>BUFFER: 001
>CURSOR: 3
zletest $'-001\C-n\C-n'
0:Leading zeros are preserved when the digit turns from negative to positive
>BUFFER: 001
>CURSOR: 3
zletest $'001\C-p\C-p'
0:Leading zeros are preserved when the digit turns from positive to negative
>BUFFER: -001
>CURSOR: 4
zletest $'001\e1000\C-n'
0:Incrementing an integer works when the result has more zeros than the original
>BUFFER: 1001
>CURSOR: 3
zletest $'001\e2000\C-p'
0:Decrementing an integer with leading zeros works when the result has more digits than the original
>BUFFER: -1999
>CURSOR: 4
zletest $'0b11\C-n'
0:Increment a binary integer
>BUFFER: 0b100
>CURSOR: 5
zletest $'0B11\C-n'
0:Increment a binary integer with an upper case prefix
>BUFFER: 0B100
>CURSOR: 5
zletest $'0b100\C-p'
0:Decrement a binary integer
>BUFFER: 0b11
>CURSOR: 4
zletest $'0b0011\C-n'
0:Increment a binary integer preserves leading zeros
>BUFFER: 0b0100
>CURSOR: 6
zletest $'0b001\e8\C-n'
0:Incrementing a binary integer work when the result has more zeros than the original
>BUFFER: 0b1001
>CURSOR: 5
zletest $'0b0\C-p'
0:Decrementing a binary integer to a negative value will fail
>BUFFER: 0b0
>CURSOR: 3
zletest $'0o7\C-n'
0:Increment an octal integer
>BUFFER: 0o10
>CURSOR: 4
zletest $'0O7\C-n'
0:Increment an octal integer with an upper case prefix
>BUFFER: 0O10
>CURSOR: 4
zletest $'0o10\C-p'
0:Decrement an octal integer
>BUFFER: 0o7
>CURSOR: 3
zletest $'0o0\C-p'
0:Decrementing an octal integer to a negative value will fail
>BUFFER: 0o0
>CURSOR: 3
zletest $'0x9\C-n'
0:Increment a hexadecimal integer
>BUFFER: 0xa
>CURSOR: 3
zletest $'0X9\C-n'
0:Increment a hexadecimal integer with an upper case prefix
>BUFFER: 0XA
>CURSOR: 3
zletest $'0xf\C-n'
0:Increment a hexadecimal integer with no numeric digit
>BUFFER: 0x10
>CURSOR: 4
zletest $'0x10\C-p'
0:Decrement a hexadecimal integer
>BUFFER: 0xf
>CURSOR: 3
zletest $'0x0\C-p'
0:Decrementing an octal integer to a negative value will fail
>BUFFER: 0x0
>CURSOR: 3
zletest $'0x0b1\C-n'
0:a number that starts with 0x0b is interpreted as a hexadecimal integer
>BUFFER: 0x0b2
>CURSOR: 5
zletest $'10x9\e0\C-n'
0:[0-9]0x[0-9a-f] will become [0-9]1x[0-9a-f] when incremented from the left of x
>BUFFER: 11x9
>CURSOR: 1
zletest $'10x9\eFx\C-n'
0:[0-9]0x[0-9a-f] will increment the hexadecimal 0x[0-9a-f] when the cursor is on x
>BUFFER: 10xa
>CURSOR: 3
zletest $'10x9\e\C-n'
0:[0-9]0x[0-9a-f] will increment the hexadecimal 0x[0-9a-f] when the cursor is on the right of x
>BUFFER: 10xa
>CURSOR: 3
zletest $'10b1\e0\C-n'
0:[0-9]0b[01] will become [0-9]1b[01] when incremented from the left of b
>BUFFER: 11b1
>CURSOR: 1
zletest $'10b1\eFb\C-n'
0:[0-9]0b[01] will increment the binary 0b[01] when the cursor is on b
>BUFFER: 10b10
>CURSOR: 4
zletest $'10b1\e\C-n'
0:[0-9]0b[01] will increment the binary 0b[01] when the cursor is on the right of b
>BUFFER: 10b10
>CURSOR: 4
zletest $'10o7\e0\C-n'
0:[0-9]0o[0-7] will become [0-9]1o[0-7] when incremented from the left of o
>BUFFER: 11o7
>CURSOR: 1
zletest $'10o7\eFo\C-n'
0:[0-9]0o[0-7] will increment the octal 0o[0-7] when the cursor is on o
>BUFFER: 10o10
>CURSOR: 4
zletest $'10o7\e\C-n'
0:[0-9]0o[0-7] will increment the octal 0o[0-7] when the cursor is on the right of o
>BUFFER: 10o10
>CURSOR: 4
zletest $'0b0x9\eF0\C-n'
0:0b0x[0-9a-f] will increment the binary 0b0 when the cursor is on the left of x
>BUFFER: 0b1x9
>CURSOR: 2
zletest $'0b0x9\eFx\C-n'
0:0b0x[0-9a-f] will increment the hexadecimal 0x[0-9a-f] when the cursor is on top of x
>BUFFER: 0b0xa
>CURSOR: 4
zletest $'0b0x9\e\C-n'
0:0b0x[0-9a-f] will increment the hexadecimal 0x[0-9a-f] when the cursor is on the right of x
>BUFFER: 0b0xa
>CURSOR: 4
zletest $'echo 012ab\eF i\C-n'
0:incarg does nothing when the cursor is placed just to the left of an integer
>BUFFER: echo 012ab
>CURSOR: 4
zletest $'echo 012ab\eF0i\C-n'
0:incarg works when the cursor is placed at the leftmost digit of an integer
>BUFFER: echo 013ab
>CURSOR: 8
zletest $'echo 012ab\eF1i\C-n'
0:incarg works when the cursor is placed at the inner digit of an integer
>BUFFER: echo 013ab
>CURSOR: 8
zletest $'echo 012ab\eF2i\C-n'
0:incarg works when the cursor is placed at the rightmost digit of an integer
>BUFFER: echo 013ab
>CURSOR: 8
zletest $'echo 012ab\eFai\C-n'
0:incarg works when the cursor is placed just to the right of an integer
>BUFFER: echo 013ab
>CURSOR: 8
zletest $'echo 012ab\ei\C-n'
0:incarg does nothing when the cursor is placed more than a single letter away to the right
>BUFFER: echo 012ab
>CURSOR: 9
zletest $'echo 012ab\eF \C-n'
0:vim-incarg works when the cursor is placed to the left of an integer
>BUFFER: echo 013ab
>CURSOR: 7
zletest $'echo 012ab\eF0\C-n'
0:vim-incarg works when the cursor is placed at the leftmost digit of an integer
>BUFFER: echo 013ab
>CURSOR: 7
zletest $'echo 012ab\eF1\C-n'
0:vim-incarg works when the cursor is placed at the inner digit of an integer
>BUFFER: echo 013ab
>CURSOR: 7
zletest $'echo 012ab\eF2\C-n'
0:incarg works when the cursor is placed at the rightmost digit of an integer
>BUFFER: echo 013ab
>CURSOR: 7
zletest $'echo 012ab\eFa\C-n'
0:vim-incarg does nothing when the cursor is placed to the right of an integer
>BUFFER: echo 012ab
>CURSOR: 8
zletest $'echo 012ab\ei\C-n'
0:vim-incarg does nothing when the cursor is placed more than a single letter away to the right
>BUFFER: echo 012ab
>CURSOR: 9
%clean
zmodload -ui zsh/zpty
Loading…
Cancel
Save