# 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 alway 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 hilight 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 defferent 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 # Hanle 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" == '