zsh/.zsh/plugins/autopairs/autopair.zsh
2024-12-11 15:02:36 +01:00

227 lines
6.4 KiB
Bash

#!/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-)"}
AUTOPAIR_DELWORD_WIDGET=${AUTOPAIR_DELWORD_WIDGET:-"$(bindkey "^w" | 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}
}
autopair-delete-word() {
_ap-can-delete-p && RBUFFER=${RBUFFER:1}
zle ${AUTOPAIR_DELWORD_WIDGET:-backward-delete-word}
}
### Initialization #####################
autopair-init() {
zle -N autopair-insert
zle -N autopair-close
zle -N autopair-delete
zle -N autopair-delete-word
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 "^w" autopair-delete-word
bindkey -M isearch "^?" backward-delete-char
bindkey -M isearch "^h" backward-delete-char
bindkey -M isearch "^w" backward-delete-word
}
[[ -n $AUTOPAIR_INHIBIT_INIT ]] || autopair-init