From ed4abbf0cbb9dac5bcac1da40b1ae7443bdcbc78 Mon Sep 17 00:00:00 2001 From: Huck Boles Date: Sun, 16 Apr 2023 19:09:02 -0500 Subject: [PATCH 1/1] creation --- .gitignore | 8 + .zprofile | 4 + .zshenv | 80 + .zshrc | 104 + include/aliases.zsh | 50 + include/autopair.zsh | 218 ++ include/dircycle.zsh | 41 + include/functions.zsh | 28 + include/fzf.zsh | 406 +++ include/git-prompt-enhanced.plugin.zsh | 126 + include/gitstatus.zsh | 126 + include/plugins.zsh | 16 + include/tmuxvim.zsh | 45 + include/zsh-vi-mode.zsh | 3459 ++++++++++++++++++++++++ include/zshmux.zsh | 40 + 15 files changed, 4751 insertions(+) create mode 100644 .gitignore create mode 100644 .zprofile create mode 100644 .zshenv create mode 100644 .zshrc create mode 100644 include/aliases.zsh create mode 100644 include/autopair.zsh create mode 100644 include/dircycle.zsh create mode 100644 include/functions.zsh create mode 100644 include/fzf.zsh create mode 100644 include/git-prompt-enhanced.plugin.zsh create mode 100644 include/gitstatus.zsh create mode 100644 include/plugins.zsh create mode 100644 include/tmuxvim.zsh create mode 100644 include/zsh-vi-mode.zsh create mode 100644 include/zshmux.zsh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ceca2e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +* +!.gitignore +!*.zsh +!.zshrc +!.zshenv +!.zprofile +!include/ + diff --git a/.zprofile b/.zprofile new file mode 100644 index 0000000..35e9ccf --- /dev/null +++ b/.zprofile @@ -0,0 +1,4 @@ +unset XDG_RUNTIME_DIR +export XDG_RUNTIME_DIR=$(mktemp -d /tmp/$(id -u)-runtime-dir.XXX) + +[[ -t 0 && $(tty) == /dev/tty1 && ! $DISPLAY ]] && exec startx diff --git a/.zshenv b/.zshenv new file mode 100644 index 0000000..a065e23 --- /dev/null +++ b/.zshenv @@ -0,0 +1,80 @@ +# xdg variables +export XDG_CONFIG_HOME="$HOME/.config" +export XDG_DATA_HOME="$HOME/.local/share" +export XDG_STATE_HOME="$HOME/.local/state" +export XDG_CACHE_HOME="$HOME/.local/cache" + +# shell variables +export SHELL="/bin/zsh" +export ZSH="$ZDOTDIR" +export ZSH_CUSTOM="$ZSH/custom" +export ZSH_COMPDUMP="$ZSH/cache/.zcompdump-$HOST" + +# my folders +export REPOS="$HOME/repos" +export DOTFILES="$REPOS/dotfiles" +export SCRIPTS="$REPOS/script" +export DOWNLOADS="$HOME/downloads" +export PACKAGES="$HOME/.packages" +export CONFIG="$XDG_CONFIG_HOME" + +# system variables +export LANG=en_US.UTF-8 + +# path variables +export PATH="$PATH:$HOME/.local/bin:$REPOS/script:$REPOS/ruby:$XDG_DATA_HOME/cargo/bin:." +export CDPATH=".:$HOME:$HOME/$REPOS:$HOME/$XDG_CONFIG_HOME:/" + +# language variables +export GOPATH="$XDG_DATA_HOME/go" +export PERL_LOCAL_LIB_ROOT="$XDG_DATA_HOME/perl5" +export CARGO_HOME="$XDG_DATA_HOME/cargo" +export RUSTUP_HOME="$XDG_DATA_HOME/rustup" + +# terminal setting: +[[ -n $TMUX ]] && export TERM=tmux-256color || export TERM=st-256color + +# preferred programs +export PAGER='less' +export LESS='-R --use-color -Dd+r$Du+b' +export MANPAGER='less -R --use-color -Dd+r -Du+b' +export EDITOR="/usr/bin/nvim" +export DIFFPROG="/usr/bin/nvim -d" +export VISUAL="/usr/bin/nvim" +export BROWSER="/usr/bin/firefox" +export EMAIL='huck@huck.website' + +# vim config +export MYVIMRC="$XDG_CONFIG_HOME/nvim/init.lua" + +# config paths +export GTK_2_RC_FILES="$XDG_CONFIG_HOME/gtk-2.0/gtkrc" +export LESSHISTFILE="$XDG_STATE_HOME/less/history" +export TERMINFO="$XDG_DATA_HOME/terminfo" +export TERMINFO_DIRS="$XDG_DATA_HOME/terminfo:/usr/share/terminfo" +export INPUTRC="$XDG_CONFIG_HOME/readline/inputrc" +export CUDA_CACHE_PATH="$XDG_CACHE_HOME/nv" + +# zoxide +export _ZO_ECHO='1' +export _ZO_RESOLVE_SYMLINKS='1' + +# 16 theme colors +export BACKGROUND='#151515' +export FOREGROUND='#d7d0c7' +export BLACK='#101010' +export GREY='#404040' +export RED='#e84f4f' +export BRRED='#d23d3d' +export GREEN='#b8d68c' +export BRGREEN='#a0cf5d' +export YELLOW='#e1aa5d' +export BRYELLOW='#f39d21' +export BLUE='#4D88FF' +export BRBLUE='#2B76D4' +export MAGENTA='#9b64fb' +export BRMAGENTA='#8542ff' +export CYAN='#00CCCC' +export BRCYAN='#21d6c7' +export WHITE='#909090' +export BRWHITE='#dddddd' diff --git a/.zshrc b/.zshrc new file mode 100644 index 0000000..bcf2641 --- /dev/null +++ b/.zshrc @@ -0,0 +1,104 @@ +#shellcheck disable=all + +# sourcing ------------------------------------------------- +. $ZSH/.zshenv +ZSHEXTRA=$ZSH/extra +ZSHINCLUDE=$ZSH/include +[[ ! -d $ZSH ]] && mkdir $ZSH/{cache,extra,include} + +fpath+="$ZSHEXTRA/zfunc" + +# source files from include dir +for file in $ZSHINCLUDE/*; do . ${file}; done + +# start programs +[[ -f $XDG_CONFIG_HOME/.dircolors ]] && eval $(dircolors -b $XDG_CONFIG_HOME/.dircolors) +[[ $(which zoxide) ]] && eval "$(zoxide init zsh --cmd z)" + +# options ------------------------------------------------------------ +ENABLE_CORRECTION="true" +COMPLETION_WAITING_DOTS="true" +HISTFILE=$ZSH/history/zsh-history +HISTORY_LOG=$ZSH/history/zsh-history.log +SAVEHIST=1000000 +HISTSIZE=10000 +HIST_STAMPS="yyyy-mm-dd" +REPORTTIME=5 +DIRSTACKSISZE=10 + +setopt autopushd chase_links +setopt share_history append_history inc_append_history extended_history +setopt hist_expire_dups_first hist_ignore_dups hist_find_no_dups hist_ignore_all_dups hist_reduce_blanks +setopt correct correct_all +setopt list_ambiguous +setopt mark_dirs interactive_comments +setopt append_create clobber_empty +setopt hash_cmds hash_dirs +setopt path_script prompt_subst +setopt auto_param_keys auto_param_slash auto_remove_slash +setopt null_glob glob glob_dots glob_star_short bang_hist +unsetopt flow_control + +# completion --------------------------------------------------------- +zstyle ':completion:*' auto-description '%d' +zstyle ':completion:*' completer _expand _complete _ignored _correct _approximate +zstyle ':completion:*' completions 1 +zstyle ':completion:*' expand prefix suffix +zstyle ':completion:*' file-sort access +zstyle ':completion:*' format '%d' +zstyle ':completion:*' glob 1 +zstyle ':completion:*' group-name '' +zstyle ':completion:*' ignore-parents parent pwd +zstyle ':completion:*' insert-unambiguous true +zstyle ':completion:*' list-colors ${(s.:.)LS_COLORS} +zstyle ':completion:*' list-prompt %SAt %p: Hit TAB for more, or the character to insert%s +zstyle ':completion:*' matcher-list '' 'm:{[:lower:]}={[:upper:]}' 'r:|[._-,]=** r:|=**' 'l:|=* r:|=*' +zstyle ':completion:*' max-errors 3 +zstyle ':completion:*' menu select=long interactive +zstyle ':completion:*' original true +zstyle ':completion:*' prompt '%e' +zstyle ':completion:*' select-prompt %SScrolling active: current selection at %p%s +zstyle ':completion:*' squeeze-slashes true +zstyle ':completion:*' substitute 1 +zstyle ':completion:*' verbose true + +autoload -Uz compinit +for dump in ~/.zcompdump(N.mh+24); do + compinit +done +compinit -C +zshcache_time="$(date +%s%N)" + +# zle plugins ------------------------------------------------------- +autoload -Uz chpwd_recent_dirs cdr add-zsh-hook + +add-zsh-hook chpwd chpwd_recent_dirs + +chpwd() { ls -Ah } + +autoload -Uz run-help + +# keybindings ------------------------------------------------------- +bindkey "\e[1~" beginning-of-line +bindkey "\e[4~" end-of-line +bindkey "\e[5~" beginning-of-history +bindkey "\e[6~" end-of-history +bindkey "\e[3~" delete-char +bindkey "\e[2~" quoted-insert + +# custom prompt ----------------------------------------------------- +if [[ -n $SSH_CONNECTION ]]; then +PROMPT='%(?..%B%F{red}x) +%B%F{yellow}%n%b%f@%B%F{blue}%M%b%f -> %F{green}%d %F{red}[SSH] %B%F{magenta}[gentoo]%f%b %B%F{yellow}%t %W +%B%F{green}%(!.#.$) %f%b> ' +RPROMPT='$(git_prompt_enhanced_status) %f[%!]' +else +PROMPT='%(?..%B%F{red}x) +%B%F{green}%n%b%f@%B%F{red}%m%b%f: %F{blue}%~ %f$(git_prompt_enhanced_status) +%B%F{green}%(!.#.$) %f%b> ' +RPROMPT='%f[%!]' +fi + +PS2='%B%F{green}%(!.#.$)%f%b > %F{cyan}*%f ' +PS3='%B%F{green}%(!.#.$)%f%b %B%F{magenta}? ' + diff --git a/include/aliases.zsh b/include/aliases.zsh new file mode 100644 index 0000000..94d65eb --- /dev/null +++ b/include/aliases.zsh @@ -0,0 +1,50 @@ +#shell aliases +alias cp='cp -vr' +alias rm='rm -v' +alias mv='mv -v' +alias mkdir='mkdir -pv' +alias mount='mount --mkdir' +alias lsblk='lsblk -f' +alias reload='source $ZSH/.zshrc' +alias quit='disown -a && exit' +alias :q='exit' + +# convinience aliases +alias v='nvim' +alias sv='sudoedit' +alias vsesh='nvim -S' +alias vdiff='nvim -d' +alias ..='cd ..' +alias ...='cd ../..' +alias ....='cd ../../..' +alias h='history 0' +alias j='jobs -l' +alias c='clear' +alias ear='clear' +alias zc='z;clear' +alias make='make -j16 -l6 CFLAGS="-march=bdver1"' +alias dust='dust -rb' + +# ls/exa aliases +alias ls='lsd' +alias l='ls' +alias ll='ls -lAh' +alias la='ls -Ah' +alias lt='ls --tree' + +# color aliases +alias ip='ip --color=auto' +alias grep='grep --color=auto' + +# tmux aliases +alias tmux='tmux' +alias tlist='tmux list-sessions' +alias tsesh='tmux new-session -s' +alias tkill='tmux kill-session -t' +alias tattach='tmux attach-session -t' + +# config aliases +alias wget='wget --hsts-file="$XDG_DATA_HOME/wget-hsts"' +alias xrdb='xrdb -load "XDG_CONFIG_HOME/X11/xresources"' +alias background='feh --bg-fill --no-fehbg --randomize /home/huck/media/photos/backgrounds' +alias nvidia-settings='nvidia-settings --config=$XDG_CONFIG_HOME/nvidia/settings' diff --git a/include/autopair.zsh b/include/autopair.zsh new file mode 100644 index 0000000..3295ada --- /dev/null +++ b/include/autopair.zsh @@ -0,0 +1,218 @@ +#!/usr/bin/env zsh + +AUTOPAIR_INHIBIT_INIT=${AUTOPAIR_INHIBIT_INIT:-} +AUTOPAIR_BETWEEN_WHITESPACE=${AUTOPAIR_BETWEEN_WHITESPACE:-} +AUTOPAIR_SPC_WIDGET=${AUTOPAIR_SPC_WIDGET:-"$(bindkey " " | cut -c5-)"} +AUTOPAIR_BKSPC_WIDGET=${AUTOPAIR_BKSPC_WIDGET:-"$(bindkey "^?" | cut -c6-)"} + +typeset -gA AUTOPAIR_PAIRS +AUTOPAIR_PAIRS=('`' '`' "'" "'" '"' '"' '{' '}' '[' ']' '(' ')' ' ' ' ') + +typeset -gA AUTOPAIR_LBOUNDS +AUTOPAIR_LBOUNDS=(all '[.:/\!]') +AUTOPAIR_LBOUNDS+=(quotes '[]})a-zA-Z0-9]') +AUTOPAIR_LBOUNDS+=(spaces '[^{([]') +AUTOPAIR_LBOUNDS+=(braces '') +AUTOPAIR_LBOUNDS+=('`' '`') +AUTOPAIR_LBOUNDS+=('"' '"') +AUTOPAIR_LBOUNDS+=("'" "'") + +typeset -gA AUTOPAIR_RBOUNDS +AUTOPAIR_RBOUNDS=(all '[[{(<,.:?/%$!a-zA-Z0-9]') +AUTOPAIR_RBOUNDS+=(quotes '[a-zA-Z0-9]') +AUTOPAIR_RBOUNDS+=(spaces '[^]})]') +AUTOPAIR_RBOUNDS+=(braces '') + + +### Helpers ############################ + +# Returns the other pair for $1 (a char), blank otherwise +_ap-get-pair() { + if [[ -n $1 ]]; then + echo $AUTOPAIR_PAIRS[$1] + elif [[ -n $2 ]]; then + local i + for i in ${(@k)AUTOPAIR_PAIRS}; do + [[ $2 == $AUTOPAIR_PAIRS[$i] ]] && echo $i && break + done + fi +} + +# Return 0 if cursor's surroundings match either regexp: $1 (left) or $2 (right) +_ap-boundary-p() { + [[ -n $1 && $LBUFFER =~ "$1$" ]] || [[ -n $2 && $RBUFFER =~ "^$2" ]] +} + +# Return 0 if the surrounding text matches any of the AUTOPAIR_*BOUNDS regexps +_ap-next-to-boundary-p() { + local -a groups + groups=(all) + case $1 in + \'|\"|\`) groups+=quotes ;; + \{|\[|\(|\<) groups+=braces ;; + " ") groups+=spaces ;; + esac + groups+=$1 + local group + for group in $groups; do + _ap-boundary-p $AUTOPAIR_LBOUNDS[$group] $AUTOPAIR_RBOUNDS[$group] && return 0 + done + return 1 +} + +# Return 0 if there are the same number of $1 as there are $2 (chars; a +# delimiter pair) in the buffer. +_ap-balanced-p() { + local lbuf="${LBUFFER//\\$1}" + local rbuf="${RBUFFER//\\$2}" + local llen="${#lbuf//[^$1]}" + local rlen="${#rbuf//[^$2]}" + if (( rlen == 0 && llen == 0 )); then + return 0 + elif [[ $1 == $2 ]]; then + if [[ $1 == " " ]]; then + # Silence WARN_CREATE_GLOBAL errors + local match= + local mbegin= + local mend= + # Balancing spaces is unnecessary. If there is at least one space on + # either side of the cursor, it is considered balanced. + [[ $LBUFFER =~ "[^'\"]([ ]+)$" && $RBUFFER =~ "^${match[1]}" ]] && return 0 + return 1 + elif (( llen == rlen || (llen + rlen) % 2 == 0 )); then + return 0 + fi + else + local l2len="${#lbuf//[^$2]}" + local r2len="${#rbuf//[^$1]}" + local ltotal=$((llen - l2len)) + local rtotal=$((rlen - r2len)) + + (( ltotal < 0 )) && ltotal=0 + (( ltotal < rtotal )) && return 1 + return 0 + fi + return 1 +} + +# Return 0 if the last keypress can be auto-paired. +_ap-can-pair-p() { + local rchar="$(_ap-get-pair $KEYS)" + + [[ -n $rchar ]] || return 1 + + if [[ $rchar != " " ]]; then + # Force pair if surrounded by space/[BE]OL, regardless of + # boundaries/balance + [[ -n $AUTOPAIR_BETWEEN_WHITESPACE && \ + $LBUFFER =~ "(^|[ ])$" && \ + $RBUFFER =~ "^($|[ ])" ]] && return 0 + + # Don't pair quotes if the delimiters are unbalanced + ! _ap-balanced-p $KEYS $rchar && return 1 + elif [[ $RBUFFER =~ "^[ ]*$" ]]; then + # Don't pair spaces surrounded by whitespace + return 1 + fi + + # Don't pair when in front of characters that likely signify the start of a + # string, path or undesirable boundary. + _ap-next-to-boundary-p $KEYS $rchar && return 1 + + return 0 +} + +# Return 0 if the adjacent character (on the right) can be safely skipped over. +_ap-can-skip-p() { + if [[ -z $LBUFFER ]]; then + return 1 + elif [[ $1 == $2 ]]; then + if [[ $1 == " " ]]; then + return 1 + elif ! _ap-balanced-p $1 $2; then + return 1 + fi + fi + if ! [[ -n $2 && $RBUFFER[1] == $2 && $LBUFFER[-1] != '\' ]]; then + return 1 + fi + return 0 +} + +# Return 0 if the adjacent character (on the right) can be safely deleted. +_ap-can-delete-p() { + local lchar="$LBUFFER[-1]" + local rchar="$(_ap-get-pair $lchar)" + ! [[ -n $rchar && $RBUFFER[1] == $rchar ]] && return 1 + if [[ $lchar == $rchar ]]; then + if [[ $lchar == ' ' && ( $LBUFFER =~ "[^{([] +$" || $RBUFFER =~ "^ +[^]})]" ) ]]; then + # Don't collapse spaces unless in delimiters + return 1 + elif ! _ap-balanced-p $lchar $rchar; then + return 1 + fi + fi + return 0 +} + +# Insert $1 and add $2 after the cursor +_ap-self-insert() { + LBUFFER+=$1 + RBUFFER="$2$RBUFFER" +} + + +### Widgets ############################ + +autopair-insert() { + local rchar="$(_ap-get-pair $KEYS)" + if [[ $KEYS == (\'|\"|\`| ) ]] && _ap-can-skip-p $KEYS $rchar; then + zle forward-char + elif _ap-can-pair-p; then + _ap-self-insert $KEYS $rchar + elif [[ $rchar == " " ]]; then + zle ${AUTOPAIR_SPC_WIDGET:-self-insert} + else + zle self-insert + fi +} + +autopair-close() { + if _ap-can-skip-p "$(_ap-get-pair "" $KEYS)" $KEYS; then + zle forward-char + else + zle self-insert + fi +} + +autopair-delete() { + _ap-can-delete-p && RBUFFER=${RBUFFER:1} + zle ${AUTOPAIR_BKSPC_WIDGET:-backward-delete-char} +} + + +### Initialization ##################### + +autopair-init() { + zle -N autopair-insert + zle -N autopair-close + zle -N autopair-delete + + local p + for p in ${(@k)AUTOPAIR_PAIRS}; do + bindkey "$p" autopair-insert + bindkey -M isearch "$p" self-insert + + local rchar="$(_ap-get-pair $p)" + if [[ $p != $rchar ]]; then + bindkey "$rchar" autopair-close + bindkey -M isearch "$rchar" self-insert + fi + done + + bindkey "^?" autopair-delete + bindkey "^h" autopair-delete + bindkey -M isearch "^?" backward-delete-char + bindkey -M isearch "^h" backward-delete-char +} +[[ -n $AUTOPAIR_INHIBIT_INIT ]] || autopair-init diff --git a/include/dircycle.zsh b/include/dircycle.zsh new file mode 100644 index 0000000..2269e25 --- /dev/null +++ b/include/dircycle.zsh @@ -0,0 +1,41 @@ +# A Zsh plugin for cycling through the directory stack. + +_dircycle_update_cycled() { + setopt localoptions nopushdminus + + [[ ${#dirstack} -eq 0 ]] && return + + while ! builtin pushd -q $1 &>/dev/null; do + # A missing directory was found; pop it out of the directory stack. + builtin popd -q $1 + + # Stop trying if there are no more directories in the directory stack. + [[ ${#dirstack} -eq 0 ]] && break + done + + # Trigger a prompt update if using Pure (https://github.com/sindresorhus/pure). + if typeset -f prompt_pure_setup > /dev/null; then + prompt_pure_async_tasks + prompt_pure_preprompt_render + else + zle reset-prompt + fi +} + +_dircycle_insert_cycled_left() { + _dircycle_update_cycled +1 || true +} + +zle -N _dircycle_insert_cycled_left + +_dircycle_insert_cycled_right() { + _dircycle_update_cycled -0 || true +} + +zle -N _dircycle_insert_cycled_right + +# Bind Ctrl + Shift + Left key combination to backwards direction. +bindkey "\e[1;6D" _dircycle_insert_cycled_left + +# Bind Ctrl + Shift + Right key combination to forwards direction. +bindkey "\e[1;6C" _dircycle_insert_cycled_right diff --git a/include/functions.zsh b/include/functions.zsh new file mode 100644 index 0000000..b382f7f --- /dev/null +++ b/include/functions.zsh @@ -0,0 +1,28 @@ +#!/bin/bash + +# easy find files +f() { + find . | grep "$1"; +} + +cd() { + z $1 + la +} + +# smart remove +rem() { + if [ ! -d "$HOME/.trash" ]; then mkdir "$HOME/.trash"; fi + for arg in "$@" + do + if [ "$(ls -A $arg)" ]; then # move to trash if non empty dir + mv -f "$arg" "$HOME/.trash/$1" + elif [ -d "$arg" ]; then # delete empty directories + rmdir "$arg" + elif [ -f "$arg" ]; then # move files to trash + mv -f "$arg" "$HOME/.trash/$1" + else + echo -e "\n\033[31;1;4mFile Not Found:\033[0m\033[34;1m $1\033[0m\n" + fi + done +} diff --git a/include/fzf.zsh b/include/fzf.zsh new file mode 100644 index 0000000..acf63a8 --- /dev/null +++ b/include/fzf.zsh @@ -0,0 +1,406 @@ +# ____ ____ +# / __/___ / __/ +# / /_/_ / / /_ +# / __/ / /_/ __/ +# /_/ /___/_/ completion.zsh +# +# - $FZF_TMUX (default: 0) +# - $FZF_TMUX_OPTS (default: '-d 40%') +# - $FZF_COMPLETION_TRIGGER (default: '**') +# - $FZF_COMPLETION_OPTS (default: empty) + +# Both branches of the following `if` do the same thing -- define +# __fzf_completion_options such that `eval $__fzf_completion_options` sets +# all options to the same values they currently have. We'll do just that at +# the bottom of the file after changing options to what we prefer. +# +# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted +# *must* be quoted in order to prevent alias expansion. In addition, code must +# be written in a way works with any set of zsh options. This is very tricky, so +# careful when you change it. +# +# Start by loading the builtin zsh/parameter module. It provides `options` +# associative array that stores current shell options. +if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then + # This is the fast branch and it gets taken on virtually all Zsh installations. + # + # ${(kv)options[@]} expands to array of keys (option names) and values ("on" + # or "off"). The subsequent expansion# with (j: :) flag joins all elements + # together separated by spaces. __fzf_completion_options ends up with a value + # like this: "options=(shwordsplit off aliases on ...)". + __fzf_completion_options="options=(${(j: :)${(kv)options[@]}})" +else + # This branch is much slower because it forks to get the names of all + # zsh options. It's possible to eliminate this fork but it's not worth the + # trouble because this branch gets taken only on very ancient or broken + # zsh installations. + () { + # That `()` above defines an anonymous function. This is essentially a scope + # for local parameters. We use it to avoid polluting global scope. + 'local' '__fzf_opt' + __fzf_completion_options="setopt" + # `set -o` prints one line for every zsh option. Each line contains option + # name, some spaces, and then either "on" or "off". We just want option names. + # Expansion with (@f) flag splits a string into lines. The outer expansion + # removes spaces and everything that follow them on every line. __fzf_opt + # ends up iterating over option names: shwordsplit, aliases, etc. + for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do + if [[ -o "$__fzf_opt" ]]; then + # Option $__fzf_opt is currently on, so remember to set it back on. + __fzf_completion_options+=" -o $__fzf_opt" + else + # Option $__fzf_opt is currently off, so remember to set it back off. + __fzf_completion_options+=" +o $__fzf_opt" + fi + done + # The value of __fzf_completion_options here looks like this: + # "setopt +o shwordsplit -o aliases ..." + } +fi + +# Enable the default zsh options (those marked with in `man zshoptions`) +# but without `aliases`. Aliases in functions are expanded when functions are +# defined, so if we disable aliases here, we'll be sure to have no pesky +# aliases in any of our functions. This way we won't need prefix every +# command with `command` or to quote every word to defend against global +# aliases. Note that `aliases` is not the only option that's important to +# control. There are several others that could wreck havoc if they are set +# to values we don't expect. With the following `emulate` command we +# sidestep this issue entirely. +'emulate' 'zsh' '-o' 'no_aliases' + +# This brace is the start of try-always block. The `always` part is like +# `finally` in lesser languages. We use it to *always* restore user options. +{ + +# Bail out if not interactive shell. +[[ -o interactive ]] || return 0 + +# To use custom commands instead of find, override _fzf_compgen_{path,dir} +if ! declare -f _fzf_compgen_path > /dev/null; then + _fzf_compgen_path() { + echo "$1" + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +if ! declare -f _fzf_compgen_dir > /dev/null; then + _fzf_compgen_dir() { + command find -L "$1" \ + -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \ + -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@' + } +fi + +########################################################### + +__fzf_comprun() { + if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then + _fzf_comprun "$@" + elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then + shift + if [ -n "$FZF_TMUX_OPTS" ]; then + fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@" + else + fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@" + fi + else + shift + fzf "$@" + fi +} + +# Extract the name of the command. e.g. foo=1 bar baz** +__fzf_extract_command() { + local token tokens + tokens=(${(z)1}) + for token in $tokens; do + token=${(Q)token} + if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then + echo "$token" + return + fi + done + echo "${tokens[1]}" +} + +__fzf_generic_path_completion() { + local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches + base=$1 + lbuf=$2 + cmd=$(__fzf_extract_command "$lbuf") + compgen=$3 + fzf_opts=$4 + suffix=$5 + tail=$6 + + setopt localoptions nonomatch + eval "base=$base" + [[ $base = *"/"* ]] && dir="$base" + while [ 1 ]; do + if [[ -z "$dir" || -d ${dir} ]]; then + leftover=${base/#"$dir"} + leftover=${leftover/#\/} + [ -z "$dir" ] && dir='.' + [ "$dir" != "/" ] && dir="${dir/%\//}" + matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do + item="${item%$suffix}$suffix" + echo -n "${(q)item} " + done) + matches=${matches% } + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches$tail" + fi + zle reset-prompt + break + fi + dir=$(dirname "$dir") + dir=${dir%/}/ + done +} +_fzf_path_completion() { + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_path \ + "-m" "" " " +} +_fzf_dir_completion() { + __fzf_generic_path_completion "$1" "$2" _fzf_compgen_dir \ + "" "/" "" +} +_fzf_feed_fifo() ( + command rm -f "$1" + mkfifo "$1" + cat <&0 > "$1" & +) +_fzf_complete() { + setopt localoptions ksh_arrays + # Split arguments around -- + local args rest str_arg i sep + args=("$@") + sep= + for i in {0..${#args[@]}}; do + if [[ "${args[$i]}" = -- ]]; then + sep=$i + break + fi + done + if [[ -n "$sep" ]]; then + str_arg= + rest=("${args[@]:$((sep + 1)):${#args[@]}}") + args=("${args[@]:0:$sep}") + else + str_arg=$1 + args=() + shift + rest=("$@") + fi + local fifo lbuf cmd matches post + fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$" + lbuf=${rest[0]} + cmd=$(__fzf_extract_command "$lbuf") + post="${funcstack[1]}_post" + type $post > /dev/null 2>&1 || post=cat + _fzf_feed_fifo "$fifo" + matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') + if [ -n "$matches" ]; then + LBUFFER="$lbuf$matches" + fi + command rm -f "$fifo" +} +_fzf_complete_telnet() { + _fzf_complete +m -- "$@" < <( + command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) +} +_fzf_complete_ssh() { + _fzf_complete +m -- "$@" < <( + setopt localoptions nonomatch + command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \ + <(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \ + <(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') | + awk '{if (length($2) > 0) {print $2}}' | sort -u + ) +} +_fzf_complete_export() { + _fzf_complete -m -- "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} +_fzf_complete_unset() { + _fzf_complete -m -- "$@" < <( + declare -xp | sed 's/=.*//' | sed 's/.* //' + ) +} +_fzf_complete_unalias() { + _fzf_complete +m -- "$@" < <( + alias | sed 's/=.*//' + ) +} +_fzf_complete_kill() { + _fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <( + command ps -ef | sed 1d + ) +} +_fzf_complete_kill_post() { + awk '{print $2}' +} +fzf-completion() { + local tokens cmd prefix trigger tail matches lbuf d_cmds + setopt localoptions noshwordsplit noksh_arrays noposixbuiltins + # http://zsh.sourceforge.net/FAQ/zshfaq03.html + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags + tokens=(${(z)LBUFFER}) + if [ ${#tokens} -lt 1 ]; then + zle ${fzf_default_completion:-expand-or-complete} + return + fi + cmd=$(__fzf_extract_command "$LBUFFER") + # Explicitly allow for empty trigger. + trigger=${FZF_COMPLETION_TRIGGER-'**'} + [ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("") + # When the trigger starts with ';', it becomes a separate token + if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then + tokens[-2]="${tokens[-2]}${tokens[-1]}" + tokens=(${tokens[0,-2]}) + fi + lbuf=$LBUFFER + tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))} + # Trigger sequence given + if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then + d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) + [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} + [ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}} + if eval "type _fzf_complete_${cmd} > /dev/null"; then + prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf} + zle reset-prompt + elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then + _fzf_dir_completion "$prefix" "$lbuf" + else + _fzf_path_completion "$prefix" "$lbuf" + fi + # Fall back to default completion + else + zle ${fzf_default_completion:-expand-or-complete} + fi +} +[ -z "$fzf_default_completion" ] && { + binding=$(bindkey '^I') + [[ $binding =~ 'undefined-key' ]] || fzf_default_completion=$binding[(s: :w)2] + unset binding +} +zle -N fzf-completion +bindkey '^I' fzf-completion +} always { + # Restore the original options. + eval $__fzf_completion_options + 'unset' '__fzf_completion_options' +} +# Key bindings +# ------------ + +# The code at the top and the bottom of this file is the same as in completion.zsh. +# Refer to that file for explanation. +if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then + __fzf_key_bindings_options="options=(${(j: :)${(kv)options[@]}})" +else + () { + __fzf_key_bindings_options="setopt" + 'local' '__fzf_opt' + for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do + if [[ -o "$__fzf_opt" ]]; then + __fzf_key_bindings_options+=" -o $__fzf_opt" + else + __fzf_key_bindings_options+=" +o $__fzf_opt" + fi + done + } +fi + +'emulate' 'zsh' '-o' 'no_aliases' + +{ + +[[ -o interactive ]] || return 0 + +# CTRL-T - Paste the selected file path(s) into the command line +__fsel() { + local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type f -print \ + -o -type d -print \ + -o -type l -print 2> /dev/null | cut -b3-"}" + setopt localoptions pipefail no_aliases 2> /dev/null + local item + eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do + echo -n "${(q)item} " + done + local ret=$? + echo + return $ret +} + +__fzfcmd() { + [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } && + echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf" +} + +fzf-file-widget() { + LBUFFER="${LBUFFER}$(__fsel)" + local ret=$? + zle reset-prompt + return $ret +} +zle -N fzf-file-widget +bindkey -M emacs '^T' fzf-file-widget +bindkey -M vicmd '^T' fzf-file-widget +bindkey -M viins '^T' fzf-file-widget + +# ALT-C - cd into the selected directory +fzf-cd-widget() { + local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \ + -o -type d -print 2> /dev/null | cut -b3-"}" + setopt localoptions pipefail no_aliases 2> /dev/null + local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)" + if [[ -z "$dir" ]]; then + zle redisplay + return 0 + fi + zle push-line # Clear buffer. Auto-restored on next prompt. + BUFFER="builtin cd -- ${(q)dir}" + zle accept-line + local ret=$? + unset dir # ensure this doesn't end up appearing in prompt expansion + zle reset-prompt + return $ret +} +zle -N fzf-cd-widget +bindkey -M emacs '\ec' fzf-cd-widget +bindkey -M vicmd '\ec' fzf-cd-widget +bindkey -M viins '\ec' fzf-cd-widget + +# CTRL-R - Paste the selected command from history into the command line +fzf-history-widget() { + local selected num + setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null + selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | + FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) ) + local ret=$? + if [ -n "$selected" ]; then + num=$selected[1] + if [ -n "$num" ]; then + zle vi-fetch-history -n $num + fi + fi + zle reset-prompt + return $ret +} +zle -N fzf-history-widget +bindkey -M emacs '^R' fzf-history-widget +bindkey -M vicmd '^R' fzf-history-widget +bindkey -M viins '^R' fzf-history-widget + +} always { + eval $__fzf_key_bindings_options + 'unset' '__fzf_key_bindings_options' +} diff --git a/include/git-prompt-enhanced.plugin.zsh b/include/git-prompt-enhanced.plugin.zsh new file mode 100644 index 0000000..592e06d --- /dev/null +++ b/include/git-prompt-enhanced.plugin.zsh @@ -0,0 +1,126 @@ +# Handle $0 according to the standard: +# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html +0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" +0="${${(M)0:#/*}:-$PWD/$0}" + +function update_git_branch_variables() { + GIT_BRANCH="${$(git branch --show-current):-"$(git rev-parse --short HEAD)"}" + GIT_AHEAD=$(git rev-list --count @{upstream}..HEAD 2>/dev/null) + GIT_BEHIND=$(git rev-list --count HEAD..@{upstream} 2>/dev/null) + + GIT_STASHED=0 + if $(git rev-parse --verify refs/stash &>/dev/null); then + GIT_STASHED=1 + fi + + GIT_NO_UPSTREAM=1 + if $(git rev-parse --abbrev-ref --symbolic-full-name @{u} &>/dev/null); then + GIT_NO_UPSTREAM=0 + fi +} + +function update_git_status_varaibles() { + GIT_MODIFIED=0 + GIT_DELETED=0 + GIT_STAGED=0 + GIT_UNTRACKED=0 + GIT_CONFLICTS=0 + + GIT_STATUS=$(git status --porcelain -uall 2>/dev/null) + + while IFS= read -r line; do + if [[ "$line" =~ '^\?\?.*' ]]; then + GIT_UNTRACKED=$(($GIT_UNTRACKED+1)) + else + if [[ "$line" =~ ^U.* ]]; then + GIT_CONFLICTS=$(($GIT_CONFLICTS+1)) + elif [[ "$line" =~ ^[MRAD].* ]]; then + GIT_STAGED=$(($GIT_STAGED+1)) + fi + + if [[ "$line" =~ ^.M.* ]]; then + GIT_MODIFIED=$(($GIT_MODIFIED+1)) + elif [[ "$line" =~ ^.D.* ]]; then + GIT_DELETED=$(($GIT_DELETED+1)) + fi + fi + done <<< "$GIT_STATUS" +} + +function compose_git_prompt_string() { + SEPARATOR=$ZSH_THEME_GIT_PROMPT_STATUS_SEPARATOR + + GIT_PROMPT="$ZSH_THEME_GIT_PROMPT_PREFIX$ZSH_THEME_GIT_PROMPT_BRANCH$GIT_BRANCH%{${reset_color}%}" + + if [ "$GIT_NO_UPSTREAM" -eq "1" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_NO_UPSTREAM%{${reset_color}%}" + fi + if [ "$GIT_AHEAD" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_AHEAD$GIT_AHEAD%{${reset_color}%}" + fi + if [ "$GIT_BEHIND" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_BEHIND$GIT_BEHIND%{${reset_color}%}" + fi + if [ "$GIT_STASHED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_STASHED%{${reset_color}%}" + fi + + GIT_PROMPT+="$ZSH_THEME_GIT_PROMPT_SEPARATOR" + if [ "$GIT_MODIFIED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_MODIFIED$GIT_MODIFIED%{${reset_color}%}" + fi + if [ "$GIT_DELETED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_DELETED$GIT_DELETED%{${reset_color}%}" + fi + if [ "$GIT_STAGED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_STAGED$GIT_STAGED%{${reset_color}%}" + fi + if [ "$GIT_UNTRACKED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_UNTRACKED$GIT_UNTRACKED%{${reset_color}%}" + fi + if [ "$GIT_CONFLICTS" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_CONFLICTS$GIT_CONFLICTS%{${reset_color}%}" + fi + + if [ "$GIT_MODIFIED" -eq "0" ] \ + && [ "$GIT_DELETED" -eq "0" ] \ + && [ "$GIT_STAGED" -eq "0" ] \ + && [ "$GIT_UNTRACKED" -eq "0" ] \ + && [ "$GIT_CONFLICTS" -eq "0" ] + then + GIT_PROMPT+="$STATUS_SEPARATOR$ZSH_THEME_GIT_PROMPT_CLEAN%{${reset_color}%}" + fi + + GIT_PROMPT+="%{${reset_color}%}$ZSH_THEME_GIT_PROMPT_SUFFIX%{${reset_color}%}" + echo "$GIT_PROMPT" +} + +function git_prompt_enhanced_status() { + if git rev-parse --git-dir &> /dev/null; then + update_git_branch_variables + update_git_status_varaibles + compose_git_prompt_string + fi +} + +# Default values for the appearance of the prompt. +ZSH_THEME_GIT_PROMPT_PREFIX="[" +ZSH_THEME_GIT_PROMPT_SUFFIX="]" +ZSH_THEME_GIT_PROMPT_SEPARATOR=" |" +ZSH_THEME_GIT_PROMPT_STATUS_SEPARATOR=" " + +ZSH_THEME_GIT_PROMPT_BRANCH="%{$fg_bold[magenta]%}" +ZSH_THEME_GIT_PROMPT_NO_UPSTREAM="%{$fg_bold[red]%}X" +ZSH_THEME_GIT_PROMPT_BEHIND="%{$fg[yellow]%}%{↓%G%}" +ZSH_THEME_GIT_PROMPT_AHEAD="%{$fg[cyan]%}%{↑%G%}" +ZSH_THEME_GIT_PROMPT_STASHED="%{$fg_bold[yellow]%}S" + +ZSH_THEME_GIT_PROMPT_MODIFIED="%{$fg[blue]%}%{+%G%}" +ZSH_THEME_GIT_PROMPT_DELETED="%{$fg[red]%}%{-%G%}" +ZSH_THEME_GIT_PROMPT_STAGED="%{$fg[yellow]%}%{s%G%}" +ZSH_THEME_GIT_PROMPT_UNTRACKED="%{$fg[cyan]%}%{u%G%}" +ZSH_THEME_GIT_PROMPT_CONFLICTS="%{$fg[red]%}%{!%G%}" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[green]%}%{%G%}" + +# Set the prompt. +RPROMPT='$(git_prompt_enhanced_status)' diff --git a/include/gitstatus.zsh b/include/gitstatus.zsh new file mode 100644 index 0000000..e571c6d --- /dev/null +++ b/include/gitstatus.zsh @@ -0,0 +1,126 @@ +# Handle $0 according to the standard: +# https://zdharma-continuum.github.io/Zsh-100-Commits-Club/Zsh-Plugin-Standard.html +0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" +0="${${(M)0:#/*}:-$PWD/$0}" + +function update_git_branch_variables() { + GIT_BRANCH="${$(git branch --show-current):-"$(git rev-parse --short HEAD)"}" + GIT_AHEAD=$(git rev-list --count @{upstream}..HEAD 2>/dev/null) + GIT_BEHIND=$(git rev-list --count HEAD..@{upstream} 2>/dev/null) + + GIT_STASHED=0 + if $(git rev-parse --verify refs/stash &>/dev/null); then + GIT_STASHED=1 + fi + + GIT_NO_UPSTREAM=1 + if $(git rev-parse --abbrev-ref --symbolic-full-name @{u} &>/dev/null); then + GIT_NO_UPSTREAM=0 + fi +} + +function update_git_status_varaibles() { + GIT_MODIFIED=0 + GIT_DELETED=0 + GIT_STAGED=0 + GIT_UNTRACKED=0 + GIT_CONFLICTS=0 + + GIT_STATUS=$(git status --porcelain -uall 2>/dev/null) + + while IFS= read -r line; do + if [[ "$line" =~ '^\?\?.*' ]]; then + GIT_UNTRACKED=$(($GIT_UNTRACKED+1)) + else + if [[ "$line" =~ ^U.* ]]; then + GIT_CONFLICTS=$(($GIT_CONFLICTS+1)) + elif [[ "$line" =~ ^[MRAD].* ]]; then + GIT_STAGED=$(($GIT_STAGED+1)) + fi + + if [[ "$line" =~ ^.M.* ]]; then + GIT_MODIFIED=$(($GIT_MODIFIED+1)) + elif [[ "$line" =~ ^.D.* ]]; then + GIT_DELETED=$(($GIT_DELETED+1)) + fi + fi + done <<< "$GIT_STATUS" +} + +function compose_git_prompt_string() { + SEPARATOR=$ZSH_THEME_GIT_PROMPT_STATUS_SEPARATOR + + GIT_PROMPT="$ZSH_THEME_GIT_PROMPT_PREFIX$ZSH_THEME_GIT_PROMPT_BRANCH$GIT_BRANCH%{${reset_color}%}" + + if [ "$GIT_NO_UPSTREAM" -eq "1" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_NO_UPSTREAM%{${reset_color}%}" + fi + if [ "$GIT_AHEAD" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_AHEAD$GIT_AHEAD%{${reset_color}%}" + fi + if [ "$GIT_BEHIND" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_BEHIND$GIT_BEHIND%{${reset_color}%}" + fi + if [ "$GIT_STASHED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_STASHED%{${reset_color}%}" + fi + + GIT_PROMPT+="$ZSH_THEME_GIT_PROMPT_SEPARATOR" + if [ "$GIT_MODIFIED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_MODIFIED$GIT_MODIFIED%{${reset_color}%}" + fi + if [ "$GIT_DELETED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_DELETED$GIT_DELETED%{${reset_color}%}" + fi + if [ "$GIT_STAGED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_STAGED$GIT_STAGED%{${reset_color}%}" + fi + if [ "$GIT_UNTRACKED" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_UNTRACKED$GIT_UNTRACKED%{${reset_color}%}" + fi + if [ "$GIT_CONFLICTS" -ne "0" ]; then + GIT_PROMPT+="$SEPARATOR$ZSH_THEME_GIT_PROMPT_CONFLICTS$GIT_CONFLICTS%{${reset_color}%}" + fi + + if [ "$GIT_MODIFIED" -eq "0" ] \ + && [ "$GIT_DELETED" -eq "0" ] \ + && [ "$GIT_STAGED" -eq "0" ] \ + && [ "$GIT_UNTRACKED" -eq "0" ] \ + && [ "$GIT_CONFLICTS" -eq "0" ] + then + GIT_PROMPT+="$STATUS_SEPARATOR$ZSH_THEME_GIT_PROMPT_CLEAN%{${reset_color}%}" + fi + + GIT_PROMPT+="%{${reset_color}%}$ZSH_THEME_GIT_PROMPT_SUFFIX%{${reset_color}%}" + echo "$GIT_PROMPT" +} + +function git_prompt_enhanced_status() { + if git rev-parse --git-dir &> /dev/null; then + update_git_branch_variables + update_git_status_varaibles + compose_git_prompt_string + fi +} + +# Default values for the appearance of the prompt. +ZSH_THEME_GIT_PROMPT_PREFIX="[" +ZSH_THEME_GIT_PROMPT_SUFFIX="]" +ZSH_THEME_GIT_PROMPT_SEPARATOR="" +ZSH_THEME_GIT_PROMPT_STATUS_SEPARATOR=" " + +ZSH_THEME_GIT_PROMPT_BRANCH="%B%F{cyan}" +ZSH_THEME_GIT_PROMPT_NO_UPSTREAM="(local)" +ZSH_THEME_GIT_PROMPT_BEHIND="%F{yellow}v" +ZSH_THEME_GIT_PROMPT_AHEAD="%F{magenta}^" +ZSH_THEME_GIT_PROMPT_STASHED="%F{yellow}&" + +ZSH_THEME_GIT_PROMPT_MODIFIED="%B%F{green}+" +ZSH_THEME_GIT_PROMPT_DELETED="%B%F{red}-" +ZSH_THEME_GIT_PROMPT_STAGED="%F{yellow}s" +ZSH_THEME_GIT_PROMPT_UNTRACKED="%F{cyan}u" +ZSH_THEME_GIT_PROMPT_CONFLICTS="%B%F{red}!" +ZSH_THEME_GIT_PROMPT_CLEAN="" + +# Set the prompt. +RPROMPT='$(git_prompt_enhanced_status)' diff --git a/include/plugins.zsh b/include/plugins.zsh new file mode 100644 index 0000000..b81330b --- /dev/null +++ b/include/plugins.zsh @@ -0,0 +1,16 @@ +if [[ -d $ZSHEXTRA/zsh-autosuggestions ]]; then + source $ZSHEXTRA/zsh-autosuggestions/zsh-autosuggestions.zsh + ZSH_AUTOSUGGEST_STRATEGY=(history completion) + set ZSH_AUTOSUGGEST_USE_ASYNC +fi + +if [[ -d $ZSHEXTRA/zsh-fast-syntax-highlighting ]]; then + source $ZSHEXTRA/zsh-fast-syntax-highlighting/fast-syntax-highlighting.plugin.zsh + fast-theme -q $ZSH/extra/zsh-fast-syntax-highlighting/themes/custom.ini +fi + +if [[ -d $ZSHEXTRA/zsh-history-substring-search ]]; then + source $ZSHEXTRA/zsh-history-substring-search/zsh-history-substring-search.zsh + bindkey '^[[A' history-substring-search-up + bindkey '^[[B' history-substring-search-down +fi diff --git a/include/tmuxvim.zsh b/include/tmuxvim.zsh new file mode 100644 index 0000000..86408f3 --- /dev/null +++ b/include/tmuxvim.zsh @@ -0,0 +1,45 @@ +#!/bin/zsh + +__tmux_get_vim_pane() { + local vim_pane + + tmux list-panes -F '#I #P [command:#{pane_current_command}] #{?pane_active,(active),}' | while read -r pane; do + if [[ "${pane#*(active)}" = "${pane}" ]] && [[ "${pane}" =~ "\[command:n?vim\]" ]]; then + vim_pane=$(echo "${pane}" | awk '{print $2}') + fi + done + + echo $vim_pane +} + +# Open a file in adjacent vim +# $1 is the vim command to use (tabedit, split, vsplit) +# $2 is the path of the file to open +__tmux_vim_open() { + local vim_command="$1" + local filepath=$(realpath "$2") + local vim_pane=$(__tmux_get_vim_pane) + + if [ -z $vim_pane ]; then + echo "error: Vim pane not found" + return + fi + + tmux send-keys -t $vim_pane Escape ":${vim_command} $(printf "%q" "${filepath}")" Enter + tmux select-pane -t $vim_pane +} + +# Make available if inside a Tmux session +if [ -n "${TMUX}" ]; then + tmux-vim-tabedit() { + __tmux_vim_open "tabedit" "$1" + } + + tmux-vim-split() { + __tmux_vim_open "split" "$1" + } + + tmux-vim-vsplit() { + __tmux_vim_open "vsplit" "$1" + } +fi diff --git a/include/zsh-vi-mode.zsh b/include/zsh-vi-mode.zsh new file mode 100644 index 0000000..993a33b --- /dev/null +++ b/include/zsh-vi-mode.zsh @@ -0,0 +1,3459 @@ +# zsh-vi-mode.zsh -- A better and friendly vi(vim) mode for Zsh +# https://github.com/jeffreytse/zsh-vi-mode +# +# Copyright (c) 2020 Jeffrey Tse +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# All Settings +# Some of these variables should be set before sourcing this file. +# +# ZVM_CONFIG_FUNC +# the config function (default is `zvm_config`), if this config function +# exists, it will be called automatically, you can do some configurations +# in this aspect before you source this plugin. +# +# For example: +# +# ```zsh +# function zvm_config() { +# ZVM_LINE_INIT_MODE=$ZVM_MODE_INSERT +# ZVM_VI_INSERT_ESCAPE_BINDKEY=jk +# } +# +# source ~/zsh-vi-mode.zsh +# ``` +# +# ZVM_INIT_MODE +# the plugin initial mode (default is doing the initialization when the first +# new command line is starting. For doing the initialization instantly, you +# can set it to `sourcing`. +# +# ZVM_VI_ESCAPE_BINDKEY +# the vi escape key for all modes (default is ^[ => ), you can set it +# to whatever you like, such as `jj`, `jk` and so on. +# +# ZVM_VI_INSERT_ESCAPE_BINDKEY +# the vi escape key of insert mode (default is $ZVM_VI_ESCAPE_BINDKEY), you +# can set it to whatever, such as `jj`, `jk` and so on. +# +# ZVM_VI_VISUAL_ESCAPE_BINDKEY +# the vi escape key of visual mode (default is $ZVM_VI_ESCAPE_BINDKEY), you +# can set it to whatever, such as `jj`, `jk` and so on. +# +# ZVM_VI_OPPEND_ESCAPE_BINDKEY +# the vi escape key of operator pendding mode (default is +# $ZVM_VI_ESCAPE_BINDKEY), you can set it to whatever, such as `jj`, `jk` +# and so on. +# +# ZVM_VI_INSERT_MODE_LEGACY_UNDO: +# using legacy undo behavior in vi insert mode +# +# 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=#b8d68c +# ZVM_VI_HIGHLIGHT_BACKGROUND=red # Color name +ZVM_VI_HIGHLIGHT_BACKGROUND=#e84f4f +# 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=false +# 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) +# + +# 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.8.5' + +# Plugin initial status +ZVM_INIT_DONE=false + +# Postpone reset prompt (i.e. postpone the widget `reset-prompt`) +# empty (No postponing) +# true (Enter postponing) +# false (Trigger reset prompt) +ZVM_POSTPONE_RESET_PROMPT= + +# 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='i' + +# 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) + +########################################## +# Initial all default settings + +# Default config function +: ${ZVM_CONFIG_FUNC:='zvm_config'} + +# Load config by calling the config function +if command -v "$ZVM_CONFIG_FUNC" >/dev/null; then + $ZVM_CONFIG_FUNC +fi + +# Set the readkey engine (default is NEX engine) +: ${ZVM_READKEY_ENGINE:=$ZVM_READKEY_ENGINE_DEFAULT} + +# Set key input timeout (default is 0.4 seconds) +: ${ZVM_KEYTIMEOUT:=0.4} + +# Set the escape key timeout (default is 0.03 seconds) +: ${ZVM_ESCAPE_KEYTIMEOUT:=0.03} + +# Set keybindings mode (default is true) +# The lazy keybindings will post the keybindings of vicmd and visual +# keymaps to the first time entering the normal mode +: ${ZVM_LAZY_KEYBINDINGS:=true} + +# All keybindings for lazy loading +if $ZVM_LAZY_KEYBINDINGS; then + ZVM_LAZY_KEYBINDINGS_LIST=() +fi + +# Set the cursor style in defferent vi modes, the value you could use +# the predefined value, such as $ZVM_CURSOR_BLOCK, $ZVM_CURSOR_BEAM, +# $ZVM_CURSOR_BLINKING_BLOCK and so on. +: ${ZVM_INSERT_MODE_CURSOR:=$ZVM_CURSOR_BEAM} +: ${ZVM_NORMAL_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} +: ${ZVM_VISUAL_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} +: ${ZVM_VISUAL_LINE_MODE_CURSOR:=$ZVM_CURSOR_BLOCK} + +# Operator pending mode cursor style (default is underscore) +: ${ZVM_OPPEND_MODE_CURSOR:=$ZVM_CURSOR_UNDERLINE} + +# Set the vi escape key (default is ^[ => ) +: ${ZVM_VI_ESCAPE_BINDKEY:=^[} +: ${ZVM_VI_INSERT_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} +: ${ZVM_VI_VISUAL_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} +: ${ZVM_VI_OPPEND_ESCAPE_BINDKEY:=$ZVM_VI_ESCAPE_BINDKEY} + +# Set the line init mode (empty will keep the last mode) +# you can also set it to others, such as $ZVM_MODE_INSERT. +: ${ZVM_LINE_INIT_MODE:=$ZVM_MODE_LAST} + +: ${ZVM_VI_INSERT_MODE_LEGACY_UNDO:=false} +: ${ZVM_VI_SURROUND_BINDKEY:=classic} +: ${ZVM_VI_HIGHLIGHT_BACKGROUND:=#cc0000} +: ${ZVM_VI_HIGHLIGHT_FOREGROUND:=#eeeeee} +: ${ZVM_VI_HIGHLIGHT_EXTRASTYLE:=default} +: ${ZVM_VI_EDITOR:=${EDITOR:-vim}} +: ${ZVM_TMPDIR:=${TMPDIR:-/tmp}} + +# Set the term for handling terminal sequences, it's important for some +# terminal emulators to show cursor properly (default is $TERM) +: ${ZVM_TERM:=${TERM:-xterm-256color}} + +# Enable the cursor style feature +: ${ZVM_CURSOR_STYLE_ENABLED:=true} + +# 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 +) + +# Display version information +function zvm_version() { + echo -e "$ZVM_NAME $ZVM_VERSION" + 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 -i retval + $func "${@:3}" + 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" + eval "$wrapper() { zvm_widget_wrapper $rawfunc $func \"\$@\" }" + 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= + + # 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 + 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) + for ((i=$#result;i>=0;i--)); do + + # Backward find the first whitespace character + [[ "${result:$i:1}" == ' ' ]] || continue + + # Retrieve the keys and widget + local k=${result:1:$i-2} + + # Escape spaces in key bindings (space -> $ZVM_ESCAPE_SPACE) + k=${k// /$ZVM_ESCAPE_SPACE} + retval+=($k ${result:$i+1}) + + break + done + fi +} + +# Read keys for retrieving widget +function zvm_readkeys() { + local keymap=$1 + local key=${2:-$(zvm_keys)} + local keys= + local widget= + local result= + local pattern= + local timeout= + + while :; do + # Keep reading key for escape character + if [[ "$key" == $'\e' ]]; then + while :; do + local k= + read -t $ZVM_ESCAPE_KEYTIMEOUT -k 1 k || break + key="${key}${k}" + done + fi + + keys="${keys}${key}" + + # Handle the pattern + if [[ -n "$key" ]]; then + # Transform the non-printed characters + local k=$(zvm_escape_non_printed_characters "${key}") + + # Escape keys + # " -> \" It's a special character in bash syntax + # ` -> \` It's a special character in bash syntax + # -> ` ` It's a special character in bash syntax + k=${k//\"/\\\"} + k=${k//\`/\\\`} + k=${k//$ZVM_ESCAPE_SPACE/ } + + pattern="${pattern}${k}" + fi + + # Find out widgets that match this key pattern + zvm_find_bindkey_widget $keymap "$pattern" true + result=(${retval[@]}) + + # Exit key input if there is only one widget matched + # or no more widget matched. + case ${#result[@]} in + 2) key=; widget=${result[2]}; break;; + 0) break;; + esac + + # Evaluate the readkey timeout + # Special timeout for the escape sequence + if [[ "${keys}" == $'\e' ]]; then + timeout=$ZVM_ESCAPE_KEYTIMEOUT + # Check if there is any one custom escape sequence + 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} + 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= + 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= + 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 +} + +# 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 +} + +# 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 +} + +# 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 == '' ]]; then + key= + fi + + cache=(${cache[@]:0:-1}) + BUFFER[$cursor+1]=$key + + # Remove from commands + cmds=(${cmds[@]:0:-1}) + fi + else + # If the key or the character at cursor is a newline character, + # or the cursor is at the end of buffer, we should insert the + # key instead of replacing with the key. + if [[ $key == $'\n' || + $BUFFER[$cursor+1] == $'\n' || + $BUFFER[$cursor+1] == '' + ]]; then + cache+=('') + LBUFFER+=$key + else + cache+=(${BUFFER[$cursor+1]}) + BUFFER[$cursor+1]=$key + fi + + cursor=$((cursor+1)) + + # Push to commands + cmds+=($key) + fi + + # Update next cursor position + CURSOR=$cursor + + zle redisplay + done + + # The cursor position should go back one character after + # exiting the replace mode + zle vi-backward-char + + zvm_select_vi_mode $ZVM_MODE_NORMAL + zvm_reset_repeat_commands $ZVM_MODE R $cmds + elif [[ $ZVM_MODE == $ZVM_MODE_VISUAL ]]; then + zvm_enter_visual_mode V + zvm_vi_change + elif [[ $ZVM_MODE == $ZVM_MODE_VISUAL_LINE ]]; then + zvm_vi_change + fi +} + +# Replace characters in one time +function zvm_vi_replace_chars() { + local cmds=() + local key= + + # Read a character for replacing + zvm_enter_oppend_mode + + # Redisplay the command line, this is to be called from within + # a user-defined widget to allow changes to become visible + zle redisplay + zle -R + + read -k 1 key + + zvm_exit_oppend_mode + + # Escape key will break the replacing process + case $(zvm_escape_non_printed_characters $key) in + $ZVM_VI_OPPEND_ESCAPE_BINDKEY) + zvm_exit_visual_mode + return + esac + + if [[ $ZVM_MODE == $ZVM_MODE_NORMAL ]]; then + cmds+=($key) + BUFFER[$CURSOR+1]=$key + else + local ret=($(zvm_calc_selection)) + local bpos=${ret[1]} epos=${ret[2]} + for ((bpos=bpos+1; bpos<=epos; bpos++)); do + # Newline character is no need to be replaced + if [[ $BUFFER[$bpos] == $'\n' ]]; then + cmds+=($'\n') + continue + fi + + cmds+=($key) + BUFFER[$bpos]=$key + done + zvm_exit_visual_mode + fi + + # Reset the repeat commands + zvm_reset_repeat_commands $ZVM_MODE r $cmds +} + +# Substitute characters of selection +function zvm_vi_substitute() { + # Substitute one character in normal mode + if [[ $ZVM_MODE == $ZVM_MODE_NORMAL ]]; then + BUFFER="${BUFFER:0:$CURSOR}${BUFFER:$((CURSOR+1))}" + zvm_reset_repeat_commands $ZVM_MODE c 0 1 + zvm_select_vi_mode $ZVM_MODE_INSERT + else + zvm_vi_change + fi +} + +# Substitute all characters of a line +function zvm_vi_substitute_whole_line() { + zvm_select_vi_mode $ZVM_MODE_VISUAL_LINE false + zvm_vi_substitute +} + +# Check if cursor is at an empty line +function zvm_is_empty_line() { + local cursor=${1:-$CURSOR} + if [[ ${BUFFER:$cursor:1} == $'\n' && + ${BUFFER:$((cursor-1)):1} == $'\n' ]]; then + return + fi + return 1 +} + +# Get the beginning and end position of selection +function zvm_selection() { + local bpos= epos= + if (( MARK > CURSOR )) ; then + bpos=$CURSOR epos=$((MARK+1)) + else + bpos=$MARK epos=$((CURSOR+1)) + fi + echo $bpos $epos +} + +# Calculate the region of selection +function zvm_calc_selection() { + local ret=($(zvm_selection)) + local bpos=${ret[1]} epos=${ret[2]} cpos= + + # Save the current cursor position + cpos=$bpos + + # Check if it is visual-line mode + if [[ "${1:-$ZVM_MODE}" == $ZVM_MODE_VISUAL_LINE ]]; then + + # Extend the selection to whole line + for ((bpos=$bpos-1; $bpos>0; bpos--)); do + if [[ "${BUFFER:$bpos:1}" == $'\n' ]]; then + bpos=$((bpos+1)) + break + fi + done + for ((epos=$epos-1; $epos<$#BUFFER; epos++)); do + if [[ "${BUFFER:$epos:1}" == $'\n' ]]; then + break + fi + done + + # The begin position must not be less than zero + if (( bpos < 0 )); then + bpos=0 + fi + + ########################################### + # Calculate the new cursor position, here we consider that + # the selection will be delected. + + # Calculate the indent of current cursor line + for ((cpos=$((CURSOR-1)); $cpos>=0; cpos--)); do + [[ "${BUFFER:$cpos:1}" == $'\n' ]] && break + done + + local indent=$((CURSOR-cpos-1)) + + # If the selection includes the last line, the cursor + # will move up to above line. Otherwise the cursor will + # keep in the same line. + + local hpos= # Line head position + local rpos= # Reference position + + if (( $epos < $#BUFFER )); then + # Get the head position of next line + hpos=$((epos+1)) + rpos=$bpos + else + # Get the head position of above line + for ((hpos=$((bpos-2)); $hpos>0; hpos--)); do + if [[ "${BUFFER:$hpos:1}" == $'\n' ]]; then + break + fi + done + if (( $hpos < -1 )); then + hpos=-1 + fi + hpos=$((hpos+1)) + rpos=$hpos + fi + + # Calculate the cursor 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 +} + +# 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))} + 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 +} + +# Put cutbuffer after the cursor +function zvm_vi_put_after() { + 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 + + # Special handling if cursor at an empty line + if zvm_is_empty_line; then + head=${BUFFER:0:$pos} + foot=${BUFFER:$pos} + else + head=${BUFFER:0:$pos} + foot=${BUFFER:$pos} + if [[ $pos == $#BUFFER ]]; then + content=$'\n'${content:0:-1} + pos=$pos+1 + fi + fi + + offset=0 + BUFFER="${head}${content}${foot}" + CURSOR=$pos + else + # 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 + + BUFFER="${head}${BUFFER:$CURSOR:1}${content}${foot}" + CURSOR=$CURSOR+$#content + fi + + # Reresh display and highlight buffer + zvm_highlight clear + zvm_highlight custom $(($#head+$offset)) $(($#head+$#content+$offset)) +} + +# Put cutbuffer before the cursor +function zvm_vi_put_before() { + 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 + + BUFFER="${head}${content}${foot}" + CURSOR=$pos + else + head="${BUFFER:0:$CURSOR}" + foot="${BUFFER:$((CURSOR+1))}" + BUFFER="${head}${content}${BUFFER:$CURSOR:1}${foot}" + CURSOR=$CURSOR+$#content + CURSOR=$((CURSOR-1)) + fi + + # Reresh display and highlight buffer + zvm_highlight clear + zvm_highlight custom $#head $(($#head+$#content)) +} + +# Delete characters of the visual selection +function zvm_vi_delete() { + local ret=($(zvm_calc_selection)) + local bpos=$ret[1] epos=$ret[2] cpos=$ret[3] + + 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}${BUFFER:$epos}" + CURSOR=$cpos + + 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 + + # 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 +} + +# 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_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 + ZVM_KEYS=${extra_keys} + return + ;; + [vV]'^['|[vV]$ZVM_VI_VISUAL_ESCAPE_BINDKEY) + zvm_exit_visual_mode + ZVM_KEYS=${extra_keys} + return + ;; + esac + + case "$KEYMAP" in + vicmd) + case "$keys" in + [vV]c) zvm_vi_change;; + [vV]d) zvm_vi_delete;; + [vV]y) zvm_vi_yank;; + [vV]S) zvm_change_surround S;; + [cdyvV]*) zvm_range_handler "${keys}${extra_keys}";; + *) + 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 + 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 + zle zvm_default_handler "$key" + + # Push back to the key input stack + if [[ -n "$ZVM_KEYS" ]]; then + zle -U "$ZVM_KEYS" + 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 + + cmd=(zvm_find_and_move_cursor ${keys[-1]} $count $forward $skip) + count=1 + 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 + + # Call the widget, 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 + for ((c=0; c `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]*)?w$' ]]; then + zle vi-backward-char + count=${match[1]:-1} + navkey='e' + elif [[ $keys =~ '^c([1-9][0-9]*)?W$' ]]; then + zle vi-backward-blank-char + count=${match[1]:-1} + navkey='E' + elif [[ $keys =~ '^c([1-9][0-9]*)?e$' ]]; then + count=${match[1]:-1} + navkey='e' + elif [[ $keys =~ '^c([1-9][0-9]*)?E$' ]]; then + count=${match[1]:-1} + navkey='E' + 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} + 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} + for ((i=$((CURSOR+1)); i>0; i--)); do + [[ ${BUFFER[$i]} == $'\n' ]] && navkey='k' + done + elif [[ $keys =~ '^[cdy]([1-9][0-9]*)?h$' ]]; then + MARK=$((MARK-1)) + count=${match[1]:-1} + navkey='h' + + # 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 =~ '^.([1-9][0-9]*)?([^0-9]+)$' ]]; then + count=${match[1]:-1} + navkey=${match[2]} + else + navkey= + fi + + # Handle navigation + case $navkey in + '') exit_code=1;; + *[ia][wW]) + local widget= + local mark= + + # At least 1 time + if [[ -z $count ]]; then + count=1 + fi + + # Retrieve the widget + case ${navkey: -2} in + iw) widget=select-in-word;; + aw) widget=select-a-word;; + iW) widget=select-in-blank-word;; + aW) widget=select-a-blank-word;; + esac + + # Execute the widget for `count` times, and + # save the `mark` position of the first time + for ((c=0; c= $#BUFFER)); then + break + fi + done + + MARK=$mark + CURSOR=$((CURSOR-1)) + ;; + *) + 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=1 + fi + + BUFFER[-1]='' + ;; + esac + + # Check if there is no range selected + if [[ $exit_code != 0 ]]; then + zvm_exit_visual_mode + return + 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; cursor=;; + d*) zvm_vi_delete; cursor=;; + y*) zvm_vi_yank;; + [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 +} + +# 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 =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 + 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='>';; + 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=${ret[1]} + local surround=${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 current mode for prompt redraw + zle reset-prompt +} + +# Change surround in vicmd or visual mode +function zvm_change_surround() { + local ret=($(zvm_parse_surround_keys)) + local action=${1:-${ret[1]}} + local surround=${2:-${ret[2]//$ZVM_ESCAPE_SPACE/ }} + local bpos=${3} epos=${4} + local is_appending=false + case $action in + S|y|a) is_appending=true;; + esac + if $is_appending; then + if [[ -z $bpos && -z $epos ]]; then + ret=($(zvm_selection)) + bpos=${ret[1]} epos=${ret[2]} + fi + else + ret=($(zvm_search_surround "$surround")) + (( ${#ret[@]} )) || return + bpos=${ret[1]} epos=${ret[2]} + zvm_highlight custom $bpos $(($bpos+1)) + zvm_highlight custom $epos $(($epos+1)) + fi + local key= + case $action in + c|r) + zvm_enter_oppend_mode + read -k 1 key + zvm_exit_oppend_mode + ;; + S|y|a) + if [[ -z $surround ]]; then + zvm_enter_oppend_mode + read -k 1 key + zvm_exit_oppend_mode + else + key=$surround + fi + if [[ $ZVM_MODE == $ZVM_MODE_VISUAL ]]; then + zle visual-mode + fi + ;; + esac + + # Check if it is ESCAPE key ( or ZVM_VI_ESCAPE_BINDKEY) + case "$key" in + $'\e'|"${ZVM_VI_ESCAPE_BINDKEY//\^\[/$'\e'}") + zvm_highlight clear + return + esac + + # Start changing surround + ret=($(zvm_match_surround "$key")) + local bchar=${${ret[1]//$ZVM_ESCAPE_SPACE/ }:-$key} + local echar=${${ret[2]//$ZVM_ESCAPE_SPACE/ }:-$key} + local value=$($is_appending && echo 0 || echo 1 ) + local head=${BUFFER:0:$bpos} + local body=${BUFFER:$((bpos+value)):$((epos-(bpos+value)))} + local foot=${BUFFER:$((epos+value))} + BUFFER="${head}${bchar}${body}${echar}${foot}" + + # Clear highliht + zvm_highlight clear + + case $action in + S|y|a) zvm_select_vi_mode $ZVM_MODE_NORMAL;; + esac +} + +# Change surround text object +function zvm_change_surround_text_object() { + local ret=($(zvm_parse_surround_keys)) + local action=${ret[1]} + local surround=${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() { + ZVM_REPEAT_MODE=true + + local cmd=${ZVM_REPEAT_COMMANDS[2]} + + # Handle repeat command + 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 + + zle redisplay + + 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 + for ((i=1; i<=${#cmds[@]}; i++)); do + cmd="${cmds[$i]}" + + # Hanlde 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 + LBUFFER+=$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 + + # 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 + + 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)) + + 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 + + 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 + if (( $#result == $#word )); then + result=${months[i]} + else + result=${months[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 +} + +# 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=() + 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 +} + +# 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_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 + CURSOR=$((CURSOR+1)) + 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 + mode= + 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 + + if $toggle; then + ZVM_POSTPONE_RESET_PROMPT=true + else + if [[ $ZVM_POSTPONE_RESET_PROMPT == false || $force ]]; then + ZVM_POSTPONE_RESET_PROMPT= + zle reset-prompt + else + ZVM_POSTPONE_RESET_PROMPT= + fi + fi +} + +# Reset prompt +function zvm_reset_prompt() { + # Return if postponing is enabled + if [[ -n $ZVM_POSTPONE_RESET_PROMPT ]]; then + ZVM_POSTPONE_RESET_PROMPT=false + return + fi + + local -i retval + if [[ -z "$rawfunc" ]]; then + zle .reset-prompt -- "$@" + else + $rawfunc -- "$@" + 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_INS_LEGACY_UNDO ]]; then + zvm_kill_line + else + zvm_backward_kill_line + fi +} + +function zvm_set_cursor() { + # Term of vim isn't supported + if [[ -n $VIMRUNTIME ]]; 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 + # and st implement a superset of VT100 and VT100, they support + # 256 colors the same way xterm does. + xterm*|rxvt*|screen*|tmux*|konsole*|alacritty*|st*) + 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_OPPEND) old_style=$ZVM_OPPEND_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 +} + +# 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 +} + +# 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_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 + + # 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 'y' zvm_vi_yank + zvm_bindkey vicmd 'p' zvm_vi_put_after + zvm_bindkey vicmd 'P' zvm_vi_put_before + 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 + + 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 + + # Append for escaping visual mode + if $is_custom_escape_key; then + surrounds+=("$ZVM_VI_ESCAPE_BINDKEY") + fi + + # 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 + + # 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 +} + +# Initialize this plugin according to the mode +case $ZVM_INIT_MODE in + sourcing) zvm_init;; + *) precmd_functions+=(zvm_init);; +esac + diff --git a/include/zshmux.zsh b/include/zshmux.zsh new file mode 100644 index 0000000..f82c848 --- /dev/null +++ b/include/zshmux.zsh @@ -0,0 +1,40 @@ +#!/bin/zsh + +# start tmux server if it's not already running +[[ ! $(pgrep tmux) ]] && tmux start + +# exit if already in a tmux session +[[ -n $TMUX ]] && return + +# check if any sessions exit +if [[ $(tmux list-sessions | wc -l) -ne 0 ]]; then + if [[ $(tmux list-sessions -F"#{?session_attached,,x}" | grep "x" | wc -l) -eq 1 ]] && [[ -z $SSH_CLIENT ]]; then + tmux attach-session -t $(tmux list-sessions -F"#{?session_attached,,#S}" ) + return + else + printf '\033[32;1mCurrent \033[33;1mtmux \033[32;1msessions\033[0m:\n' + tmux list-sessions -F"#S - (#{?session_attached,Attached,Not Attached})" + printf '\033[34;1mAttach session?\033[0;m: [\033[35;1mname\033[0m] \033[37;2m(empty if none)\033[0m: ' + fi +else + printf '\033[34;1mNo \033[33;1mtmux \033[34;1msessions active\n' + printf '\033[34;1mCreate new session?\033[0;m: [\033[35;1mname\033[0m] \033[37;2m(empty if none)\033[0m: ' +fi + +read session + +# start requested session - since it uses grep you just have to put enough of a unique identifier to select +if [[ -n $session ]]; then + if tmux list-sessions -F"#S" | grep -q "$session"; then + tmux attach-session -t $session + return + elif [[ $session == new ]]; then + tmux new-session + return + else + tmux new-session -s $session + return + fi +fi + +return -- 2.45.2