# zsh-vi-mode.zsh -- A better and friendly vi(vim) mode for Zsh # https://github.com/jeffreytse/zsh-vi-mode # # Copyright (c) 2020 Jeffrey Tse # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # All Settings # Some of these variables should be set before sourcing this file. # # ZVM_CONFIG_FUNC # the config function (default is `zvm_config`), if this config function # exists, it will be called automatically, you can do some configurations # in this aspect before you source this plugin. # # For example: # # ```zsh # function zvm_config() { # ZVM_LINE_INIT_MODE=$ZVM_MODE_INSERT # ZVM_VI_INSERT_ESCAPE_BINDKEY=jk # } # # source ~/zsh-vi-mode.zsh # ``` # # ZVM_INIT_MODE # the plugin initial mode (default is doing the initialization when the first # new command line is starting. For doing the initialization instantly, you # can set it to `sourcing`. # # ZVM_VI_ESCAPE_BINDKEY # the vi escape key for all modes (default is ^[ => ), you can set it # to whatever you like, such as `jj`, `jk` and so on. # # ZVM_VI_INSERT_ESCAPE_BINDKEY # the vi escape key of insert mode (default is $ZVM_VI_ESCAPE_BINDKEY), you # can set it to whatever, such as `jj`, `jk` and so on. # # ZVM_VI_VISUAL_ESCAPE_BINDKEY # the vi escape key of visual mode (default is $ZVM_VI_ESCAPE_BINDKEY), you # can set it to whatever, such as `jj`, `jk` and so on. # # ZVM_VI_OPPEND_ESCAPE_BINDKEY # the vi escape key of operator pendding mode (default is # $ZVM_VI_ESCAPE_BINDKEY), you can set it to whatever, such as `jj`, `jk` # and so on. # # ZVM_VI_INSERT_MODE_LEGACY_UNDO: # using legacy undo behavior in vi insert mode (default is false) # # ZVM_VI_HIGHLIGHT_FOREGROUND: # the behavior of highlight foreground (surrounds, visual-line, etc) in vi mode # # ZVM_VI_HIGHLIGHT_BACKGROUND: # the behavior of highlight background (surrounds, visual-line, etc) in vi mode # # ZVM_VI_HIGHLIGHT_EXTRASTYLE: # the behavior of highlight extra style (i.e. bold, underline) in vi mode # # For example: # ZVM_VI_HIGHLIGHT_FOREGROUND=green # Color name # ZVM_VI_HIGHLIGHT_FOREGROUND=#008800 # Hex value # ZVM_VI_HIGHLIGHT_BACKGROUND=red # Color name # ZVM_VI_HIGHLIGHT_BACKGROUND=#ff0000 # Hex value # ZVM_VI_HIGHLIGHT_EXTRASTYLE=bold,underline # bold and underline # # ZVM_VI_SURROUND_BINDKEY # the key binding mode for surround operating (default is 'classic') # # 1. 'classic' mode (verb->s->surround): # S" Add " for visual selection # ys" Add " for visual selection # cs"' Change " to ' # ds" Delete " # # 2. 's-prefix' mode (s->verb->surround): # sa" Add " for visual selection # sd" Delete " # sr"' Change " to ' # # How to select surround text object? # vi" Select the text object inside the quotes # va( Select the text object including the brackets # # Then you can do any operation for the selection: # # 1. Add surrounds for text object # vi" -> S[ or sa[ => "object" -> "[object]" # # 2. Delete/Yank/Change text object # di( or vi( -> d # ca( or va( -> c # yi( or vi( -> y # # ZVM_READKEY_ENGINE # the readkey engine for reading and processing the key events, and the # below engines are supported: # ZVM_READKEY_ENGINE_NEX (Default) # ZVM_READKEY_ENGINE_ZLE # # the NEX is a better engine for reading and handling the key events than # the Zsh's ZLE engine, currently the NEX engine is at beta stage, and # you can change to Zsh's ZLE engine if you want. # # ZVM_KEYTIMEOUT: # the key input timeout for waiting for next key (default is 0.4 seconds) # # ZVM_ESCAPE_KEYTIMEOUT: # the key input timeout for waiting for next key if it is beginning with # an escape character (default is 0.03 seconds), and this option is just # available for the NEX readkey engine # # ZVM_LINE_INIT_MODE # the setting for init mode of command line (default is empty), empty will # keep the last command mode, for the first command line it will be insert # mode, you can also set it to a specific vi mode to always keep the mode # for each command line # # For example: # ZVM_LINE_INIT_MODE=$ZVM_MODE_INSERT # ZVM_LINE_INIT_MODE=$ZVM_MODE_NORMAL # # ZVM_LAZY_KEYBINDINGS: # the setting for lazy keybindings (default is true), and lazy keybindings # will postpone the keybindings of vicmd and visual keymaps to the first # time entering normal mode # # ZVM_NORMAL_MODE_CURSOR: # the prompt cursor in normal mode # # ZVM_INSERT_MODE_CURSOR: # the prompt cursor in insert mode # # ZVM_VISUAL_MODE_CURSOR: # the prompt cursor in visual mode # # ZVM_VISUAL_LINE_MODE_CURSOR: # the prompt cursor in visual line mode # # ZVM_OPPEND_MODE_CURSOR: # the prompt cursor in operator pending mode # # You can change the cursor style by below: # ZVM_INSERT_MODE_CURSOR=$ZVM_CURSOR_BLOCK # # and the below cursor style are supported: # ZVM_CURSOR_USER_DEFAULT # ZVM_CURSOR_BLOCK # ZVM_CURSOR_UNDERLINE # ZVM_CURSOR_BEAM # ZVM_CURSOR_BLINKING_BLOCK # ZVM_CURSOR_BLINKING_UNDERLINE # ZVM_CURSOR_BLINKING_BEAM # # ZVM_VI_EDITOR # the editor to edit your command line (default is $EDITOR) # # ZVM_TMPDIR # the temporary directory (default is $TMPDIR, otherwise it's /tmp) # # ZVM_TERM # the term for handling terminal sequences, it's important for some # terminal emulators to show cursor properly (default is $TERM) # # ZVM_CURSOR_STYLE_ENABLED # enable the cursor style feature (default is true) # # ZVM_SYSTEM_CLIPBOARD_ENABLED # enable the system clipboard feature (default is false), if you want to enable # it, you should also set the copy and paste commands below: # # ZVM_CLIPBOARD_COPY_CMD # the command for copying text to system clipboard # # ZVM_CLIPBOARD_PASTE_CMD # the command for pasting text from system clipboard # # For example: # - For macOS, you can set it to `pbcopy` and `pbpaste` # - For Linux, you can set it to `xclip -selection clipboard` and # `xclip -selection clipboard -o`) # - For Wayland, you can also use `wl-copy` and `wl-paste` # - For WSL, you can also use `clip.exe` # # If you don't set these two commands, the plugin will try to detect them # automatically for you. # # ZVM_OPEN_CMD # the command for opening URL or file path (e.g. `xdg-open`, `open`, `start` # and so on) # # ZVM_OPEN_URL_CMD # the command for opening URL (default is $ZVM_OPEN_CMD) # # ZVM_OPEN_FILE_CMD # the command for opening file path (default is $ZVM_OPEN_CMD) # # Avoid sourcing plugin multiple times command -v 'zvm_version' >/dev/null && return # Plugin information typeset -gr ZVM_NAME='zsh-vi-mode' typeset -gr ZVM_DESCRIPTION='💻 A better and friendly vi(vim) mode plugin for ZSH.' typeset -gr ZVM_REPOSITORY='https://github.com/jeffreytse/zsh-vi-mode' typeset -gr ZVM_VERSION='0.12.0' # Plugin initial status ZVM_INIT_DONE=false # Postpone reset prompt (i.e. postpone the widget `reset-prompt`) # -1 (No postponing) # >=0 (Postponing, the decimal value stands for calling times of `reset-prompt`) ZVM_POSTPONE_RESET_PROMPT=-1 # Disable reset prompt (i.e. postpone the widget `reset-prompt`) ZVM_RESET_PROMPT_DISABLED=false # Operator pending mode ZVM_OPPEND_MODE=false # Insert mode could be # `i` (insert) # `a` (append) # `I` (insert at the non-blank beginning of current line) # `A` (append at the end of current line) ZVM_INSERT_MODE='i' # The mode could be the below value: # `n` (normal) # `i` (insert) # `v` (visual) # `vl` (visual-line) ZVM_MODE='' # The keys typed to invoke this widget, as a literal string ZVM_KEYS='' # The region highlight information ZVM_REGION_HIGHLIGHT=() # Default zvm readkey engines ZVM_READKEY_ENGINE_NEX='nex' ZVM_READKEY_ENGINE_ZLE='zle' ZVM_READKEY_ENGINE_DEFAULT=$ZVM_READKEY_ENGINE_NEX # Default alternative character for escape characters ZVM_ESCAPE_SPACE='\s' ZVM_ESCAPE_NEWLINE='^J' # Default vi modes ZVM_MODE_LAST='' ZVM_MODE_NORMAL='n' ZVM_MODE_INSERT='i' ZVM_MODE_VISUAL='v' ZVM_MODE_VISUAL_LINE='vl' ZVM_MODE_REPLACE='r' # Default cursor styles ZVM_CURSOR_USER_DEFAULT='ud' ZVM_CURSOR_BLOCK='bl' ZVM_CURSOR_UNDERLINE='ul' ZVM_CURSOR_BEAM='be' ZVM_CURSOR_BLINKING_BLOCK='bbl' ZVM_CURSOR_BLINKING_UNDERLINE='bul' ZVM_CURSOR_BLINKING_BEAM='bbe' # The commands need to be repeated ZVM_REPEAT_MODE=false ZVM_REPEAT_RESET=false ZVM_REPEAT_COMMANDS=($ZVM_MODE_NORMAL i) # Range handling return values ZVM_RANGE_HANDLER_RET_OK=0 ZVM_RANGE_HANDLER_RET_CONTINUE=1 ZVM_RANGE_HANDLER_RET_PUSHBACK=2 ZVM_RANGE_HANDLER_RET_CANCEL=3 # URL regex pattern ZVM_URL_SCHEME='^(http(s)?:\/\/.)?(ftp(s)?:\/\/.)?(file:\/\/.)?(www\.)?' ZVM_URL_HOST='[-a-zA-Z0-9@:%._\+~#=]{0,255}\.[a-z]{2,6}' ZVM_URL_PATH='([-a-zA-Z0-9@:%_\+.~#?&\/=]*)$' ZVM_URL_REGEX="${ZVM_URL_SCHEME}${ZVM_URL_HOST}${ZVM_URL_PATH}" ########################################## # Initial all default settings # Default config function : ${ZVM_CONFIG_FUNC:='zvm_config'} # Set the readkey engine (default is NEX engine) : ${ZVM_READKEY_ENGINE:=$ZVM_READKEY_ENGINE_DEFAULT} # Set key input timeout (default is 0.4 seconds) : ${ZVM_KEYTIMEOUT:=0.4} # Set the escape key timeout (default is 0.03 seconds) : ${ZVM_ESCAPE_KEYTIMEOUT:=0.03} # Set keybindings mode (default is true) # The lazy keybindings will post the keybindings of vicmd and visual # keymaps to the first time entering the normal mode : ${ZVM_LAZY_KEYBINDINGS:=true} # All keybindings for lazy loading if $ZVM_LAZY_KEYBINDINGS; then ZVM_LAZY_KEYBINDINGS_LIST=() fi # Set the cursor style in different vi modes, the value you could use # the predefined value, such as $ZVM_CURSOR_BLOCK, $ZVM_CURSOR_BEAM, # $ZVM_CURSOR_BLINKING_BLOCK and so on. : ${ZVM_INSERT_MODE_CURSOR:=$ZVM_CURSOR_BEAM} : ${ZVM_NORMAL_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} : ${ZVM_VISUAL_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} : ${ZVM_VISUAL_LINE_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} # Operator pending mode cursor style (default is underscore) : ${ZVM_OPPEND_MODE_CURSOR:=$ZVM_CURSOR_UNDERLINE} # Set the vi escape key (default is ^[ => ) : ${ZVM_VI_ESCAPE_BINDKEY:=^[} : ${ZVM_VI_INSERT_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} : ${ZVM_VI_VISUAL_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} : ${ZVM_VI_OPPEND_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} # Set the line init mode (empty will keep the last mode) # you can also set it to others, such as $ZVM_MODE_INSERT. : ${ZVM_LINE_INIT_MODE:=$ZVM_MODE_LAST} : ${ZVM_VI_INSERT_MODE_LEGACY_UNDO:=false} : ${ZVM_VI_SURROUND_BINDKEY:=classic} : ${ZVM_VI_HIGHLIGHT_BACKGROUND:=#cc0000} : ${ZVM_VI_HIGHLIGHT_FOREGROUND:=#eeeeee} : ${ZVM_VI_HIGHLIGHT_EXTRASTYLE:=default} : ${ZVM_VI_EDITOR:=${EDITOR:-vim}} : ${ZVM_TMPDIR:=${TMPDIR:-/tmp}} # Set the term for handling terminal sequences, it's important for some # terminal emulators to show cursor properly (default is $TERM) : ${ZVM_TERM:=${TERM:-xterm-256color}} # Enable the cursor style feature : ${ZVM_CURSOR_STYLE_ENABLED:=true} # Enable system clipboard feature : ${ZVM_SYSTEM_CLIPBOARD_ENABLED:=false} : ${ZVM_CLIPBOARD_COPY_CMD:=} : ${ZVM_CLIPBOARD_PASTE_CMD:=} # Open URL or file path feature : ${ZVM_OPEN_CMD:=} : ${ZVM_OPEN_URL_CMD:=${ZVM_OPEN_CMD:-}} : ${ZVM_OPEN_FILE_CMD:=${ZVM_OPEN_CMD:-}} # All the extra commands commands_array_names=( zvm_before_init_commands zvm_after_init_commands zvm_before_select_vi_mode_commands zvm_after_select_vi_mode_commands zvm_before_lazy_keybindings_commands zvm_after_lazy_keybindings_commands ) for commands_array_name in $commands_array_names; do # Ensure commands set to an empty array, if not already set. if [[ -z "${(P)commands_array_name}" ]]; then typeset -g -a $commands_array_name fi done # All the handlers for switching keyword zvm_switch_keyword_handlers=( zvm_switch_number zvm_switch_boolean zvm_switch_operator zvm_switch_weekday zvm_switch_month ) # History for switching keyword zvm_switch_keyword_history=() # Display version information function zvm_version() { local git_info=$(git show -s --format="(%h, %ci)" 2>/dev/null) echo -e "$ZVM_NAME $ZVM_VERSION $git_info" echo -e "\e[4m$ZVM_REPOSITORY\e[0m" echo -e "$ZVM_DESCRIPTION" } # The widget wrapper function zvm_widget_wrapper() { local rawfunc=$1; local func=$2; local called=$3; local -i retval=0 if ! $called; then $rawfunc "${@:4}" retval=$? fi $func "${@:4}" [[ $retval -eq 0 ]] && retval=$? return $retval } # Define widget function function zvm_define_widget() { local widget=$1 local func=$2 || $1 local result=($(zle -l -L "${widget}")) # Check if existing the same name if [[ ${#result[@]} == 4 ]]; then local rawfunc=${result[4]} local wrapper="zvm_${widget}-wrapper" # To avoid double calling, we need to check if the raw function # has been called already in the custom widget function local rawcode=$(declare -f $func 2>/dev/null) local called=false [[ "$rawcode" == *"\$rawfunc"* ]] && { called=true } eval "$wrapper() { zvm_widget_wrapper $rawfunc $func $called \"\$@\" }" func=$wrapper fi zle -N $widget $func } # Get the keys typed to invoke this widget, as a literal string function zvm_keys() { local keys=${ZVM_KEYS:-$KEYS} # Append the prefix of keys if it is visual or visual-line mode case "${ZVM_MODE}" in $ZVM_MODE_VISUAL) if [[ "$keys" != v* ]]; then keys="v${keys}" fi ;; $ZVM_MODE_VISUAL_LINE) if [[ "$keys" != V* ]]; then keys="V${keys}" fi ;; esac # Escape the newline and space characters, otherwise, we can't # get the output from subshell correctly. keys=${keys//$'\n'/$ZVM_ESCAPE_NEWLINE} keys=${keys// /$ZVM_ESCAPE_SPACE} echo $keys } # Find the widget on a specified bindkey function zvm_find_bindkey_widget() { local keymap=$1 local keys=$2 local prefix_mode=${3:-false} retval=() if $prefix_mode; then local pos=0 local spos=3 local prefix_keys=$keys # Get the prefix keys if [[ $prefix_keys ]]; then prefix_keys=${prefix_keys:0:-1} # If the last key is an escape key (e.g. \", \`, \\) we still # need to remove the escape backslash `\` if [[ ${prefix_keys: -1} == '\' ]]; then prefix_keys=${prefix_keys:0:-1} fi fi local result=$(bindkey -M ${keymap} -p "$prefix_keys")$'\n' # Split string to array by newline local i= for ((i=$spos;i<$#result;i++)); do # Save the last whitespace character of the line # and continue continue handling while meeting `\n` case "${result:$i:1}" in ' ') spos=$i; i=$i+1; continue;; [$'\n']);; *) continue;; esac # Check if it has the same prefix keys and retrieve the widgets if [[ "${result:$((pos+1)):$#keys}" == "$keys" ]]; then # Get the binding keys local k=${result:$((pos+1)):$((spos-pos-2))} # Escape spaces in key bindings (space -> $ZVM_ESCAPE_SPACE) k=${k// /$ZVM_ESCAPE_SPACE} retval+=($k ${result:$((spos+1)):$((i-spos-1))}) fi # Save as new position pos=$i+1 # Skip 3 characters # One key and quotes at least (i.e \n"_" ) i=$i+3 done else local result=$(bindkey -M ${keymap} "$keys") if [[ "${result: -14}" == ' undefined-key' ]]; then return fi # Escape spaces in key bindings (space -> $ZVM_ESCAPE_SPACE) local i= for ((i=$#result;i>=0;i--)); do # Backward find the first whitespace character [[ "${result:$i:1}" == ' ' ]] || continue # Retrieve the keys and widget local k=${result:1:$i-2} # Escape spaces in key bindings (space -> $ZVM_ESCAPE_SPACE) k=${k// /$ZVM_ESCAPE_SPACE} retval+=($k ${result:$i+1}) break done fi } # Read keys for retrieving widget function zvm_readkeys() { local keymap=$1 local key=${2:-$(zvm_keys)} local keys= local widget= local result= local pattern= local timeout= while :; do # Keep reading key for escape character if [[ "$key" == $'\e' ]]; then while :; do local k= read -t $ZVM_ESCAPE_KEYTIMEOUT -k 1 k || break key="${key}${k}" done fi keys="${keys}${key}" # Handle the pattern if [[ -n "$key" ]]; then # Transform the non-printed characters local k=$(zvm_escape_non_printed_characters "${key}") # Escape keys # " -> \" It's a special character in bash syntax # ` -> \` It's a special character in bash syntax # -> ` ` It's a special character in bash syntax k=${k//\"/\\\"} k=${k//\`/\\\`} k=${k//$ZVM_ESCAPE_SPACE/ } pattern="${pattern}${k}" fi # Find out widgets that match this key pattern zvm_find_bindkey_widget $keymap "$pattern" true result=(${retval[@]}) # Exit key input if there is only one widget matched # or no more widget matched. case ${#result[@]} in 2) key=; widget=${result[2]}; break;; 0) break;; esac # Evaluate the readkey timeout # Special timeout for the escape sequence if [[ "${keys}" == $'\e' ]]; then timeout=$ZVM_ESCAPE_KEYTIMEOUT # Check if there is any one custom escape sequence local i= for ((i=1; i<=${#result[@]}; i=i+2)); do if [[ "${result[$i]}" =~ '^\^\[\[?[A-Z0-9]*~?\^\[' ]]; then timeout=$ZVM_KEYTIMEOUT break fi done else timeout=$ZVM_KEYTIMEOUT fi # Wait for reading next key, and we should save the widget # as the final widget if it is full matching key= if [[ "${result[1]}" == "${pattern}" ]]; then widget=${result[2]} # Get current widget as final widget when reading key timeout read -t $timeout -k 1 key || break else zvm_enter_oppend_mode read -k 1 key fi done # Exit operator pending mode if $ZVM_OPPEND_MODE; then zvm_exit_oppend_mode fi if [[ -z "$key" ]]; then retval=(${keys} $widget) else retval=(${keys:0:-$#key} $widget $key) fi } # Add key bindings function zvm_bindkey() { local keymap=$1 local keys=$2 local widget=$3 local params=$4 local key= # We should bind keys with an existing widget [[ -z $widget ]] && return # If lazy keybindings is enabled, we need to add to the lazy list if [[ ${ZVM_LAZY_KEYBINDINGS_LIST+x} && ${keymap} != viins ]]; then keys=${keys//\"/\\\"} keys=${keys//\`/\\\`} ZVM_LAZY_KEYBINDINGS_LIST+=( "${keymap} \"${keys}\" ${widget} \"${params}\"" ) return fi # Handle the keybinding of NEX readkey engine if [[ $ZVM_READKEY_ENGINE == $ZVM_READKEY_ENGINE_NEX ]]; then # Get the first key (especially check if ctrl characters) if [[ $#keys -gt 1 && "${keys:0:1}" == '^' ]]; then key=${keys:0:2} else key=${keys:0:1} # As any character that is not bound to one of the history control # related functions, or self-insert or self-insert-unmeta, will # cause the mode to be exited To prevent history search, so that # we need to bind keys explicitly. if [[ "$keymap" == "viins" ]]; then bindkey -M isearch "${key}" self-insert fi fi bindkey -M $keymap "${key}" zvm_readkeys_handler fi # Wrap params to a new widget if [[ -n $params ]]; then local suffix=$(zvm_string_to_hex $params) eval "$widget:$suffix() { $widget $params }" widget="$widget:$suffix" zvm_define_widget $widget fi # Bind keys with with a widget bindkey -M $keymap "${keys}" $widget } # Convert string to hexadecimal function zvm_string_to_hex() { local str= i= for ((i=1;i<=$#1;i++)); do str+=$(printf '%x' "'${1[$i]}") done echo "$str" } # Escape non-printed characters function zvm_escape_non_printed_characters() { local str= i= for ((i=0;i<$#1;i++)); do local c=${1:$i:1} if [[ "$c" < ' ' ]]; then local ord=$(($(printf '%d' "'$c")+64)) c=$(printf \\$(printf '%03o' $ord)) str="${str}^${c}" elif [[ "$c" == '' ]]; then str="${str}^?" elif [[ "$c" == '' ]]; then str="${str}^@" else str="${str}${c}" fi done # Escape the newline and space characters, otherwise, we can't # get the output from subshell correctly. str=${str// /$ZVM_ESCAPE_SPACE} str=${str//$'\n'/$ZVM_ESCAPE_NEWLINE} echo -n $str } # Backward remove characters of an emacs region in the line function zvm_backward_kill_region() { local bpos=$CURSOR-1 epos=$CURSOR # Backward search the boundary of current region for ((; bpos >= 0; bpos--)); do # Break when cursor is at the beginning of line [[ "${BUFFER:$bpos:1}" == $'\n' ]] && break # Break when cursor is at the boundary of a word region [[ "${BUFFER:$bpos:2}" =~ ^\ [^\ $'\n']$ ]] && break done bpos=$bpos+1 CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$bpos zvm_clipboard_copy_buffer } # Remove all characters between the cursor position and the # beginning of the line. function zvm_backward_kill_line() { BUFFER=${BUFFER:$CURSOR:$#BUFFER} CURSOR=0 } # Remove all characters between the cursor position and the # end of the line. function zvm_forward_kill_line() { BUFFER=${BUFFER:0:$CURSOR} } # Remove all characters of the line. function zvm_kill_line() { local ret=($(zvm_calc_selection $ZVM_MODE_VISUAL_LINE)) local bpos=${ret[1]} epos=${ret[2]} CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))}$'\n' BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$bpos zvm_clipboard_copy_buffer } # Remove all characters of the whole line. function zvm_kill_whole_line() { local ret=($(zvm_calc_selection $ZVM_MODE_VISUAL_LINE)) local bpos=$ret[1] epos=$ret[2] cpos=$ret[3] CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))}$'\n' # Adjust region range of deletion if (( $epos < $#BUFFER )); then epos=$epos+1 fi BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$cpos zvm_clipboard_copy_buffer } # Exchange the point and mark function zvm_exchange_point_and_mark() { cursor=$MARK MARK=$CURSOR CURSOR=$cursor zvm_highlight } # Open line below function zvm_open_line_below() { local i=$CURSOR # If there is a completion suffix, we should break at the # position of suffix begin, otherwise, it should break when # forward finding out the first newline character. for ((; i<$#BUFFER; i++)); do if ((SUFFIX_ACTIVE == 1)) && ((i >= SUFFIX_BEGIN)); then break fi if [[ "${BUFFER[$i]}" == $'\n' ]]; then i=$((i-1)) break fi done CURSOR=$i LBUFFER+=$'\n' zvm_reset_repeat_commands $ZVM_MODE_NORMAL o zvm_select_vi_mode $ZVM_MODE_INSERT } # Open line above function zvm_open_line_above() { local i=$CURSOR # Break when backward finding out the first newline character. for ((; i>0; i--)); do if [[ "${BUFFER[$i]}" == $'\n' ]]; then break fi done CURSOR=$i LBUFFER+=$'\n' CURSOR=$((CURSOR-1)) zvm_reset_repeat_commands $ZVM_MODE_NORMAL O zvm_select_vi_mode $ZVM_MODE_INSERT } # Replace characters one by one (Replacing mode) function zvm_vi_replace() { if [[ $ZVM_MODE == $ZVM_MODE_NORMAL ]]; then local cursor=$CURSOR local cache=() local cmds=() local key= zvm_select_vi_mode $ZVM_MODE_REPLACE while :; do # Read a character for replacing zvm_update_cursor # Redisplay the command line, this is to be called from within # a user-defined widget to allow changes to become visible zle -R read -k 1 key # Escape key will break the replacing process, and enter key # will replace with a newline character. case $(zvm_escape_non_printed_characters $key) in '^['|$ZVM_VI_OPPEND_ESCAPE_BINDKEY) break;; '^M') key=$'\n';; esac # If the key is backspace, we should move backward the cursor if [[ $key == '' ]]; then # Cursor position should not be less than zero if ((cursor > 0)); then cursor=$((cursor-1)) fi # We should recover the character when cache size is not zero if ((${#cache[@]} > 0)); then key=${cache[-1]} if [[ $key == '' ]]; then key= fi cache=(${cache[@]:0:-1}) BUFFER[$cursor+1]=$key # Remove from commands cmds=(${cmds[@]:0:-1}) fi else # If the key or the character at cursor is a newline character, # or the cursor is at the end of buffer, we should insert the # key instead of replacing with the key. if [[ $key == $'\n' || $BUFFER[$cursor+1] == $'\n' || $BUFFER[$cursor+1] == '' ]]; then cache+=('') LBUFFER+=$key else cache+=(${BUFFER[$cursor+1]}) BUFFER[$cursor+1]=$key fi cursor=$((cursor+1)) # Push to commands cmds+=($key) fi # Update next cursor position CURSOR=$cursor zle redisplay done # The cursor position should go back one character after # exiting the replace mode zle vi-backward-char zvm_select_vi_mode $ZVM_MODE_NORMAL zvm_reset_repeat_commands $ZVM_MODE R $cmds elif [[ $ZVM_MODE == $ZVM_MODE_VISUAL ]]; then zvm_enter_visual_mode V zvm_vi_change elif [[ $ZVM_MODE == $ZVM_MODE_VISUAL_LINE ]]; then zvm_vi_change fi } # Replace characters in one time function zvm_vi_replace_chars() { local cmds=() local key= # Read a character for replacing zvm_enter_oppend_mode # Redisplay the command line, this is to be called from within # a user-defined widget to allow changes to become visible zle redisplay zle -R read -k 1 key zvm_exit_oppend_mode # Escape key will break the replacing process case $(zvm_escape_non_printed_characters $key) in $ZVM_VI_OPPEND_ESCAPE_BINDKEY) zvm_exit_visual_mode return esac if [[ $ZVM_MODE == $ZVM_MODE_NORMAL ]]; then cmds+=($key) BUFFER[$CURSOR+1]=$key else local ret=($(zvm_calc_selection)) local bpos=${ret[1]} epos=${ret[2]} for ((bpos=bpos+1; bpos<=epos; bpos++)); do # Newline character is no need to be replaced if [[ $BUFFER[$bpos] == $'\n' ]]; then cmds+=($'\n') continue fi cmds+=($key) BUFFER[$bpos]=$key done zvm_exit_visual_mode fi # Reset the repeat commands zvm_reset_repeat_commands $ZVM_MODE r $cmds } # Substitute characters of selection function zvm_vi_substitute() { # Substitute one character in normal mode if [[ $ZVM_MODE == $ZVM_MODE_NORMAL ]]; then BUFFER="${BUFFER:0:$CURSOR}${BUFFER:$((CURSOR+1))}" zvm_reset_repeat_commands $ZVM_MODE c 0 1 zvm_select_vi_mode $ZVM_MODE_INSERT else zvm_vi_change fi } # Substitute all characters of a line function zvm_vi_substitute_whole_line() { zvm_select_vi_mode $ZVM_MODE_VISUAL_LINE false zvm_vi_substitute } # Check if cursor is at an empty line function zvm_is_empty_line() { local cursor=${1:-$CURSOR} if [[ ${BUFFER:$cursor:1} == $'\n' && ${BUFFER:$((cursor-1)):1} == $'\n' ]]; then return fi return 1 } # Get the beginning and end position of selection function zvm_selection() { local bpos= epos= if (( MARK > CURSOR )) ; then bpos=$CURSOR epos=$((MARK+1)) else bpos=$MARK epos=$((CURSOR+1)) fi echo $bpos $epos } # Calculate the region of selection function zvm_calc_selection() { local ret=($(zvm_selection)) local bpos=${ret[1]} epos=${ret[2]} cpos= # Save the current cursor position cpos=$bpos # Check if it is visual-line mode if [[ "${1:-$ZVM_MODE}" == $ZVM_MODE_VISUAL_LINE ]]; then # Extend the selection to whole line for ((bpos=$bpos-1; $bpos>0; bpos--)); do if [[ "${BUFFER:$bpos:1}" == $'\n' ]]; then bpos=$((bpos+1)) break fi done for ((epos=$epos-1; $epos<$#BUFFER; epos++)); do if [[ "${BUFFER:$epos:1}" == $'\n' ]]; then break fi done # The begin position must not be less than zero if (( bpos < 0 )); then bpos=0 fi ########################################### # Calculate the new cursor position, here we consider that # the selection will be delected. # Calculate the indent of current cursor line for ((cpos=$((CURSOR-1)); $cpos>=0; cpos--)); do [[ "${BUFFER:$cpos:1}" == $'\n' ]] && break done local indent=$((CURSOR-cpos-1)) # If the selection includes the last line, the cursor # will move up to above line. Otherwise the cursor will # keep in the same line. local hpos= # Line head position local rpos= # Reference position if (( $epos < $#BUFFER )); then # Get the head position of next line hpos=$((epos+1)) rpos=$bpos else # Get the head position of above line for ((hpos=$((bpos-2)); $hpos>0; hpos--)); do if [[ "${BUFFER:$hpos:1}" == $'\n' ]]; then break fi done if (( $hpos < -1 )); then hpos=-1 fi hpos=$((hpos+1)) rpos=$hpos fi # Calculate the cursor position, the indent must be # less than the line characters. for ((cpos=$hpos; $cpos<$#BUFFER; cpos++)); do if [[ "${BUFFER:$cpos:1}" == $'\n' ]]; then break fi if (( $hpos + $indent <= $cpos )); then break fi done cpos=$((rpos+cpos-hpos)) fi echo $bpos $epos $cpos } # Yank characters of the marked region function zvm_yank() { local ret=($(zvm_calc_selection $1)) local bpos=$ret[1] epos=$ret[2] cpos=$ret[3] CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} if [[ ${1:-$ZVM_MODE} == $ZVM_MODE_VISUAL_LINE ]]; then CUTBUFFER=${CUTBUFFER}$'\n' fi CURSOR=$bpos MARK=$epos zvm_clipboard_copy_buffer } # Up case of the visual selection function zvm_vi_up_case() { local ret=($(zvm_selection)) local bpos=${ret[1]} epos=${ret[2]} local content=${BUFFER:$bpos:$((epos-bpos))} BUFFER="${BUFFER:0:$bpos}${(U)content}${BUFFER:$epos}" zvm_exit_visual_mode } # Down case of the visual selection function zvm_vi_down_case() { local ret=($(zvm_selection)) local bpos=${ret[1]} epos=${ret[2]} local content=${BUFFER:$bpos:$((epos-bpos))} BUFFER="${BUFFER:0:$bpos}${(L)content}${BUFFER:$epos}" zvm_exit_visual_mode } # Opposite case of the visual selection function zvm_vi_opp_case() { local ret=($(zvm_selection)) local bpos=${ret[1]} epos=${ret[2]} local content=${BUFFER:$bpos:$((epos-bpos))} local i= for ((i=1; i<=$#content; i++)); do if [[ ${content[i]} =~ [A-Z] ]]; then content[i]=${(L)content[i]} elif [[ ${content[i]} =~ [a-z] ]]; then content[i]=${(U)content[i]} fi done BUFFER="${BUFFER:0:$bpos}${content}${BUFFER:$epos}" zvm_exit_visual_mode } # Yank characters of the visual selection function zvm_vi_yank() { zvm_yank zvm_exit_visual_mode ${1:-true} } # Put cutbuffer after the cursor function zvm_vi_put_after() { local count=${NUMERIC:-1} local head= foot= local content=${CUTBUFFER} local offset=1 if [[ ${content: -1} == $'\n' ]]; then local pos=${CURSOR} # Find the end of current line for ((; $pos<$#BUFFER; pos++)); do if [[ ${BUFFER:$pos:1} == $'\n' ]]; then pos=$pos+1 break fi done head=${BUFFER:0:$pos} foot=${BUFFER:$pos} # If at end of buffer (no trailing newline), prepend one and drop trailing one if ! zvm_is_empty_line; then if [[ $pos == $#BUFFER ]]; then content=$'\n'${content:0:-1} pos=$pos+1 head=${BUFFER:0:$pos} foot=${BUFFER:$pos} fi fi local repeated= i= for ((i=1; i<=count; i++)); do repeated+="$content" done offset=0 BUFFER="${head}${repeated}${foot}" CURSOR=$pos else local char_at_cursor=${BUFFER:$CURSOR:1} # Special handling if cursor at an empty line if zvm_is_empty_line; then head="${BUFFER:0:$((CURSOR-1))}" foot="${BUFFER:$CURSOR}" else head="${BUFFER:0:$CURSOR}" foot="${BUFFER:$((CURSOR+1))}" fi local repeated= i= for ((i=1; i<=count; i++)); do repeated+="$content" done BUFFER="${head}${char_at_cursor}${repeated}${foot}" CURSOR=$CURSOR+$#repeated fi # Refresh display and highlight buffer zvm_highlight clear zvm_highlight custom $(($#head+$offset)) $(($#head+$#repeated+$offset)) } # Put cutbuffer before the cursor function zvm_vi_put_before() { local count=${NUMERIC:-1} local head= foot= local content=${CUTBUFFER} if [[ ${content: -1} == $'\n' ]]; then local pos=$CURSOR # Find the beginning of current line for ((; $pos>0; pos--)); do if [[ "${BUFFER:$pos:1}" == $'\n' ]]; then pos=$pos+1 break fi done # Check if it is an empty line if zvm_is_empty_line; then head=${BUFFER:0:$((pos-1))} foot=$'\n'${BUFFER:$pos} pos=$((pos-1)) else head=${BUFFER:0:$pos} foot=${BUFFER:$pos} fi local repeated= i= for ((i=1; i<=count; i++)); do repeated+="$content" done BUFFER="${head}${repeated}${foot}" CURSOR=$pos else head="${BUFFER:0:$CURSOR}" foot="${BUFFER:$((CURSOR+1))}" local repeated= i= for ((i=1; i<=count; i++)); do repeated+="$content" done BUFFER="${head}${repeated}${BUFFER:$CURSOR:1}${foot}" CURSOR=$CURSOR+$#repeated CURSOR=$((CURSOR-1)) fi # Refresh display and highlight buffer zvm_highlight clear zvm_highlight custom $#head $(($#head+$#repeated)) } # Replace a selection function zvm_replace_selection() { local ret=($(zvm_calc_selection)) local bpos=$ret[1] epos=$ret[2] cpos=$ret[3] local cutbuf=$1 # If there's a replacement, we need to calculate cursor position if (( $#cutbuf > 0 )); then cpos=$(($bpos + $#cutbuf - 1)) fi CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} # Check if it is visual line mode if [[ $ZVM_MODE == $ZVM_MODE_VISUAL_LINE ]]; then if (( $epos < $#BUFFER )); then epos=$epos+1 elif (( $bpos > 0 )); then bpos=$bpos-1 fi CUTBUFFER=${CUTBUFFER}$'\n' fi BUFFER="${BUFFER:0:$bpos}${cutbuf}${BUFFER:$epos}" CURSOR=$cpos zvm_clipboard_copy_buffer } # Replace characters of the visual selection function zvm_vi_replace_selection() { zvm_replace_selection $CUTBUFFER zvm_exit_visual_mode ${1:-true} } # Delete characters of the visual selection function zvm_vi_delete() { zvm_replace_selection zvm_exit_visual_mode ${1:-true} } # Yank characters of the visual selection function zvm_vi_change() { local ret=($(zvm_calc_selection)) local bpos=$ret[1] epos=$ret[2] CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} # Check if it is visual line mode if [[ $ZVM_MODE == $ZVM_MODE_VISUAL_LINE ]]; then CUTBUFFER=${CUTBUFFER}$'\n' fi BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$bpos zvm_clipboard_copy_buffer # Return when it's repeating mode $ZVM_REPEAT_MODE && return # Reset the repeat commands if [[ $ZVM_MODE != $ZVM_MODE_NORMAL ]]; then local npos=0 ncount=0 ccount=0 # Count the amount of newline character and the amount of # characters after the last newline character. while :; do # Forward find the last newline character's position npos=$(zvm_substr_pos $CUTBUFFER $'\n' $npos) if [[ $npos == -1 ]]; then if (($ncount == 0)); then ccount=$#CUTBUFFER fi break fi npos=$((npos+1)) ncount=$(($ncount + 1)) ccount=$(($#CUTBUFFER - $npos)) done zvm_reset_repeat_commands $ZVM_MODE c $ncount $ccount fi zvm_exit_visual_mode false zvm_select_vi_mode $ZVM_MODE_INSERT ${1:-true} } # Change characters from cursor to the end of current line function zvm_vi_change_eol() { local bpos=$CURSOR epos=$CURSOR # Find the end of current line for ((; $epos<$#BUFFER; epos++)); do if [[ "${BUFFER:$epos:1}" == $'\n' ]]; then break fi done CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" zvm_clipboard_copy_buffer zvm_reset_repeat_commands $ZVM_MODE c 0 $#CUTBUFFER zvm_select_vi_mode $ZVM_MODE_INSERT } # Default handler for unhandled key events function zvm_default_handler() { local keys=$(zvm_keys) local extra_keys=$1 # Exit vi mode if keys is the escape keys case $(zvm_escape_non_printed_characters "$keys") in '^['|$ZVM_VI_INSERT_ESCAPE_BINDKEY) zvm_exit_insert_mode false zvm_reset_prompt ZVM_KEYS=${extra_keys} return ;; [vV]'^['|[vV]$ZVM_VI_VISUAL_ESCAPE_BINDKEY) zvm_exit_visual_mode false zvm_reset_prompt ZVM_KEYS=${extra_keys} return ;; esac case "$KEYMAP" in vicmd) case "$keys" in [vV]c) zvm_vi_change false;; [vV]d) zvm_vi_delete false;; [vV]y) zvm_vi_yank false;; [vV]S) zvm_change_surround S;; [cdyvV]*) # We must loop util we meet a valid range action while :; do zvm_range_handler "${keys}${extra_keys}" case $? in $ZVM_RANGE_HANDLER_RET_OK) # The range action is handled successfully and exit break ;; $ZVM_RANGE_HANDLER_RET_CONTINUE) # Continue to ask to provide the action when we're # still in visual mode keys='v'; extra_keys= ;; $ZVM_RANGE_HANDLER_RET_PUSHBACK) # Push the keys onto the input stack of ZLE, it's # handled in zvm_readkeys_handler function zvm_exit_visual_mode false zvm_reset_prompt return ;; $ZVM_RANGE_HANDLER_RET_CANCEL) # Exit visual mode and cancel the range action zvm_exit_visual_mode false zvm_reset_prompt break ;; esac done ;; *) local i= for ((i=0;i<$#keys;i++)) do zvm_navigation_handler ${keys:$i:1} zvm_highlight done ;; esac ;; viins|main) if [[ "${keys:0:1}" =~ [a-zA-Z0-9\ ] ]]; then zvm_self_insert "${keys:0:1}" zle redisplay ZVM_KEYS="${keys:1}${extra_keys}" return elif [[ "${keys:0:1}" == $'\e' ]]; then zvm_exit_insert_mode false ZVM_KEYS="${keys:1}${extra_keys}" return fi ;; visual) ;; esac ZVM_KEYS= } # Read keys for retrieving and executing a widget function zvm_readkeys_handler() { local keymap=${1} local keys=${2:-$KEYS} local key= local widget= # Get the keymap if keymap is empty if [[ -z $keymap ]]; then case "$ZVM_MODE" in $ZVM_MODE_INSERT) keymap=viins;; $ZVM_MODE_NORMAL) keymap=vicmd;; $ZVM_MODE_VISUAL|$ZVM_MODE_VISUAL_LINE) keymap=visual;; esac fi # Read keys and retrieve the widget zvm_readkeys $keymap $keys keys=${retval[1]} widget=${retval[2]} key=${retval[3]} # Escape space in keys keys=${keys//$ZVM_ESCAPE_SPACE/ } key=${key//$ZVM_ESCAPE_SPACE/ } ZVM_KEYS="${keys}" # If the widget is current handler, we should call the default handler if [[ "${widget}" == "${funcstack[1]}" ]]; then widget= fi # If the widget isn't matched, we should call the default handler if [[ -z ${widget} ]]; then # Disable reset prompt action, as multiple calling this function # will cause potential line eaten issue. ZVM_RESET_PROMPT_DISABLED=true zle zvm_default_handler "$key" ZVM_RESET_PROMPT_DISABLED=false # Push back to the key input stack, and postpone reset prompt if [[ -n "$ZVM_KEYS" ]]; then # To prevent ZLE from error "not enough arguments for -U", the # parameter should be put after `--` symbols. zle -U -- "${ZVM_KEYS}" else # If there is any reset prompt, we need to execute for # prompt resetting. zvm_postpone_reset_prompt false fi else zle $widget fi ZVM_KEYS= } # Find and move cursor to next character function zvm_find_and_move_cursor() { local char=$1 local count=${2:-1} local forward=${3:-true} local skip=${4:-false} local cursor=$CURSOR [[ -z $char ]] && return 1 # Find the specific character while :; do if $forward; then cursor=$((cursor+1)) ((cursor > $#BUFFER)) && break else cursor=$((cursor-1)) ((cursor < 0)) && break fi if [[ ${BUFFER[$cursor+1]} == $char ]]; then count=$((count-1)) fi ((count == 0)) && break done [[ $count > 0 ]] && return 1 # Skip the character if $skip; then if $forward; then cursor=$((cursor-1)) else cursor=$((cursor+1)) fi fi CURSOR=$cursor } # Handle the navigation action function zvm_navigation_handler() { # Return if no keys provided [[ -z $1 ]] && return 1 local keys=$1 local count= local cmd= # Retrieve the calling command if [[ $keys =~ '^([1-9][0-9]*)?([fFtT].?)$' ]]; then count=${match[1]:-1} # The length of keys must be 2 if (( ${#match[2]} < 2)); then zvm_enter_oppend_mode read -k 1 cmd keys+=$cmd case "$(zvm_escape_non_printed_characters ${keys[-1]})" in $ZVM_VI_OPPEND_ESCAPE_BINDKEY) return 1;; esac zvm_exit_oppend_mode fi local forward=true local skip=false [[ ${keys[-2]} =~ '[FT]' ]] && forward=false [[ ${keys[-2]} =~ '[tT]' ]] && skip=true # Escape special characters (e.g. ', ", `, ~, ^, |, &, ) local key=${keys[-1]} if [[ $key =~ "['\\\"\`\~\^\|\#\&\*\;\}\(\)\<\>\ ]" ]]; then key=\\${key} fi cmd=(zvm_find_and_move_cursor $key $count $forward $skip) count=1 # Handle G command elif [[ $keys =~ '^([1-9][0-9]*)?G$' ]]; then count=${match[1]:-1} cmd=(CURSOR=$#BUFFER) # Handle gg command elif [[ $keys =~ '^([1-9][0-9]*)?gg$' ]]; then count=${match[1]:-1} cmd=(CURSOR=0) else count=${keys:0:-1} case ${keys: -1} in '^') cmd=(zle vi-first-non-blank);; '$') cmd=(zle vi-end-of-line);; ' ') cmd=(zle vi-forward-char);; '0') cmd=(zle vi-digit-or-beginning-of-line);; 'h') cmd=(zle vi-backward-char);; 'j') cmd=(zle down-line-or-history);; 'k') cmd=(zle up-line-or-history);; 'l') cmd=(zle vi-forward-char);; 'w') cmd=(zle vi-forward-word);; 'W') cmd=(zle vi-forward-blank-word);; 'e') cmd=(zle vi-forward-word-end);; 'E') cmd=(zle vi-forward-blank-word-end);; 'b') cmd=(zle vi-backward-word);; 'B') cmd=(zle vi-backward-blank-word);; esac fi # Check widget if the widget is empty if [[ -z $cmd ]]; then return 0 fi # Check if keys includes the count if [[ ! $count =~ ^[0-9]+$ ]]; then count=1 fi zvm_repeat_command "$cmd" $count local exit_code=$? if [[ $exit_code == 0 ]]; then retval=$keys fi return $exit_code } # Handle a range of characters function zvm_range_handler() { local keys=$1 local cursor=$CURSOR local key= local mode= local cmds=($ZVM_MODE) local count=1 local exit_code=$ZVM_RANGE_HANDLER_RET_OK # Enter operator pending mode zvm_enter_oppend_mode false # If the keys is less than 2 keys, we should read more # keys (e.g. d, c, y, etc.) while (( ${#keys} < 2 )); do zvm_update_cursor read -k 1 key keys="${keys}${key}" done # If the keys ends in numbers, we should read more # keys (e.g. d2, c3, y10, etc.) while [[ ${keys: 1} =~ ^[1-9][0-9]*$ ]]; do zvm_update_cursor read -k 1 key keys="${keys}${key}" done # If the 2nd character is `i` or `a`, we should read # one more key if [[ ${keys} =~ '^.[ia]$' ]]; then zvm_update_cursor read -k 1 key keys="${keys}${key}" elif [[ ${keys} =~ '^.g$' ]]; then # If the 2nd character is `g`, we should also read # one more key for `gg` zvm_update_cursor read -k 1 key keys="${keys}${key}" fi # Exit operator pending mode zvm_exit_oppend_mode # Handle escape in operator pending mode # escape non-printed characters (e.g. ^[) if [[ $(zvm_escape_non_printed_characters "$keys") =~ ${ZVM_VI_OPPEND_ESCAPE_BINDKEY/\^\[/\\^\\[} ]]; then return $ZVM_RANGE_HANDLER_RET_CANCEL fi # Enter visual mode or visual line mode if [[ $ZVM_MODE != $ZVM_MODE_VISUAL && $ZVM_MODE != $ZVM_MODE_VISUAL_LINE ]]; then case "${keys}" in [cdy][jk]) mode=$ZVM_MODE_VISUAL_LINE;; cc|dd|yy) mode=$ZVM_MODE_VISUAL_LINE;; *) mode=$ZVM_MODE_VISUAL;; esac # Select the mode if [[ ! -z $mode ]]; then zvm_select_vi_mode $mode false fi fi ####################################### # Selection Cases: # # 1. SAMPLE: `word1 word2 w`, CURSOR: at `w` of `word1` # # c[we] -> `word1` # c2[we] -> `word1 word2` # ve -> `word1` # v2e -> `word1 word2` # vw -> `word1 w` # v2w -> `word1 word2 w` # [dy]e -> `word1` # [dy]2e -> `word1 word2` # [dy]w -> `word1 ` # [dy]2w -> `word1 word2 ` # [cdyv]iw -> `word1` # [cdyv]aw -> `word1 ` # [cdyv]2iw -> `word1 ` # [cdyv]2aw -> `word1 word2 ` # # 2. SAMPLE: `a bb c dd`, CURSOR: at `a` # # cw -> `a` # c2w -> `a bb` # ce -> `a bb` # c2e -> `a bb c` # # 3. SAMPLE: ` .foo. bar. baz.`, CURSOR: at `f` # # c[WE] -> `foo.` # c2[WE] -> `foo. bar.` # vE -> `foo.` # v2E -> `foo. bar.` # vW -> `foo. b` # v2W -> `foo. bar. b` # d2W -> `foo. bar. b` # [dy]E -> `foo.` # [dy]2E -> `foo. bar.` # [dy]W -> `foo. ` # [dy]2W -> `foo. bar. ` # [cdyv]iW -> `.foo.` # [cdyv]aW -> `.foo. ` # [cdyv]2iW -> `.foo. ` # [cdyv]2aW -> `.foo. bar. ` # # 4. SAMPLE: ` .foo.bar.baz.`, CURSOR: at `r` # # [cdy]b -> `ba` # [cdy]B -> `.foo.ba` # vb -> `bar` # vB -> `.foo.bar` # vFf -> `foo.bar` # vTf -> `oo.bar` # [cdyv]fz -> `r.baz` # [cdy]Ff -> `foo.ba` # [cdyv]tz -> `r.ba` # [cdy]Tf -> `oo.ba` # # Pre navigation handling local navkey= if [[ $keys =~ '^c([1-9][0-9]*)?[ia][wW]$' ]]; then count=${match[1]:-1} navkey=${keys: -2} elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?[ia][eE]$' ]]; then navkey= elif [[ $keys =~ '^c([1-9][0-9]*)?[eEwW]$' ]]; then count=${match[1]:-1} navkey=c${keys: -1} elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?[bB]$' ]]; then MARK=$((MARK-1)) count=${match[1]:-1} navkey=${keys: -1} elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?([FT].?)$' ]]; then MARK=$((MARK-1)) count=${match[1]:-1} navkey=${match[2]} elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?j$' ]]; then # Exit if there is no line below count=${match[1]:-1} local i= for ((i=$((CURSOR+1)); i<=$#BUFFER; i++)); do [[ ${BUFFER[$i]} == $'\n' ]] && navkey='j' done elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?k$' ]]; then # Exit if there is no line above count=${match[1]:-1} local i= for ((i=$((CURSOR+1)); i>0; i--)); do [[ ${BUFFER[$i]} == $'\n' ]] && navkey='k' done elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?[\^h0]$' ]]; then MARK=$((MARK-1)) count=${match[1]:-1} navkey=${keys: -1} # Exit if the cursor is at the beginning of a line if ((MARK < 0)); then navkey= elif [[ ${BUFFER[$MARK+1]} == $'\n' ]]; then navkey= fi elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?l$' ]]; then count=${match[1]:-1} count=$((count-1)) navkey=${count}l elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?G$' ]]; then count=${match[1]:-1} navkey=G elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?gg$' ]]; then MARK=$((MARK-1)) count=${match[1]:-1} navkey=gg elif [[ $keys =~ '^.([1-9][0-9]*)?([^0-9]+)$' ]]; then count=${match[1]:-1} navkey=${match[2]} else navkey= fi # Handle navigation case $navkey in '') exit_code=$ZVM_RANGE_HANDLER_RET_CONTINUE;; *[ia]?) # At least 1 time if [[ -z $count ]]; then count=1 fi # Retrieve the widget cmd= case ${navkey: -2} in iw) cmd=(zle select-in-word);; aw) cmd=(zle select-a-word);; iW) cmd=(zle select-in-blank-word);; aW) cmd=(zle select-a-blank-word);; esac if [[ -n "$cmd" ]]; then zvm_repeat_command "$cmd" $count elif [[ -n "$(zvm_match_surround "${keys[-1]}")" ]]; then ZVM_KEYS="${keys}" exit_code=$ZVM_RANGE_HANDLER_RET_PUSHBACK elif [[ "${keys[1]}" == 'v' ]]; then exit_code=$ZVM_RANGE_HANDLER_RET_CONTINUE else exit_code=$ZVM_RANGE_HANDLER_RET_CANCEL fi ;; c[eEwW]) ####################################### # Selection Cases: # # 1. SAMPLE: `word1 word2 w`, CURSOR: at `1` of `word1` # # c[weWE] -> `1` # c2[weWE] -> `1 word2` # # 2. SAMPLE: `word1 word2 w`, CURSOR: at ` ` after `word1` # # cw -> ` ` # c2w -> ` word2 ` # ce -> ` word2` # c2e -> ` word2 w` # if [[ "${BUFFER[$((CURSOR + 1))]}" == ' ' ]]; then case ${navkey: -1} in w) cmd=(zle vi-forward-word);; W) cmd=(zle vi-forward-blank-word);; e) cmd=(zle vi-forward-word-end);; E) cmd=(zle vi-forward-blank-word-end);; esac zvm_repeat_command "$cmd" $count case ${navkey: -1} in w|W) CURSOR=$((CURSOR-1));; esac else if [[ "${BUFFER[$((CURSOR + 2))]}" == ' ' ]]; then count=$((count - 1)) fi case ${navkey: -1} in e|w) cmd=(zle vi-forward-word-end);; E|W) cmd=(zle vi-forward-blank-word-end);; esac zvm_repeat_command "$cmd" $count fi ;; *) local retval= # Prevent some actions(e.g. w, e) from affecting the auto # suggestion suffix BUFFER+=$'\0' if zvm_navigation_handler "${count}${navkey}"; then keys="${keys[1]}${retval}" else exit_code=$ZVM_RANGE_HANDLER_RET_CONTINUE fi BUFFER[-1]='' ;; esac # Check if there is no range selected # For the exit code: # 1) Loop in visual mode # 2) Loop by ZVM_KEYS # 3) Exit loop if [[ $exit_code != 0 ]]; then return $exit_code fi # Post navigation handling if [[ $keys =~ '^[cdy]([1-9][0-9]*)?[ia][wW]$' ]]; then cursor=$MARK elif [[ $keys =~ '[dy]([1-9][0-9]*)?[wW]' ]]; then CURSOR=$((CURSOR-1)) # If the CURSOR is at the newline character, we should # move backward a character if [[ "${BUFFER:$CURSOR:1}" == $'\n' ]]; then CURSOR=$((CURSOR-1)) fi else cursor=$CURSOR fi # Handle operation case "${keys}" in c*) zvm_vi_change false; cursor=;; d*) zvm_vi_delete false; cursor=;; y*) zvm_vi_yank false; cursor=;; [vV]*) cursor=;; esac # Reset the repeat commands when it's changing or deleting if $ZVM_REPEAT_MODE; then zvm_exit_visual_mode false elif [[ $keys =~ '^[cd].*' ]]; then cmds+=($keys) zvm_reset_repeat_commands $cmds fi # Change the cursor position if the cursor is not null if [[ ! -z $cursor ]]; then CURSOR=$cursor fi } # Repeat executing command function zvm_repeat_command { local cmd=$1 local count=${2:-1} # check if it's a zle command local is_zle_cmd=false if [[ ${cmd} =~ '^zle .*' ]]; then is_zle_cmd=true fi # Execute the command for `count` times. We can not use # variable `i`, since some widgets will affect the variable # `i`, and it will cause an infinite loop. local init_cursor=$CURSOR local last_cursor=$CURSOR local exit_code=0 c= for ((c=0; c operator, we should # use >! operator to ignore the noclobber. echo "$BUFFER" >! "$tmp_file" # Edit the file with the specific editor, in case of # the warning about input not from a terminal (e.g. # vim), we should tell the editor input is from the # terminal and not from standard input. "${(@Q)${(z)${ZVM_VI_EDITOR}}}" $tmp_file =0; bpos--)); do if [[ "${buffer:$bpos:1}" == $'\n' ]]; then _bpos=$((bpos+1)) break fi done # Find the end of current line for ((epos=$cursor; $epos<$#buffer; epos++)); do if [[ "${buffer:$epos:1}" == $'\n' ]]; then _epos=$epos break fi done # Search for the URL or path for ((bpos=$_bpos; $bpos<=$cursor; bpos++)); do for ((epos=$((_epos-1)); $epos>=$cursor; epos--)); do content=${buffer:$bpos:$((epos-bpos+1))} if zvm_is_url "$content" || zvm_is_path "$content"; then echo $bpos $epos return fi done done echo $cursor $cursor } # Open URL or folder under cursor (gx command) function zvm_open_under_cursor() { # Get the word under the cursor local ret=($(zvm_select_url_or_path $CURSOR $BUFFER)) local bpos=${ret[1]} epos=${ret[2]} local content=${BUFFER:$bpos:$((epos-bpos+1))} # Check if it's a valid URL if zvm_is_url "$content"; then # Open URL with default browser if [[ -n $ZVM_OPEN_URL_CMD ]]; then local -a cmd cmd=("${(z)ZVM_OPEN_URL_CMD}") "$cmd[@]" "$content" elif zvm_exist_command "open"; then open "$content" elif zvm_exist_command "xdg-open"; then xdg-open "$content" fi # Check if it's a valid path elif zvm_is_path "$content"; then if [[ -n $ZVM_OPEN_FILE_CMD ]]; then local -a cmd cmd=("${(z)ZVM_OPEN_FILE_CMD}") "$cmd[@]" "$content" elif zvm_exist_command "open"; then open "$content" elif zvm_exist_command "xdg-open"; then xdg-open "$content" fi fi } # Get the substr position in a string function zvm_substr_pos() { local pos=-1 local len=${#1} local slen=${#2} local i=${3:-0} local forward=${4:-true} local init=${i:-$($forward && echo "$i" || echo "i=$len-1")} local condition=$($forward && echo "i<$len" || echo "i>=0") local step=$($forward && echo 'i++' || echo 'i--') for (($init;$condition;$step)); do if [[ ${1:$i:$slen} == "$2" ]]; then pos=$i break fi done echo $pos } # Parse surround from keys function zvm_parse_surround_keys() { local keys=${1:-${$(zvm_keys)//$ZVM_ESCAPE_SPACE/ }} local action= local surround= case "${keys}" in vS*) action=S; surround=${keys:2};; vsa*) action=a; surround=${keys:3};; vys*) action=y; surround=${keys:3};; s[dr]*) action=${keys:1:1}; surround=${keys:2};; [acd]s*) action=${keys:0:1}; surround=${keys:2};; [cdvy][ia]*) action=${keys:0:2}; surround=${keys:2};; esac echo $action ${surround// /$ZVM_ESCAPE_SPACE} } # Move around code structure (e.g. (..), {..}) function zvm_move_around_surround() { local slen= local bpos=-1 local epos=-1 local i= s= for ((i=$CURSOR;i>=0;i--)); do # Check if it's one of the surrounds for s in {\',\",\`,\(,\[,\{,\<}; do slen=${#s} if [[ ${BUFFER:$i:$slen} == "$s" ]]; then bpos=$i break fi done if (($bpos == -1)); then continue fi # Search the nearest surround local ret=($(zvm_search_surround "$s")) if [[ -z ${ret[@]} ]]; then continue fi bpos=${ret[1]} epos=${ret[2]} # Move between the opening and close surrounds if (( $CURSOR > $((bpos-1)) )) && (( $CURSOR < $((bpos+slen)) )); then CURSOR=$epos else CURSOR=$bpos fi break done } # Match the surround pair from the part function zvm_match_surround() { local bchar=${1// /$ZVM_ESCAPE_SPACE} local echar=$bchar case $bchar in '(') echar=')';; '[') echar=']';; '{') echar='}';; '<') echar='>';; ')') bchar='(';echar=')';; ']') bchar='[';echar=']';; '}') bchar='{';echar='}';; '>') bchar='<';echar='>';; "'") ;; '"') ;; '`') ;; *) return;; esac echo $bchar $echar } # Search surround from the string function zvm_search_surround() { local ret=($(zvm_match_surround "$1")) local bchar=${${ret[1]//$ZVM_ESCAPE_SPACE/ }:- } local echar=${${ret[2]//$ZVM_ESCAPE_SPACE/ }:- } local bpos=$(zvm_substr_pos $BUFFER $bchar $CURSOR false) local epos=$(zvm_substr_pos $BUFFER $echar $CURSOR true) if [[ $bpos == $epos ]]; then epos=$(zvm_substr_pos $BUFFER $echar $((CURSOR+1)) true) if [[ $epos == -1 ]]; then epos=$(zvm_substr_pos $BUFFER $echar $((CURSOR-1)) false) if [[ $epos != -1 ]]; then local tmp=$epos; epos=$bpos; bpos=$tmp fi fi fi if [[ $bpos == -1 ]] || [[ $epos == -1 ]]; then return fi echo $bpos $epos $bchar $echar } # Select surround and highlight it in visual mode function zvm_select_surround() { local ret=($(zvm_parse_surround_keys)) local action=${1:-${ret[1]}} local surround=${2:-${ret[2]//$ZVM_ESCAPE_SPACE/ }} ret=($(zvm_search_surround ${surround})) if [[ ${#ret[@]} == 0 ]]; then zvm_exit_visual_mode return fi local bpos=${ret[1]} local epos=${ret[2]} if [[ ${action:1:1} == 'i' ]]; then ((bpos++)) else ((epos++)) fi MARK=$bpos; CURSOR=$epos-1 # refresh for highlight redraw zle redisplay } # Change surround in vicmd or visual mode function zvm_change_surround() { local ret=($(zvm_parse_surround_keys)) local action=${1:-${ret[1]}} local surround=${2:-${ret[2]//$ZVM_ESCAPE_SPACE/ }} local bpos=${3} epos=${4} local is_appending=false case $action in S|y|a) is_appending=true;; esac if $is_appending; then if [[ -z $bpos && -z $epos ]]; then ret=($(zvm_selection)) bpos=${ret[1]} epos=${ret[2]} fi else ret=($(zvm_search_surround "$surround")) (( ${#ret[@]} )) || return bpos=${ret[1]} epos=${ret[2]} zvm_highlight custom $bpos $(($bpos+1)) zvm_highlight custom $epos $(($epos+1)) fi local key= case $action in c|r) zvm_enter_oppend_mode read -k 1 key zvm_exit_oppend_mode ;; S|y|a) if [[ -z $surround ]]; then zvm_enter_oppend_mode read -k 1 key zvm_exit_oppend_mode else key=$surround fi if [[ $ZVM_MODE == $ZVM_MODE_VISUAL ]]; then zle visual-mode fi ;; esac # Check if it is ESCAPE key ( or ZVM_VI_ESCAPE_BINDKEY) case "$key" in $'\e'|"${ZVM_VI_ESCAPE_BINDKEY//\^\[/$'\e'}") zvm_highlight clear return esac # Start changing surround ret=($(zvm_match_surround "$key")) local bchar=${${ret[1]//$ZVM_ESCAPE_SPACE/ }:-$key} local echar=${${ret[2]//$ZVM_ESCAPE_SPACE/ }:-$key} local value=$($is_appending && echo 0 || echo 1 ) local head=${BUFFER:0:$bpos} local body=${BUFFER:$((bpos+value)):$((epos-(bpos+value)))} local foot=${BUFFER:$((epos+value))} BUFFER="${head}${bchar}${body}${echar}${foot}" # Clear highliht zvm_highlight clear case $action in S|y|a) zvm_select_vi_mode $ZVM_MODE_NORMAL;; esac } # Change surround text object function zvm_change_surround_text_object() { local ret=($(zvm_parse_surround_keys)) local action=${1:-${ret[1]}} local surround=${2:-${ret[2]//$ZVM_ESCAPE_SPACE/ }} ret=($(zvm_search_surround "${surround}")) if [[ ${#ret[@]} == 0 ]]; then zvm_select_vi_mode $ZVM_MODE_NORMAL return fi local bpos=${ret[1]} local epos=${ret[2]} if [[ ${action:1:1} == 'i' ]]; then ((bpos++)) else ((epos++)) fi CUTBUFFER=${BUFFER:$bpos:$(($epos-$bpos))} case ${action:0:1} in c) BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$bpos zvm_select_vi_mode $ZVM_MODE_INSERT ;; d) BUFFER="${BUFFER:0:$bpos}${BUFFER:$epos}" CURSOR=$bpos ;; esac } # Repeat last change function zvm_repeat_change() { local times=${NUMERIC:-1} ZVM_REPEAT_MODE=true ZVM_RESET_PROMPT_DISABLED=true local cmd=${ZVM_REPEAT_COMMANDS[2]} # Handle repeat command for the specified number of times local i= for ((i=0; i<$times; i++)); do case $cmd in [aioAIO]) zvm_repeat_insert;; c) zvm_repeat_vi_change;; [cd]*) zvm_repeat_range_change;; R) zvm_repeat_replace;; r) zvm_repeat_replace_chars;; *) zle vi-repeat-change;; esac done zle redisplay ZVM_RESET_PROMPT_DISABLED=false ZVM_REPEAT_MODE=false } # Repeat inserting characters function zvm_repeat_insert() { local cmd=${ZVM_REPEAT_COMMANDS[2]} local cmds=(${ZVM_REPEAT_COMMANDS[3,-1]}) # Pre-handle the command case $cmd in a) CURSOR+=1;; o) zle vi-backward-char zle vi-end-of-line LBUFFER+=$'\n' ;; A) zle vi-end-of-line CURSOR=$((CURSOR+1)) ;; I) zle vi-first-non-blank;; O) zle vi-digit-or-beginning-of-line LBUFFER+=$'\n' CURSOR=$((CURSOR-1)) ;; esac # Insert characters local i= for ((i=1; i<=${#cmds[@]}; i++)); do cmd="${cmds[$i]}" # Handle the backspace command if [[ $cmd == '' ]]; then if (($#LBUFFER > 0)); then LBUFFER=${LBUFFER:0:-1} fi continue fi # The length of character should be 1 if (($#cmd == 1)); then zvm_self_insert "$cmd" fi done } # Repeat changing visual characters function zvm_repeat_vi_change() { local mode=${ZVM_REPEAT_COMMANDS[1]} local cmds=(${ZVM_REPEAT_COMMANDS[3,-1]}) # Backward move cursor to the beginning of line if [[ $mode == $ZVM_MODE_VISUAL_LINE ]]; then zle vi-digit-or-beginning-of-line fi local ncount=${cmds[1]} local ccount=${cmds[2]} local pos=$CURSOR epos=$CURSOR local i= # Forward expand the characters to the Nth newline character for ((i=0; i<$ncount; i++)); do pos=$(zvm_substr_pos $BUFFER $'\n' $pos) if [[ $pos == -1 ]]; then epos=$#BUFFER break fi pos=$((pos+1)) epos=$pos done # Forward expand the remaining characters for ((i=0; i<$ccount; i++)); do local char=${BUFFER[$epos+i]} if [[ $char == $'\n' || $char == '' ]]; then ccount=$i break fi done epos=$((epos+ccount)) RBUFFER=${RBUFFER:$((epos-CURSOR))} } # Repeat changing a range of characters function zvm_repeat_range_change() { local cmd=${ZVM_REPEAT_COMMANDS[2]} # Remove characters zvm_range_handler $cmd # Insert characters zvm_repeat_insert } # Repeat replacing function zvm_repeat_replace() { local cmds=(${ZVM_REPEAT_COMMANDS[3,-1]}) local cmd= local cursor=$CURSOR local i= for ((i=1; i<=${#cmds[@]}; i++)); do cmd="${cmds[$i]}" # If the cmd or the character at cursor is a newline character, # or the cursor is at the end of buffer, we should insert the # cmd instead of replacing with the cmd. if [[ $cmd == $'\n' || $BUFFER[$cursor+1] == $'\n' || $BUFFER[$cursor+1] == '' ]]; then LBUFFER+=$cmd else BUFFER[$cursor+1]=$cmd fi cursor=$((cursor+1)) CURSOR=$cursor done # The cursor position should go back one character after # exiting the replace mode zle vi-backward-char } # Repeat replacing characters function zvm_repeat_replace_chars() { local mode=${ZVM_REPEAT_COMMANDS[1]} local cmds=(${ZVM_REPEAT_COMMANDS[3,-1]}) local cmd= # Replacement of visual mode should move backward cursor to the # begin of current line, and replacing to the end of last line. if [[ $mode == $ZVM_MODE_VISUAL_LINE ]]; then zle vi-digit-or-beginning-of-line cmds+=($'\n') fi local cursor=$((CURSOR+1)) local i= for ((i=1; i<=${#cmds[@]}; i++)); do cmd="${cmds[$i]}" # If we meet a newline character in the buffer, we should keep # stop replacing, util we meet next newline character command. if [[ ${BUFFER[$cursor]} == $'\n' ]]; then if [[ $cmd == $'\n' ]]; then cursor=$((cursor+1)) fi continue fi # A newline character command should keep replacing with last # character, until we meet a newline character in the buffer, # then we use next command. if [[ $cmd == $'\n' ]]; then i=$((i-1)) cmd="${cmds[$i]}" fi # The length of character should be 1 if (($#cmd == 1)); then BUFFER[$cursor]="${cmd}" fi cursor=$((cursor+1)) # Break when it reaches the end if ((cursor > $#BUFFER)); then break fi done } # Select a word under the cursor function zvm_select_in_word() { local cursor=${1:-$CURSOR} local buffer=${2:-$BUFFER} local bpos=$cursor epos=$cursor local pattern='[0-9a-zA-Z_]' if ! [[ "${buffer:$cursor:1}" =~ $pattern ]]; then pattern="[^${pattern:1:-1} ]" fi for ((; $bpos>=0; bpos--)); do [[ "${buffer:$bpos:1}" =~ $pattern ]] || break done for ((; $epos<$#buffer; epos++)); do [[ "${buffer:$epos:1}" =~ $pattern ]] || break done bpos=$((bpos+1)) # The ending position must be greater than 0 if (( epos > 0 )); then epos=$((epos-1)) fi echo $bpos $epos } # Switch keyword function zvm_switch_keyword() { local bpos= epos= cpos=$CURSOR # Cursor position cases: # # 1. Cursor on symbol: # 2+2 => + # 2-2 => - # 2 + 2 => + # 2 +2 => +2 # 2 -2 => -2 # 2 -a => -a # # 2. Cursor on number or alpha: # 2+2 => +2 # 2-2 => -2 # 2 + 2 => 2 # 2 +2 => +2 # 2 -2 => -2 # 2 -a => -a # If cursor is on the `+` or `-`, we need to check if it is a # number with a sign or an operator, only the number needs to # forward the cursor. if [[ ${BUFFER:$cpos:2} =~ [+-][0-9] ]]; then if [[ $cpos == 0 || ${BUFFER:$((cpos-1)):1} =~ [^0-9] ]]; then cpos=$((cpos+1)) fi # If cursor is on the `+` or `-`, we need to check if it is a # short option, only the short option needs to forward the cursor. elif [[ ${BUFFER:$cpos:2} =~ [+-][a-zA-Z] ]]; then if [[ $cpos == 0 || ${BUFFER:$((cpos-1)):1} == ' ' ]]; then cpos=$((cpos+1)) fi fi local result=($(zvm_select_in_word $cpos)) bpos=${result[1]} epos=$((${result[2]}+1)) # Move backward the cursor if [[ $bpos != 0 && ${BUFFER:$((bpos-1)):1} == [+-] ]]; then bpos=$((bpos-1)) fi local word=${BUFFER:$bpos:$((epos-bpos))} local keys=$(zvm_keys) if [[ $keys == '' ]]; then local increase=true else local increase=false fi # Execute extra commands for handler in $zvm_switch_keyword_handlers; do if ! zvm_exist_command ${handler}; then continue fi result=($($handler $word $increase)); if (( $#result == 0 )); then continue fi epos=$(( bpos + ${result[3]} )) bpos=$(( bpos + ${result[2]} )) if (( cpos < bpos )) || (( cpos >= epos )); then continue fi # Save to history and only keep some recent records zvm_switch_keyword_history+=("${handler}:${word}") zvm_switch_keyword_history=("${zvm_switch_keyword_history[@]: -10}") BUFFER="${BUFFER:0:$bpos}${result[1]}${BUFFER:$epos}" CURSOR=$((bpos + ${#result[1]} - 1)) zle reset-prompt return done } # Switch number keyword function zvm_switch_number { local word=$1 local increase=${2:-true} local result= bpos= epos= # Hexadecimal if [[ $word =~ [^0-9]?(0[xX][0-9a-fA-F]*) ]]; then local number=${match[1]} local prefix=${number:0:2} bpos=$((mbegin-1)) epos=$mend # Hexadecimal cases: # # 1. Increment: # 0xDe => 0xdf # 0xdE => 0xDF # 0xde0 => 0xddf # 0xffffffffffffffff => 0x0000000000000000 # 0X9 => 0XA # 0Xdf => 0Xe0 # # 2. Decrement: # 0xdE0 => 0xDDF # 0xffFf0 => 0xfffef # 0xfffF0 => 0xFFFEF # 0x0 => 0xffffffffffffffff # 0X0 => 0XFFFFFFFFFFFFFFFF # 0Xf => 0Xe local lower=true if [[ $number =~ [A-Z][0-9]*$ ]]; then lower=false fi # Fix the number truncated after 15 digits issue if (( $#number > 17 )); then local d=$(($#number - 15)) local h=${number:0:$d} number="0x${number:$d}" fi local p=$(($#number - 2)) if $increase; then if (( $number == 0x${(l:15::f:)} )); then h=$(([##16]$h+1)) h=${h: -1} number=${(l:15::0:)} else h=${h:2} number=$(([##16]$number + 1)) fi else if (( $number == 0 )); then if (( ${h:-0} == 0 )); then h=f else h=$(([##16]$h-1)) h=${h: -1} fi number=${(l:15::f:)} else h=${h:2} number=$(([##16]$number - 1)) fi fi # Padding with zero if (( $#number < $p )); then number=${(l:$p::0:)number} fi result="${h}${number}" # Transform the case if $lower; then result="${(L)result}" fi result="${prefix}${result}" # Binary elif [[ $word =~ [^0-9]?(0[bB][01]*) ]]; then # Binary cases: # # 1. Increment: # 0b1 => 0b10 # 0x1111111111111111111111111111111111111111111111111111111111111111 => # 0x0000000000000000000000000000000000000000000000000000000000000000 # 0B0 => 0B1 # # 2. Decrement: # 0b1 => 0b0 # 0b100 => 0b011 # 0B010 => 0B001 # 0b0 => # 0x1111111111111111111111111111111111111111111111111111111111111111 local number=${match[1]} local prefix=${number:0:2} bpos=$((mbegin-1)) epos=$mend # Fix the number truncated after 63 digits issue if (( $#number > 65 )); then local d=$(($#number - 63)) local h=${number:0:$d} number="0b${number:$d}" fi local p=$(($#number - 2)) if $increase; then if (( $number == 0b${(l:63::1:)} )); then h=$(([##2]$h+1)) h=${h: -1} number=${(l:63::0:)} else h=${h:2} number=$(([##2]$number + 1)) fi else if (( $number == 0b0 )); then if (( ${h:-0} == 0 )); then h=1 else h=$(([##2]$h-1)) h=${h: -1} fi number=${(l:63::1:)} else h=${h:2} number=$(([##2]$number - 1)) fi fi # Padding with zero if (( $#number < $p )); then number=${(l:$p::0:)number} fi result="${prefix}${number}" # Decimal elif [[ $word =~ ([-+]?[0-9]+) ]]; then # Decimal cases: # # 1. Increment: # 0 => 1 # 99 => 100 # # 2. Decrement: # 0 => -1 # 10 => 9 # aa1230xa => aa1231xa # aa1230bb => aa1231bb # aa123a0bb => aa124a0bb local number=${match[1]} bpos=$((mbegin-1)) epos=$mend if $increase; then result=$(($number + 1)) else result=$(($number - 1)) fi # Check if need the plus sign prefix if [[ ${word:$bpos:1} == '+' ]]; then result="+${result}" fi fi if [[ $result ]]; then echo $result $bpos $epos fi } # Switch boolean keyword function zvm_switch_boolean() { local word=$1 local increase=$2 local result= local bpos=0 epos=$#word # Remove option prefix if [[ $word =~ (^[+-]{0,2}) ]]; then local prefix=${match[1]} bpos=$mend word=${word:$bpos} fi case ${(L)word} in true) result=false;; false) result=true;; yes) result=no;; no) result=yes;; on) result=off;; off) result=on;; y) result=n;; n) result=y;; t) result=f;; f) result=t;; *) return;; esac # Transform the case if [[ $word =~ ^[A-Z]+$ ]]; then result=${(U)result} elif [[ $word =~ ^[A-Z] ]]; then result=${(U)result:0:1}${result:1} fi echo $result $bpos $epos } # Switch weekday keyword function zvm_switch_weekday() { local word=$1 local increase=$2 local result=${(L)word} local weekdays=( sunday monday tuesday wednesday thursday friday saturday ) local i=1 for ((; i<=${#weekdays[@]}; i++)); do if [[ ${weekdays[i]:0:$#result} == ${result} ]]; then result=${weekdays[i]} break fi done # Return if no match if (( i > ${#weekdays[@]} )); then return fi if $increase; then if (( i == ${#weekdays[@]} )); then i=1 else i=$((i+1)) fi else if (( i == 1 )); then i=${#weekdays[@]} else i=$((i-1)) fi fi # Abbreviation if (( $#result == $#word )); then result=${weekdays[i]} else result=${weekdays[i]:0:$#word} fi # Transform the case if [[ $word =~ ^[A-Z]+$ ]]; then result=${(U)result} elif [[ $word =~ ^[A-Z] ]]; then result=${(U)result:0:1}${result:1} fi echo $result 0 $#word } # Switch operator keyword function zvm_switch_operator() { local word=$1 local increase=$2 local result= case ${(L)word} in '&&') result='||';; '||') result='&&';; '++') result='--';; '--') result='++';; '==') result='!=';; '!=') result='==';; '===') result='!==';; '!==') result='===';; '+') result='-';; '-') result='*';; '*') result='/';; '/') result='+';; 'and') result='or';; 'or') result='and';; *) return;; esac # Transform the case if [[ $word =~ ^[A-Z]+$ ]]; then result=${(U)result} elif [[ $word =~ ^[A-Z] ]]; then result=${(U)result:0:1}${result:1} fi # Since the `echo` command can not print the character # `-`, here we use `printf` command alternatively. printf "%s 0 $#word" "${result}" } # Switch month keyword function zvm_switch_month() { local word=$1 local increase=$2 local result=${(L)word} local months=( january february march april may june july august september october november december ) local i=1 for ((; i<=${#months[@]}; i++)); do if [[ ${months[i]:0:$#result} == ${result} ]]; then result=${months[i]} break fi done # Return if no match if (( i > ${#months[@]} )); then return fi if $increase; then if (( i == ${#months[@]} )); then i=1 else i=$((i+1)) fi else if (( i == 1 )); then i=${#months[@]} else i=$((i-1)) fi fi ##################### # Abbreviation local lastlen=0 local last="${zvm_switch_keyword_history[-1]}" local funcmark="${funcstack[1]}:" if [[ "$last" =~ "^${funcmark}" ]]; then lastlen=$(($#last - $#funcmark)) fi # Use cases: # # May -> June # Apr -> May -> Jun # April -> May -> June # January -> Feb(Munual changing) -> Mar # Jan -> February(Munual changing) -> March # if [[ "$result" == "may" ]]; then if (($lastlen == 3)); then result=${months[i]:0:3} else result=${months[i]} fi else if (($#word == 3)); then result=${months[i]:0:3} else result=${months[i]} fi fi ##################### # Transform the case if [[ $word =~ ^[A-Z]+$ ]]; then result=${(U)result} elif [[ $word =~ ^[A-Z] ]]; then result=${(U)result:0:1}${result:1} fi echo $result 0 $#word } # Highlight content function zvm_highlight() { local opt=${1:-mode} local region=() local redraw=false # Handle region by the option case "$opt" in mode) case "$ZVM_MODE" in $ZVM_MODE_VISUAL|$ZVM_MODE_VISUAL_LINE) local ret=($(zvm_calc_selection)) local bpos=$((ret[1])) epos=$((ret[2])) local bg=$ZVM_VI_HIGHLIGHT_BACKGROUND local fg=$ZVM_VI_HIGHLIGHT_FOREGROUND local es=$ZVM_VI_HIGHLIGHT_EXTRASTYLE region=("$bpos $epos fg=$fg,bg=$bg,$es") ;; esac redraw=true ;; custom) local bpos=$2 epos=$3 local bg=${4:-$ZVM_VI_HIGHLIGHT_BACKGROUND} local fg=${5:-$ZVM_VI_HIGHLIGHT_FOREGROUND} local es=${6:-$ZVM_VI_HIGHLIGHT_EXTRASTYLE} region=("${ZVM_REGION_HIGHLIGHT[@]}") region+=("$bpos $epos fg=$fg,bg=$bg,$es") redraw=true ;; clear) zle redisplay redraw=true ;; redraw) redraw=true;; esac # Update region highlight if (( $#region > 0 )) || [[ "$opt" == 'clear' ]]; then # Remove old region highlight local rawhighlight=() local i= j= for ((i=1; i<=${#region_highlight[@]}; i++)); do local raw=true local spl=(${(@s/ /)region_highlight[i]}) local pat="${spl[1]} ${spl[2]}" for ((j=1; j<=${#ZVM_REGION_HIGHLIGHT[@]}; j++)); do if [[ "$pat" == "${ZVM_REGION_HIGHLIGHT[j]:0:$#pat}" ]]; then raw=false break fi done if $raw; then rawhighlight+=("${region_highlight[i]}") fi done # Assign new region highlight ZVM_REGION_HIGHLIGHT=("${region[@]}") region_highlight=("${rawhighlight[@]}" "${ZVM_REGION_HIGHLIGHT[@]}") fi # Check if we need to refresh the region highlight if $redraw; then zle -R fi } # Enter the visual mode function zvm_enter_visual_mode() { local mode= local last_mode=$ZVM_MODE local last_region= # Exit the visual mode case $last_mode in $ZVM_MODE_VISUAL|$ZVM_MODE_VISUAL_LINE) last_region=($MARK $CURSOR) zvm_exit_visual_mode ;; esac case "${1:-$(zvm_keys)}" in v) mode=$ZVM_MODE_VISUAL;; V) mode=$ZVM_MODE_VISUAL_LINE;; *) mode=$last_mode;; esac # We should just exit the visual mode if current mode # is the same with last visual mode if [[ $last_mode == $mode ]]; then return fi zvm_select_vi_mode $mode # Recover the region when changing to another visual mode if [[ -n $last_region ]]; then MARK=$last_region[1] CURSOR=$last_region[2] zle redisplay fi } # Exit the visual mode function zvm_exit_visual_mode() { case "$ZVM_MODE" in $ZVM_MODE_VISUAL) zle visual-mode;; $ZVM_MODE_VISUAL_LINE) zle visual-line-mode;; esac zvm_highlight clear zvm_select_vi_mode $ZVM_MODE_NORMAL ${1:-true} } # Enter the vi insert mode function zvm_enter_insert_mode() { local keys=${1:-$(zvm_keys)} if [[ $keys == 'i' ]]; then ZVM_INSERT_MODE='i' elif [[ $keys == 'a' ]]; then ZVM_INSERT_MODE='a' if ! zvm_is_empty_line; then CURSOR=$((CURSOR+1)) fi fi zvm_reset_repeat_commands $ZVM_MODE_NORMAL $ZVM_INSERT_MODE zvm_select_vi_mode $ZVM_MODE_INSERT } # Exit the vi insert mode function zvm_exit_insert_mode() { ZVM_INSERT_MODE= zvm_select_vi_mode $ZVM_MODE_NORMAL ${1:-true} } # Enter the vi operator pending mode function zvm_enter_oppend_mode() { ZVM_OPPEND_MODE=true ${1:-true} && zvm_update_cursor } # Exit the vi operator pending mode function zvm_exit_oppend_mode() { ZVM_OPPEND_MODE=false ${1:-true} && zvm_update_cursor } # Insert at the beginning of the line function zvm_insert_bol() { ZVM_INSERT_MODE='I' zle vi-first-non-blank zvm_exit_visual_mode false zvm_select_vi_mode $ZVM_MODE_INSERT zvm_reset_repeat_commands $ZVM_MODE_NORMAL $ZVM_INSERT_MODE } # Append at the end of the line function zvm_append_eol() { ZVM_INSERT_MODE='A' zle vi-end-of-line zvm_exit_visual_mode false zvm_select_vi_mode $ZVM_MODE_INSERT zvm_reset_repeat_commands $ZVM_MODE_NORMAL $ZVM_INSERT_MODE } # Self insert content to cursor position function zvm_self_insert() { local keys=${1:-$KEYS} # Update the autosuggestion if [[ ${POSTDISPLAY:0:$#keys} == $keys ]]; then POSTDISPLAY=${POSTDISPLAY:$#keys} else POSTDISPLAY= fi LBUFFER+=${keys} } # Reset the repeat commands function zvm_reset_repeat_commands() { ZVM_REPEAT_RESET=true ZVM_REPEAT_COMMANDS=($@) } # Select vi mode function zvm_select_vi_mode() { local mode=$1 local reset_prompt=${2:-true} # Check if current mode is the same with the new mode if [[ $mode == "$ZVM_MODE" ]]; then zvm_update_cursor return fi zvm_exec_commands 'before_select_vi_mode' # Some plugins would reset the prompt when we select the # keymap, so here we postpone executing reset-prompt. zvm_postpone_reset_prompt true # Exit operator pending mode if $ZVM_OPPEND_MODE; then zvm_exit_oppend_mode false fi case $mode in $ZVM_MODE_NORMAL) ZVM_MODE=$ZVM_MODE_NORMAL zvm_update_cursor zle vi-cmd-mode ;; $ZVM_MODE_INSERT) ZVM_MODE=$ZVM_MODE_INSERT zvm_update_cursor zle vi-insert ;; $ZVM_MODE_VISUAL) ZVM_MODE=$ZVM_MODE_VISUAL zvm_update_cursor zle visual-mode ;; $ZVM_MODE_VISUAL_LINE) ZVM_MODE=$ZVM_MODE_VISUAL_LINE zvm_update_cursor zle visual-line-mode ;; $ZVM_MODE_REPLACE) ZVM_MODE=$ZVM_MODE_REPLACE zvm_enter_oppend_mode ;; esac # This aspect provides you a moment to do something, such as # update the cursor, prompt and so on. zvm_exec_commands 'after_select_vi_mode' # Stop and trigger reset-prompt $reset_prompt && zvm_postpone_reset_prompt false true # Start the lazy keybindings when the first time entering the # normal mode, when the mode is the same as last mode, we get # empty value for $mode. if [[ $mode == $ZVM_MODE_NORMAL ]] && (( $#ZVM_LAZY_KEYBINDINGS_LIST > 0 )); then zvm_exec_commands 'before_lazy_keybindings' # Here we should unset the list for normal keybindings local list=("${ZVM_LAZY_KEYBINDINGS_LIST[@]}") unset ZVM_LAZY_KEYBINDINGS_LIST for r in "${list[@]}"; do eval "zvm_bindkey ${r}" done zvm_exec_commands 'after_lazy_keybindings' fi } # Postpone reset prompt function zvm_postpone_reset_prompt() { local toggle=$1 local force=${2:-false} if $force; then ZVM_POSTPONE_RESET_PROMPT=1 fi if $toggle; then ZVM_POSTPONE_RESET_PROMPT=0 else if (($ZVM_POSTPONE_RESET_PROMPT > 0)); then ZVM_POSTPONE_RESET_PROMPT=-1 zle reset-prompt else ZVM_POSTPONE_RESET_PROMPT=-1 fi fi } # Reset prompt function zvm_reset_prompt() { if (($ZVM_POSTPONE_RESET_PROMPT >= 0)); then ZVM_POSTPONE_RESET_PROMPT=$(($ZVM_POSTPONE_RESET_PROMPT + 1)) return fi if [[ $ZVM_RESET_PROMPT_DISABLED == true ]]; then return fi local -i retval=0 if [[ -z "$rawfunc" ]]; then zle .reset-prompt -- "$@" retval=$? else $rawfunc -- "$@" retval=$? fi return $retval } # Undo action in vi insert mode # # CTRL-U Remove all characters between the cursor position and # the beginning of the line. Previous versions of vim # deleted all characters on the line. function zvm_viins_undo() { if $ZVM_VI_INSERT_MODE_LEGACY_UNDO; then zvm_kill_line else zvm_backward_kill_line fi } function zvm_set_cursor() { # Term of vim isn't supported if [[ -n $VIM ]]; then return fi echo -ne "$1" } # Get the escape sequence of cursor style function zvm_cursor_style() { local style=${(L)1} local term=${2:-$ZVM_TERM} case $term in # For xterm and rxvt and their derivatives use the same escape # sequences as the VT520 terminal. And screen, konsole, alacritty, # st and foot implement a superset of VT100 and VT100, they support # 256 colors the same way xterm does. xterm*|rxvt*|screen*|tmux*|konsole*|alacritty*|st*|foot*|wezterm) case $style in $ZVM_CURSOR_BLOCK) style='\e[2 q';; $ZVM_CURSOR_UNDERLINE) style='\e[4 q';; $ZVM_CURSOR_BEAM) style='\e[6 q';; $ZVM_CURSOR_BLINKING_BLOCK) style='\e[1 q';; $ZVM_CURSOR_BLINKING_UNDERLINE) style='\e[3 q';; $ZVM_CURSOR_BLINKING_BEAM) style='\e[5 q';; $ZVM_CURSOR_USER_DEFAULT) style='\e[0 q';; esac ;; *) style='\e[0 q';; esac # Restore default cursor color if [[ $style == '\e[0 q' ]]; then local old_style= case $ZVM_MODE in $ZVM_MODE_INSERT) old_style=$ZVM_INSERT_MODE_CURSOR;; $ZVM_MODE_NORMAL) old_style=$ZVM_NORMAL_MODE_CURSOR;; $ZVM_MODE_VISUAL) old_style=$ZVM_VISUAL_MODE_CURSOR;; $ZVM_MODE_VISUAL_LINE) old_style=$ZVM_VISUAL_LINE_MODE_CURSOR;; esac if [[ $old_style =~ '\e\][0-9]+;.+\a' ]]; then style=$style'\e\e]112\a' fi fi echo $style } # Update the cursor according current vi mode function zvm_update_cursor() { # Check if we need to update the cursor style $ZVM_CURSOR_STYLE_ENABLED || return local mode=$1 local shape= # Check if it is operator pending mode if $ZVM_OPPEND_MODE; then mode=opp shape=$(zvm_cursor_style $ZVM_OPPEND_MODE_CURSOR) fi # Get cursor shape by the mode case "${mode:-$ZVM_MODE}" in $ZVM_MODE_NORMAL) shape=$(zvm_cursor_style $ZVM_NORMAL_MODE_CURSOR) ;; $ZVM_MODE_INSERT) shape=$(zvm_cursor_style $ZVM_INSERT_MODE_CURSOR) ;; $ZVM_MODE_VISUAL) shape=$(zvm_cursor_style $ZVM_VISUAL_MODE_CURSOR) ;; $ZVM_MODE_VISUAL_LINE) shape=$(zvm_cursor_style $ZVM_VISUAL_LINE_MODE_CURSOR) ;; esac if [[ $shape ]]; then zvm_set_cursor $shape fi } # Updates highlight region function zvm_update_highlight() { case "$ZVM_MODE" in $ZVM_MODE_VISUAL|$ZVM_MODE_VISUAL_LINE) zvm_highlight ;; esac } # Updates repeat commands function zvm_update_repeat_commands() { # We don't need to update the repeat commands if current # mode is already the repeat mode. $ZVM_REPEAT_MODE && return # We don't need to update the repeat commands if it is # resetting the repeat commands. if $ZVM_REPEAT_RESET; then ZVM_REPEAT_RESET=false return fi # We update the repeat commands when it's the insert mode [[ $ZVM_MODE == $ZVM_MODE_INSERT ]] || return local char=$KEYS # If current key is an arrow key, we should do something if [[ "$KEYS" =~ '\[[ABCD]' ]]; then # If last key is also an arrow key, we just replace it if [[ ${ZVM_REPEAT_COMMANDS[-1]} =~ '\[[ABCD]' ]]; then ZVM_REPEAT_COMMANDS=(${ZVM_REPEAT_COMMANDS[@]:0:-1}) fi else # If last command is arrow key movement, we should reset # the repeat commands with i(nsert) command if [[ ${ZVM_REPEAT_COMMANDS[-1]} =~ '\[[ABCD]' ]]; then zvm_reset_repeat_commands $ZVM_MODE_NORMAL i fi char=${BUFFER[$CURSOR]} fi # If current key is backspace key, we should remove last # one, until it has only the mode and initial command if [[ "$KEYS" == '' ]]; then if ((${#ZVM_REPEAT_COMMANDS[@]} > 2)) && [[ ${ZVM_REPEAT_COMMANDS[-1]} != '' ]]; then ZVM_REPEAT_COMMANDS=(${ZVM_REPEAT_COMMANDS[@]:0:-1}) elif (($#LBUFFER > 0)); then ZVM_REPEAT_COMMANDS+=($KEYS) fi else ZVM_REPEAT_COMMANDS+=($char) fi } # Clipboard support function zvm_clipboard_detect() { if [[ -n $ZVM_CLIPBOARD_COPY_CMD && -n $ZVM_CLIPBOARD_PASTE_CMD ]]; then return fi if zvm_exist_command pbcopy && zvm_exist_command pbpaste; then ZVM_CLIPBOARD_COPY_CMD='pbcopy' ZVM_CLIPBOARD_PASTE_CMD='pbpaste' return fi if zvm_exist_command wl-copy && zvm_exist_command wl-paste; then ZVM_CLIPBOARD_COPY_CMD='wl-copy' ZVM_CLIPBOARD_PASTE_CMD='wl-paste -n' return fi if zvm_exist_command xclip; then ZVM_CLIPBOARD_COPY_CMD='xclip -selection clipboard' ZVM_CLIPBOARD_PASTE_CMD='xclip -selection clipboard -o' return fi if zvm_exist_command xsel; then ZVM_CLIPBOARD_COPY_CMD='xsel --clipboard -i' ZVM_CLIPBOARD_PASTE_CMD='xsel --clipboard -o' return fi } # Check if clipboard is available function zvm_clipboard_available() { zvm_clipboard_detect if [[ -n $ZVM_CLIPBOARD_COPY_CMD && -n $ZVM_CLIPBOARD_PASTE_CMD ]]; then return 0 fi return 1 } # Copy CUTBUFFER to system clipboard function zvm_clipboard_copy_buffer() { $ZVM_SYSTEM_CLIPBOARD_ENABLED || return zvm_clipboard_available || return print -rn -- "$CUTBUFFER" | eval "$ZVM_CLIPBOARD_COPY_CMD" >/dev/null 2>&1 } # Get content from system clipboard function zvm_clipboard_get() { zvm_clipboard_available || return eval "$ZVM_CLIPBOARD_PASTE_CMD" 2>/dev/null } # Paste content from system clipboard after cursor function zvm_paste_clipboard_after() { local content=$(zvm_clipboard_get) [[ -z $content ]] && return local saved=$CUTBUFFER CUTBUFFER=$content zvm_vi_put_after CUTBUFFER=$saved } # Paste content from system clipboard before cursor function zvm_paste_clipboard_before() { local content=$(zvm_clipboard_get) [[ -z $content ]] && return local saved=$CUTBUFFER CUTBUFFER=$content zvm_vi_put_before CUTBUFFER=$saved } # Paste content from system clipboard in visual mode function zvm_visual_paste_clipboard() { local content=$(zvm_clipboard_get) if [[ -z $content ]]; then zvm_exit_visual_mode return fi local ret=($(zvm_calc_selection)) local bpos=$ret[1] epos=$ret[2] local cpos=$((bpos + $#content - 1)) CUTBUFFER=${BUFFER:$bpos:$((epos-bpos))} BUFFER="${BUFFER:0:$bpos}${content}${BUFFER:$epos}" CURSOR=$cpos zvm_exit_visual_mode } # Updates editor information when line pre redraw function zvm_zle-line-pre-redraw() { # Fix cursor style is not updated in tmux environment, when # there are one more panel in the same window, the program # in other panel could change the cursor shape, we need to # update cursor style when line is redrawing. if [[ -n $TMUX ]]; then zvm_update_cursor # Fix display is not updated in the terminal of IntelliJ IDE. [[ "$TERMINAL_EMULATOR" == "JetBrains-JediTerm" ]] && zle redisplay fi zvm_update_highlight zvm_update_repeat_commands } # Start every prompt in the correct vi mode function zvm_zle-line-init() { # Save last mode local mode=${ZVM_MODE:-$ZVM_MODE_INSERT} # It's necessary to set to insert mode when line init # and we don't need to reset prompt. zvm_select_vi_mode $ZVM_MODE_INSERT false # Select line init mode and reset prompt case ${ZVM_LINE_INIT_MODE:-$mode} in $ZVM_MODE_INSERT) zvm_select_vi_mode $ZVM_MODE_INSERT;; *) zvm_select_vi_mode $ZVM_MODE_NORMAL;; esac } # Restore the user default cursor style after prompt finish function zvm_zle-line-finish() { # When we start a program (e.g. vim, bash, etc.) from the # command line, the cursor style is inherited by other # programs, so that we need to reset the cursor style to # default before executing a command and set the custom # style again when the command exits. This way makes any # other interactive CLI application would not be affected # by it. local shape=$(zvm_cursor_style $ZVM_CURSOR_USER_DEFAULT) zvm_set_cursor $shape zvm_switch_keyword_history=() } # Initialize vi-mode for widgets, keybindings, etc. function zvm_init() { # Check if it has been initialized if $ZVM_INIT_DONE; then return; fi # Mark plugin initial status ZVM_INIT_DONE=true zvm_exec_commands 'before_init' # Correct the readkey engine case $ZVM_READKEY_ENGINE in $ZVM_READKEY_ENGINE_NEX|$ZVM_READKEY_ENGINE_ZLE);; *) echo -n "Warning: Unsupported readkey engine! " echo "ZVM_READKEY_ENGINE=$ZVM_READKEY_ENGINE" ZVM_READKEY_ENGINE=$ZVM_READKEY_ENGINE_DEFAULT ;; esac # Reduce ESC delay (zle default is 0.4 seconds) # Set to 0.01 second delay for taking over the key input processing case $ZVM_READKEY_ENGINE in $ZVM_READKEY_ENGINE_NEX) KEYTIMEOUT=1;; $ZVM_READKEY_ENGINE_ZLE) KEYTIMEOUT=$(($ZVM_KEYTIMEOUT*100));; esac # Create User-defined widgets zvm_define_widget zvm_default_handler zvm_define_widget zvm_readkeys_handler zvm_define_widget zvm_backward_kill_region zvm_define_widget zvm_backward_kill_line zvm_define_widget zvm_forward_kill_line zvm_define_widget zvm_kill_line zvm_define_widget zvm_viins_undo zvm_define_widget zvm_select_surround zvm_define_widget zvm_change_surround zvm_define_widget zvm_move_around_surround zvm_define_widget zvm_change_surround_text_object zvm_define_widget zvm_enter_insert_mode zvm_define_widget zvm_exit_insert_mode zvm_define_widget zvm_enter_visual_mode zvm_define_widget zvm_exit_visual_mode zvm_define_widget zvm_enter_oppend_mode zvm_define_widget zvm_exit_oppend_mode zvm_define_widget zvm_exchange_point_and_mark zvm_define_widget zvm_open_line_below zvm_define_widget zvm_open_line_above zvm_define_widget zvm_insert_bol zvm_define_widget zvm_append_eol zvm_define_widget zvm_self_insert zvm_define_widget zvm_vi_replace zvm_define_widget zvm_vi_replace_chars zvm_define_widget zvm_vi_substitute zvm_define_widget zvm_vi_substitute_whole_line zvm_define_widget zvm_vi_change zvm_define_widget zvm_vi_change_eol zvm_define_widget zvm_vi_delete zvm_define_widget zvm_vi_yank zvm_define_widget zvm_vi_put_after zvm_define_widget zvm_vi_put_before zvm_define_widget zvm_vi_replace_selection zvm_define_widget zvm_vi_up_case zvm_define_widget zvm_vi_down_case zvm_define_widget zvm_vi_opp_case zvm_define_widget zvm_vi_edit_command_line zvm_define_widget zvm_repeat_change zvm_define_widget zvm_switch_keyword zvm_define_widget zvm_paste_clipboard_after zvm_define_widget zvm_paste_clipboard_before zvm_define_widget zvm_visual_paste_clipboard # Open URL under cursor zvm_define_widget zvm_open_under_cursor # Override standard widgets zvm_define_widget zle-line-pre-redraw zvm_zle-line-pre-redraw # Ensure the correct cursor style when an interactive program # (e.g. vim, bash, etc.) starts and exits zvm_define_widget zle-line-init zvm_zle-line-init zvm_define_widget zle-line-finish zvm_zle-line-finish # Override reset-prompt widget zvm_define_widget reset-prompt zvm_reset_prompt # All Key bindings # Emacs-like bindings # Normal editing zvm_bindkey viins '^A' beginning-of-line zvm_bindkey viins '^E' end-of-line zvm_bindkey viins '^B' backward-char zvm_bindkey viins '^F' forward-char zvm_bindkey viins '^K' zvm_forward_kill_line zvm_bindkey viins '^W' backward-kill-word zvm_bindkey viins '^U' zvm_viins_undo zvm_bindkey viins '^Y' yank zvm_bindkey viins '^_' undo # Mode agnostic editing zvm_bindkey viins '^[[H' beginning-of-line zvm_bindkey vicmd '^[[H' beginning-of-line zvm_bindkey viins '^[[F' end-of-line zvm_bindkey vicmd '^[[F' end-of-line zvm_bindkey viins '^[[3~' delete-char zvm_bindkey vicmd '^[[3~' delete-char # History search zvm_bindkey viins '^R' history-incremental-search-backward zvm_bindkey viins '^S' history-incremental-search-forward zvm_bindkey viins '^P' up-line-or-history zvm_bindkey viins '^N' down-line-or-history # Insert mode zvm_bindkey vicmd 'i' zvm_enter_insert_mode zvm_bindkey vicmd 'a' zvm_enter_insert_mode zvm_bindkey vicmd 'I' zvm_insert_bol zvm_bindkey vicmd 'A' zvm_append_eol # Other key bindings zvm_bindkey vicmd 'v' zvm_enter_visual_mode zvm_bindkey vicmd 'V' zvm_enter_visual_mode zvm_bindkey visual 'o' zvm_exchange_point_and_mark zvm_bindkey vicmd 'o' zvm_open_line_below zvm_bindkey vicmd 'O' zvm_open_line_above zvm_bindkey vicmd 'r' zvm_vi_replace_chars zvm_bindkey vicmd 'R' zvm_vi_replace zvm_bindkey vicmd 's' zvm_vi_substitute zvm_bindkey vicmd 'S' zvm_vi_substitute_whole_line zvm_bindkey vicmd 'C' zvm_vi_change_eol zvm_bindkey visual 'c' zvm_vi_change zvm_bindkey visual 'd' zvm_vi_delete zvm_bindkey visual 'x' zvm_vi_delete zvm_bindkey visual 'y' zvm_vi_yank zvm_bindkey vicmd 'p' zvm_vi_put_after zvm_bindkey vicmd 'P' zvm_vi_put_before zvm_bindkey visual 'p' zvm_vi_replace_selection zvm_bindkey visual 'P' zvm_vi_replace_selection zvm_bindkey visual 'U' zvm_vi_up_case zvm_bindkey visual 'u' zvm_vi_down_case zvm_bindkey visual '~' zvm_vi_opp_case zvm_bindkey visual 'v' zvm_vi_edit_command_line zvm_bindkey vicmd '.' zvm_repeat_change # Open URL under cursor zvm_bindkey vicmd 'gx' zvm_open_under_cursor # Clipboard support zvm_bindkey vicmd 'gp' zvm_paste_clipboard_after zvm_bindkey vicmd 'gP' zvm_paste_clipboard_before zvm_bindkey visual 'gp' zvm_visual_paste_clipboard zvm_bindkey visual 'gP' zvm_visual_paste_clipboard # Switch keyword zvm_bindkey vicmd '^A' zvm_switch_keyword zvm_bindkey vicmd '^X' zvm_switch_keyword # Keybindings for escape key and some specials local exit_oppend_mode_widget= local exit_insert_mode_widget= local exit_visual_mode_widget= local default_handler_widget= case $ZVM_READKEY_ENGINE in $ZVM_READKEY_ENGINE_NEX) exit_oppend_mode_widget=zvm_readkeys_handler exit_insert_mode_widget=zvm_readkeys_handler exit_visual_mode_widget=zvm_readkeys_handler ;; $ZVM_READKEY_ENGINE_ZLE) exit_insert_mode_widget=zvm_exit_insert_mode exit_visual_mode_widget=zvm_exit_visual_mode default_handler_widget=zvm_default_handler ;; esac # Bind custom escape key zvm_bindkey vicmd "$ZVM_VI_OPPEND_ESCAPE_BINDKEY" $exit_oppend_mode_widget zvm_bindkey viins "$ZVM_VI_INSERT_ESCAPE_BINDKEY" $exit_insert_mode_widget zvm_bindkey visual "$ZVM_VI_VISUAL_ESCAPE_BINDKEY" $exit_visual_mode_widget # Bind the default escape key if the escape key is not the default case "$ZVM_VI_OPPEND_ESCAPE_BINDKEY" in '^['|'\e') ;; *) zvm_bindkey vicmd '^[' $exit_oppend_mode_widget;; esac case "$ZVM_VI_INSERT_ESCAPE_BINDKEY" in '^['|'\e') ;; *) zvm_bindkey viins '^[' $exit_insert_mode_widget;; esac case "$ZVM_VI_VISUAL_ESCAPE_BINDKEY" in '^['|'\e') ;; *) zvm_bindkey visual '^[' $exit_visual_mode_widget;; esac # Bind and overwrite original y/d/c of vicmd for c in {y,d,c}; do zvm_bindkey vicmd "$c" $default_handler_widget done # Surround text-object # Enable surround text-objects (quotes, brackets) local surrounds=() # Append brackets for s in ${(s..)^:-'()[]{}<>'}; do surrounds+=($s) done # Append quotes for s in {\',\",\`,\ ,'^['}; do surrounds+=($s) done # Surround key bindings for s in $surrounds; do for c in {a,i}${s}; do zvm_bindkey visual "$c" zvm_select_surround done for c in {c,d,y}{a,i}${s}; do zvm_bindkey vicmd "$c" zvm_change_surround_text_object done if [[ $ZVM_VI_SURROUND_BINDKEY == 's-prefix' ]]; then for c in s{d,r}${s}; do zvm_bindkey vicmd "$c" zvm_change_surround done for c in sa${s}; do zvm_bindkey visual "$c" zvm_change_surround done else for c in {d,c}s${s}; do zvm_bindkey vicmd "$c" zvm_change_surround done for c in {S,ys}${s}; do zvm_bindkey visual "$c" zvm_change_surround done fi done # Moving around surrounds zvm_bindkey vicmd '%' zvm_move_around_surround # Fix BACKSPACE was stuck in zsh # Since normally '^?' (backspace) is bound to vi-backward-delete-char zvm_bindkey viins '^?' backward-delete-char # Initialize ZVM_MODE value case ${ZVM_LINE_INIT_MODE:-$ZVM_MODE_INSERT} in $ZVM_MODE_INSERT) ZVM_MODE=$ZVM_MODE_INSERT;; *) ZVM_MODE=$ZVM_MODE_NORMAL;; esac # Enable vi keymap bindkey -v zvm_exec_commands 'after_init' } # Check if a command is existed function zvm_exist_command() { command -v "$1" >/dev/null } # Execute commands function zvm_exec_commands() { local commands="zvm_${1}_commands" commands=(${(P)commands}) # Execute the default command if zvm_exist_command "zvm_$1"; then eval "zvm_$1" ${@:2} fi # Execute extra commands for cmd in $commands; do if zvm_exist_command ${cmd}; then cmd="$cmd ${@:2}" fi eval $cmd done } # Generate system report function zvm_system_report() { # OS local os_info= case "$(uname -s)" in Darwin) local product="$(sw_vers -productName)" local version="$(sw_vers -productVersion) ($(sw_vers -buildVersion))" os_info="${product} ${version}" ;; *) os_info="$(uname -s) ($(uname -r) $(uname -v) $(uname -m) $(uname -o))";; esac # Terminal Program local term_info="${TERM_PROGRAM:-unknown} ${TERM_PROGRAM_VERSION:-unknown}" term_info="${term_info} (${TERM})" # ZSH Frameworks local zsh_frameworks=() if zvm_exist_command "omz"; then zsh_frameworks+=("oh-my-zsh $(omz version)") fi if zvm_exist_command "starship"; then zsh_frameworks+=("$(starship --version | head -n 1)") fi if zvm_exist_command "antigen"; then zsh_frameworks+=("$(antigen version | head -n 1)") fi if zvm_exist_command "zplug"; then zsh_frameworks+=("zplug $(zplug --version | head -n 1)") fi if zvm_exist_command "zinit"; then # As `zinit version` information includes term style, in order # to acquire the pure text, we need to elimindate all the escape # sequences. local version=$(zinit version \ | head -n 1 \ | sed -E $'s/(\033\[[a-zA-Z0-9;]+ ?m)//g') zsh_frameworks+=("${version}") fi # Shell local shell=$SHELL if [[ -z $shell ]]; then shell=zsh fi ################# # System Report ################# print - "- Terminal program: ${term_info}" print - "- Operating system: ${os_info}" print - "- ZSH framework: ${(j:, :)zsh_frameworks}" print - "- ZSH version: $($shell --version)" print - "- ZVM version: $(zvm_version | head -n 1)" } # Load config by calling the config function if zvm_exist_command "$ZVM_CONFIG_FUNC"; then $ZVM_CONFIG_FUNC fi # Initialize this plugin according to the mode case $ZVM_INIT_MODE in sourcing) zvm_init;; *) precmd_functions+=(zvm_init);; esac