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-backward-incarg / vim-backward-decarg # # This behaves like vim-incarg & vim-decarg, but it searches backwards for a # number. # # - 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. # # - vim-backward-sync-incarg / vim-backward-sync-decarg # # This combines the behavior of the vim-backward- and sync- variants. # # Example Usage: # # autoload -Uz incarg # 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 zle -f vichange setopt localoptions extended_glob local match mbegin mend MATCH MBEGIN MEND i [[ -z "$BUFFER" ]] && return 1 # find the number and determine the base integer pos=$(( CURSOR + 1 )) base=0 # 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 if [[ "$WIDGET" == *backward-* ]]; then # search backwards for a number while true; 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 ;; *[0-9]) base=10 ;; *-) case "$BUFFER[pos,-1]" in -0[xX][0-9a-fA-F]*) ;; -0[oO][0-7]*) ;; -0[bB][01]*) ;; -[0-9]*) base=10 ;; esac ;; esac (( base != 0 )) && break (( pos-- )) (( pos <= 0 )) && return 1 done else # jump to the nearest number after the cursor while [[ "$BUFFER[pos]" == [^0-9] ]]; do (( pos++ )) (( pos > $#BUFFER )) && return 1 done fi 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 # 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 zle -M "[$WIDGET] unsupported terminal" return 1 fi (( delta *= pane_index )) fi local old="$BUFFER[first,last]" integer oldlen=$#BUFFER integer oldnum="$base#$old" 2> /dev/null # -00 should increment to 01 instead of 001 if [[ "$BUFFER[first]" == '-' ]] && (( oldnum == 0 )); then (( ndigits-- )) fi 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 # $(( )) outputs an error message to stderr when integer truncation occurs printf -v raw_result "%0$ndigits$fmt1" $(( [$fmt2] "$base#$old" + delta )) 2> /dev/null padded="${raw_result// /0}" integer newnum="$base#$padded" 2> /dev/null # try to detect integer truncation 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 if (( 0 < first && first <= last && last <= $#BUFFER )); then BUFFER[first,last]="$new" else zle -M "[$WIDGET] The detected location of the integer was invalid. [location=BUFFER[$first,$last]]" return 1 fi integer offset=0 if [[ "$WIDGET" == vi* ]]; then offset=-1 fi (( CURSOR = last + $#BUFFER - oldlen + offset )) return 0