zsh-vi-mode.zsh
# 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 ^[ => <ESC>), 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 ^[ => <ESC>)
: ${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
      # <space> -> ` ` 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" == '' ]]; 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
  # postion 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 repace 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 == '<I>' ]]; 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+=('<I>')
          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 postion, 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

  # Reresh 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

  # Reresh 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. ', ", `, ~, ^, |, &, <space>)
    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<count; c++)); do
    eval $cmd

    exit_code=$?

    if $is_zle_cmd; then
      exit_code=0
    elif [[ $exit_code != 0 ]]; then
      CURSOR=$init_cursor
      break
    fi

    # If the cursor position is no change, we can break
    # the loop and no need to loop so many times, thus
    # when the count is quite large, it will not be
    # stuck for a long time.
    [[ $last_cursor == $CURSOR ]] && break

    last_cursor=$CURSOR
  done

  return $exit_code
}

# Edit command line in EDITOR
function zvm_vi_edit_command_line() {
  # Create a temporary file and save the BUFFER to it
  local tmp_file=$(mktemp ${ZVM_TMPDIR}/zshXXXXXX)

  # Some users may config the noclobber option to prevent from
  # overwriting existing files with the > 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 </dev/tty

  # Reload the content to the BUFFER from the temporary
  # file after editing, and delete the temporary file.
  BUFFER=$(cat "$tmp_file")
  rm "$tmp_file"

  # Exit the visual mode
  case $ZVM_MODE in
    $ZVM_MODE_VISUAL|$ZVM_MODE_VISUAL_LINE)
      zvm_exit_visual_mode
      ;;
  esac
}

# Check if content is valid in URL
function zvm_is_url() {
  local content="$1"

  # Check if it starts with a valid scheme
  if [[ "$content" =~ $ZVM_URL_REGEX ]]; then
    return 0
  fi

  return 1
}

# Check if content is a valid path
function zvm_is_path() {
  local content="$1"

  # Expand ~ if present
  if [[ "$content" =~ '^~' ]]; then
    content="${HOME}${content:1}"
  fi

  # Check if path exists
  if [[ -e "$content" ]]; then
    return 0
  fi

  return 1
}

# Select a URL or path under the cursor
function zvm_select_url_or_path() {
  local cursor=${1:-$CURSOR}
  local buffer=${2:-$BUFFER}
  local bpos= epos=
  local _bpos=0 _epos=$#buffer
  local content=

  # Find the beginning the current line
  for ((bpos=$cursor; $bpos>=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 openning 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 (<ESC> 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=

  # Replacment 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

  # Hanlde 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 mdoe 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
  # reseting 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 inital 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 neccessary 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 initalized
  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

