├── .editorconfig ├── .travis.yml ├── .zunit.yml ├── LICENSE ├── README.md ├── autopair.plugin.zsh ├── autopair.zsh ├── tests ├── _support │ └── bootstrap ├── balanced-p.zunit ├── can-delete-p.zunit ├── can-pair-p.zunit ├── can-skip-p.zunit └── get-pair.zunit └── zsh-autopair.plugin.zsh /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.md] 10 | indent_style = space 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | language: generic 5 | 6 | env: 7 | - ZSH_VERSION=5.3.1 URL=https://downloads.sourceforge.net/project/zsh/zsh/5.3.1/zsh-5.3.1.tar.xz 8 | - ZSH_VERSION=5.2 URL=https://downloads.sourceforge.net/project/zsh/zsh/5.2/zsh-5.2.tar.xz 9 | - ZSH_VERSION=5.1.1 URL=https://downloads.sourceforge.net/project/zsh/zsh/5.1.1/zsh-5.1.1.tar.xz 10 | - ZSH_VERSION=5.0.8 URL=https://downloads.sourceforge.net/project/zsh/zsh/5.0.8/zsh-5.0.8.tar.gz 11 | 12 | addons: 13 | apt: 14 | packages: 15 | - build-essential 16 | 17 | before_install: 18 | - export LOCAL="$(mktemp --directory --tmpdir=${TMPDIR:/tmp} local.bin.XXXXXX)" 19 | - wget $URL 20 | - tar -xf zsh-$ZSH_VERSION.tar.* 21 | - cd zsh-$ZSH_VERSION 22 | - ./configure --prefix=$LOCAL 23 | - make 24 | - make install 25 | - cd - 26 | - export PATH="$LOCAL/bin:$HOME/bin:$PATH" 27 | 28 | before_script: 29 | - mkdir -p ~/bin 30 | - curl -L https://github.com/zunit-zsh/zunit/releases/download/v0.8.1/zunit > ~/bin/zunit 31 | - curl -L https://raw.githubusercontent.com/molovo/revolver/master/revolver > ~/bin/revolver 32 | - chmod u+x ~/bin/{revolver,zunit} 33 | 34 | script: 35 | - zunit --verbose 36 | -------------------------------------------------------------------------------- /.zunit.yml: -------------------------------------------------------------------------------- 1 | tap: false 2 | directories: 3 | tests: tests 4 | output: tests/_output 5 | support: tests/_support 6 | time_limit: 0 7 | fail_fast: false 8 | allow_risky: false 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-24 Henrik Lissner. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Made with Doom Emacs](https://img.shields.io/badge/Made_with-Doom_Emacs-blueviolet.svg?style=flat-square&logo=GNU%20Emacs&logoColor=white)](https://github.com/hlissner/doom-emacs) 2 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](./LICENSE) 3 | ![ZSH 5.0.2+](https://img.shields.io/badge/zsh-v5.0.2-orange.svg?style=flat-square) 4 | [![Build Status](https://img.shields.io/travis/hlissner/zsh-autopair/master.svg?label=master&style=flat-square)](https://travis-ci.org/hlissner/zsh-autopair) 5 | 6 | # zsh-autopair 7 | A simple plugin that auto-closes, deletes and skips over matching delimiters in 8 | zsh intelligently. Hopefully. 9 | 10 | > NOTE: zsh-autopair is untested for versions of Zsh below 5.0.2. Please report 11 | > any issues you have in earlier versions! 12 | 13 | Specifically, zsh-autopair does 5 things for you: 14 | 15 | 1. It inserts matching pairs (by default, that means brackets, quotes and 16 | spaces): 17 | 18 | e.g. `echo |` => " => `echo "|"` 19 | 20 | 2. It skips over matched pairs: 21 | 22 | e.g. `cat ./*.{py,rb|}` => } => `cat ./*.{py,rb}|` 23 | 24 | 3. It auto-deletes pairs on backspace: 25 | 26 | e.g. `git commit -m "|"` => backspace => `git commit -m |` 27 | 28 | 4. And does all of the above only when it makes sense to do so. e.g. when the 29 | pair is balanced and when the cursor isn't next to a boundary character: 30 | 31 | e.g. `echo "|""` => backspace => `echo |""` (doesn't aggressively eat up too many quotes) 32 | 33 | 5. Spaces between brackets are expanded and contracted. 34 | 35 | e.g. `echo [|]` => space => `echo [ | ]` => backspace => `echo [|]` 36 | 37 | 38 | 39 | **Table of Contents** 40 | 41 | - [Install](#install) 42 | - [Antigen](#antigen) 43 | - [zgen](#zgen) 44 | - [zplug](#zplug) 45 | - [Hombrew](#homebrew) 46 | - [Oh My Zsh](#oh-my-zsh) 47 | - [Configuration](#configuration) 48 | - [Adding/Removing pairs](#addingremoving-pairs) 49 | - [Troubleshooting & compatibility issues](#troubleshooting--compatibility-issues) 50 | - [zgen & prezto compatibility](#zgen--prezto-compatibility) 51 | - [text on right-side of cursor interfere with completion](#text-on-right-side-of-cursor-interfere-with-completion) 52 | - [zsh-autopair & isearch?](#zsh-autopair--isearch) 53 | - [Midnight Commander](#midnight-commander) 54 | - [Other resources](#other-resources) 55 | 56 | 57 | 58 | ## Install 59 | Download and source `autopair.zsh` 60 | 61 | ```bash 62 | if [[ ! -d ~/.zsh-autopair ]]; then 63 | git clone https://github.com/hlissner/zsh-autopair ~/.zsh-autopair 64 | fi 65 | 66 | source ~/.zsh-autopair/autopair.zsh 67 | autopair-init 68 | ``` 69 | 70 | ### Hoembrew 71 | `brew install zsh-autopair` 72 | 73 | ``` bash 74 | # Add to .zshrc 75 | source $HOMEBREW_PREFIX/share/zsh-autopair/autopair.zsh 76 | ``` 77 | 78 | ### Antigen 79 | `antigen bundle hlissner/zsh-autopair` 80 | 81 | ### zgen 82 | ```bash 83 | if ! zgen saved; then 84 | echo "Creating a zgen save" 85 | 86 | # ... other plugins 87 | zgen load hlissner/zsh-autopair 88 | 89 | zgen save 90 | fi 91 | ``` 92 | 93 | ### zplug 94 | Load autopair _after compinit_, otherwise, the plugin won't work. 95 | ```bash 96 | zplug "hlissner/zsh-autopair", defer:2 97 | ``` 98 | 99 | ### Homebrew 100 | For Homebrew users, you can install it through the following command 101 | ```shell 102 | brew install zsh-autopair 103 | ``` 104 | Then source it in your `.zshrc` 105 | ```shell 106 | source $(brew --prefix)/share/zsh-autopair/autopair.zsh 107 | ``` 108 | 109 | ### Oh My Zsh 110 | 1. Clone this repository into `$ZSH_CUSTOM/plugins` (by default `~/.oh-my-zsh/custom/plugins`) 111 | 112 | ```sh 113 | git clone https://github.com/hlissner/zsh-autopair ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autopair 114 | ``` 115 | 116 | 2. Add the plugin to the list of plugins for Oh My Zsh to load (inside `~/.zshrc`): 117 | 118 | ```sh 119 | plugins=( 120 | # other plugins... 121 | zsh-autopair 122 | ) 123 | ``` 124 | 125 | 3. Start a new terminal session. 126 | 127 | ## Configuration 128 | zsh-autopair sets itself up. You can prevent this by setting 129 | `AUTOPAIR_INHIBIT_INIT`. 130 | 131 | **Options:** 132 | * `AUTOPAIR_BETWEEN_WHITESPACE` (default: blank): if set, regardless of whether 133 | delimiters are unbalanced or do not meet a boundary check, pairs will be 134 | auto-closed if surrounded by whitespace, BOL or EOL. 135 | * `AUTOPAIR_INHIBIT_INIT` (default: blank): if set, autopair will not 136 | automatically set up keybinds. [Check out the initialization 137 | code](autopair.zsh#L118) if you want to know what it does. 138 | * `AUTOPAIR_PAIRS` (default: ``('`' '`' "'" "'" '"' '"' '{' '}' '[' ']' '(' ')' 139 | ' ' ' ')``): An associative array that map pairs. Only one-character pairs are 140 | supported. To modify this, see the "Adding/Removing pairs" section. 141 | * `AUTOPAIR_LBOUNDS`/`AUTOPAIR_RBOUNDS` (default: see below): Associative lists 142 | of regex character groups dictating the 'boundaries' for autopairing depending 143 | on the delimiter. These are their default values: 144 | 145 | ```bash 146 | AUTOPAIR_LBOUNDS=(all '[.:/\!]') 147 | AUTOPAIR_LBOUNDS+=(quotes '[]})a-zA-Z0-9]') 148 | AUTOPAIR_LBOUNDS+=(spaces '[^{([]') 149 | AUTOPAIR_LBOUNDS+=(braces '') 150 | AUTOPAIR_LBOUNDS+=('`' '`') 151 | AUTOPAIR_LBOUNDS+=('"' '"') 152 | AUTOPAIR_LBOUNDS+=("'" "'") 153 | 154 | AUTOPAIR_RBOUNDS=(all '[[{(<,.:?/%$!a-zA-Z0-9]') 155 | AUTOPAIR_RBOUNDS+=(quotes '[a-zA-Z0-9]') 156 | AUTOPAIR_RBOUNDS+=(spaces '[^]})]') 157 | AUTOPAIR_RBOUNDS+=(braces '') 158 | ``` 159 | 160 | For example, if `$AUTOPAIR_LBOUNDS[braces]="[a-zA-Z]"`, then braces (`{([`) won't be 161 | autopaired if the cursor follows an alphabetical character. 162 | 163 | Individual delimiters can be used too. Setting `$AUTOPAIR_RBOUNDS['{']="[0-9]"` will 164 | cause { specifically to not be autopaired when the cursor precedes a number. 165 | 166 | ### Adding/Removing pairs 167 | You can change the designated pairs in zsh-autopair by modifying the 168 | `AUTOPAIR_PAIRS` envvar. This can be done _before_ initialization like so: 169 | 170 | ``` sh 171 | typeset -gA AUTOPAIR_PAIRS 172 | AUTOPAIR_PAIRS+=("<" ">") 173 | ``` 174 | 175 | Or after initialization; however, you'll have to bind keys to `autopair-insert` 176 | manually: 177 | 178 | ```sh 179 | AUTOPAIR_PAIRS+=("<" ">") 180 | bindkey "<" autopair-insert 181 | # prevents breakage in isearch 182 | bindkey -M isearch "<" self-insert 183 | ``` 184 | 185 | To _remove_ pairs, use `unset 'AUTOPAIR_PAIRS[<]'`. Unbinding is optional. 186 | 187 | ## Troubleshooting & compatibility issues 188 | ### zgen & prezto compatibility 189 | Prezto's Editor module is known to reset autopair's bindings. A workaround is to 190 | _defer autopair from initializing_ (by setting `AUTOPAIR_INHIBIT_INIT=1`) and 191 | initialize it manually (by calling `autopair-init`): 192 | 193 | ``` sh 194 | source "$HOME/.zgen/zgen.zsh" 195 | 196 | # Add this 197 | AUTOPAIR_INHIBIT_INIT=1 198 | 199 | if ! zgen saved; then 200 | zgen prezto 201 | # ... 202 | zgen load hlissner/zsh-autopair 'autopair.zsh' 203 | #... 204 | zgen save 205 | fi 206 | 207 | # And this 208 | autopair-init 209 | ``` 210 | 211 | ### text on right-side of cursor interfere with completion 212 | Bind Tab to `expand-or-complete-prefix` and completion will ignore 213 | what's to the right of cursor: 214 | 215 | `bindkey '^I' expand-or-complete-prefix` 216 | 217 | This has the unfortunate side-effect of overwriting whatever's right of the 218 | cursor, however. 219 | 220 | ### zsh-autopair & isearch? 221 | zsh-autopair silently disables itself in isearch, as the two are incompatible. 222 | 223 | ### Midnight Commander 224 | MC hangs when zsh-autopair tries to bind the space key. This also breaks the MC 225 | subshell. 226 | 227 | Disable space expansion to work around this: `unset 'AUTOPAIR_PAIRS[ ]'` 228 | 229 | ## Other resources 230 | * Works wonderfully with [zsh-syntax-highlight] and 231 | `ZSH_HIGHLIGHT_HIGHLIGHTERS+=brackets`, but zsh-syntax-highlight must be 232 | loaded *after* zsh-autopair. 233 | * Mixes well with these vi-mode zsh modules: [surround], [select-quoted], and 234 | [select-bracketed] (they're built into zsh as of zsh-5.0.8) 235 | * Other relevant repositories of mine: 236 | + [dotfiles] 237 | + [emacs.d] 238 | + [vimrc] 239 | + [zshrc] 240 | 241 | 242 | [dotfiles]: https://github.com/hlissner/dotfiles 243 | [vimrc]: https://github.com/hlissner/.vim 244 | [emacs.d]: https://github.com/hlissner/doom-emacs 245 | [zshrc]: https://github.com/hlissner/dotfiles/blob/master/config/zsh/.zshrc 246 | [zsh-syntax-highlighting]: https://github.com/zsh-users/zsh-syntax-highlighting/blob/master/docs/highlighters/pattern.md 247 | [surround]: https://github.com/zsh-users/zsh/blob/master/Functions/Zle/surround 248 | [select-quoted]: https://github.com/zsh-users/zsh/blob/master/Functions/Zle/select-quoted 249 | [select-bracketed]: https://github.com/zsh-users/zsh/blob/master/Functions/Zle/select-bracketed 250 | -------------------------------------------------------------------------------- /autopair.plugin.zsh: -------------------------------------------------------------------------------- 1 | # 2 | 0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" 3 | 0="${${(M)0:#/*}:-$PWD/$0}" 4 | source "${0:A:h}/autopair.zsh" 5 | -------------------------------------------------------------------------------- /autopair.zsh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | 3 | AUTOPAIR_INHIBIT_INIT=${AUTOPAIR_INHIBIT_INIT:-} 4 | AUTOPAIR_BETWEEN_WHITESPACE=${AUTOPAIR_BETWEEN_WHITESPACE:-} 5 | AUTOPAIR_SPC_WIDGET=${AUTOPAIR_SPC_WIDGET:-"$(bindkey " " | cut -c5-)"} 6 | AUTOPAIR_BKSPC_WIDGET=${AUTOPAIR_BKSPC_WIDGET:-"$(bindkey "^?" | cut -c6-)"} 7 | AUTOPAIR_DELWORD_WIDGET=${AUTOPAIR_DELWORD_WIDGET:-"$(bindkey "^w" | cut -c6-)"} 8 | 9 | typeset -gA AUTOPAIR_PAIRS 10 | AUTOPAIR_PAIRS=('`' '`' "'" "'" '"' '"' '{' '}' '[' ']' '(' ')' ' ' ' ') 11 | 12 | typeset -gA AUTOPAIR_LBOUNDS 13 | AUTOPAIR_LBOUNDS=(all '[.:/\!]') 14 | AUTOPAIR_LBOUNDS+=(quotes '[]})a-zA-Z0-9]') 15 | AUTOPAIR_LBOUNDS+=(spaces '[^{([]') 16 | AUTOPAIR_LBOUNDS+=(braces '') 17 | AUTOPAIR_LBOUNDS+=('`' '`') 18 | AUTOPAIR_LBOUNDS+=('"' '"') 19 | AUTOPAIR_LBOUNDS+=("'" "'") 20 | 21 | typeset -gA AUTOPAIR_RBOUNDS 22 | AUTOPAIR_RBOUNDS=(all '[[{(<,.:?/%$!a-zA-Z0-9]') 23 | AUTOPAIR_RBOUNDS+=(quotes '[a-zA-Z0-9]') 24 | AUTOPAIR_RBOUNDS+=(spaces '[^]})]') 25 | AUTOPAIR_RBOUNDS+=(braces '') 26 | 27 | 28 | ### Helpers ############################ 29 | 30 | # Returns the other pair for $1 (a char), blank otherwise 31 | _ap-get-pair() { 32 | if [[ -n $1 ]]; then 33 | echo ${AUTOPAIR_PAIRS[$1]} 34 | elif [[ -n $2 ]]; then 35 | local i 36 | for i in ${(@k)AUTOPAIR_PAIRS}; do 37 | [[ $2 == ${AUTOPAIR_PAIRS[$i]} ]] && echo $i && break 38 | done 39 | fi 40 | } 41 | 42 | # Return 0 if cursor's surroundings match either regexp: $1 (left) or $2 (right) 43 | _ap-boundary-p() { 44 | [[ -n $1 && $LBUFFER =~ "$1$" ]] || [[ -n $2 && $RBUFFER =~ "^$2" ]] 45 | } 46 | 47 | # Return 0 if the surrounding text matches any of the AUTOPAIR_*BOUNDS regexps 48 | _ap-next-to-boundary-p() { 49 | local -a groups 50 | groups=(all) 51 | case $1 in 52 | \'|\"|\`) groups+=quotes ;; 53 | \{|\[|\(|\<) groups+=braces ;; 54 | " ") groups+=spaces ;; 55 | esac 56 | groups+=$1 57 | local group 58 | for group in $groups; do 59 | _ap-boundary-p ${AUTOPAIR_LBOUNDS[$group]} ${AUTOPAIR_RBOUNDS[$group]} && return 0 60 | done 61 | return 1 62 | } 63 | 64 | # Return 0 if there are the same number of $1 as there are $2 (chars; a 65 | # delimiter pair) in the buffer. 66 | _ap-balanced-p() { 67 | local lbuf="${LBUFFER//\\$1}" 68 | local rbuf="${RBUFFER//\\$2}" 69 | local llen="${#lbuf//[^$1]}" 70 | local rlen="${#rbuf//[^$2]}" 71 | if (( rlen == 0 && llen == 0 )); then 72 | return 0 73 | elif [[ $1 == $2 ]]; then 74 | if [[ $1 == " " ]]; then 75 | # Silence WARN_CREATE_GLOBAL errors 76 | local match= 77 | local mbegin= 78 | local mend= 79 | # Balancing spaces is unnecessary. If there is at least one space on 80 | # either side of the cursor, it is considered balanced. 81 | [[ $LBUFFER =~ "[^'\"]([ ]+)$" && $RBUFFER =~ "^${match[1]}" ]] && return 0 82 | return 1 83 | elif (( llen == rlen || (llen + rlen) % 2 == 0 )); then 84 | return 0 85 | fi 86 | else 87 | local l2len="${#lbuf//[^$2]}" 88 | local r2len="${#rbuf//[^$1]}" 89 | local ltotal=$((llen - l2len)) 90 | local rtotal=$((rlen - r2len)) 91 | 92 | (( ltotal < 0 )) && ltotal=0 93 | (( ltotal < rtotal )) && return 1 94 | return 0 95 | fi 96 | return 1 97 | } 98 | 99 | # Return 0 if the last keypress can be auto-paired. 100 | _ap-can-pair-p() { 101 | local rchar="$(_ap-get-pair $KEYS)" 102 | 103 | [[ -n $rchar ]] || return 1 104 | 105 | if [[ $rchar != " " ]]; then 106 | # Force pair if surrounded by space/[BE]OL, regardless of 107 | # boundaries/balance 108 | [[ -n $AUTOPAIR_BETWEEN_WHITESPACE && \ 109 | $LBUFFER =~ "(^|[ ])$" && \ 110 | $RBUFFER =~ "^($|[ ])" ]] && return 0 111 | 112 | # Don't pair quotes if the delimiters are unbalanced 113 | ! _ap-balanced-p $KEYS $rchar && return 1 114 | elif [[ $RBUFFER =~ "^[ ]*$" ]]; then 115 | # Don't pair spaces surrounded by whitespace 116 | return 1 117 | fi 118 | 119 | # Don't pair when in front of characters that likely signify the start of a 120 | # string, path or undesirable boundary. 121 | _ap-next-to-boundary-p $KEYS $rchar && return 1 122 | 123 | return 0 124 | } 125 | 126 | # Return 0 if the adjacent character (on the right) can be safely skipped over. 127 | _ap-can-skip-p() { 128 | if [[ -z $LBUFFER ]]; then 129 | return 1 130 | elif [[ $1 == $2 ]]; then 131 | if [[ $1 == " " ]]; then 132 | return 1 133 | elif ! _ap-balanced-p $1 $2; then 134 | return 1 135 | fi 136 | fi 137 | if ! [[ -n $2 && ${RBUFFER[1]} == $2 && ${LBUFFER[-1]} != '\' ]]; then 138 | return 1 139 | fi 140 | return 0 141 | } 142 | 143 | # Return 0 if the adjacent character (on the right) can be safely deleted. 144 | _ap-can-delete-p() { 145 | local lchar="${LBUFFER[-1]}" 146 | local rchar="$(_ap-get-pair $lchar)" 147 | ! [[ -n $rchar && ${RBUFFER[1]} == $rchar ]] && return 1 148 | if [[ $lchar == $rchar ]]; then 149 | if [[ $lchar == ' ' && ( $LBUFFER =~ "[^{([] +$" || $RBUFFER =~ "^ +[^]})]" ) ]]; then 150 | # Don't collapse spaces unless in delimiters 151 | return 1 152 | elif ! _ap-balanced-p $lchar $rchar; then 153 | return 1 154 | fi 155 | fi 156 | return 0 157 | } 158 | 159 | # Insert $1 and add $2 after the cursor 160 | _ap-self-insert() { 161 | LBUFFER+=$1 162 | RBUFFER="$2$RBUFFER" 163 | } 164 | 165 | 166 | ### Widgets ############################ 167 | 168 | autopair-insert() { 169 | local rchar="$(_ap-get-pair $KEYS)" 170 | if [[ $KEYS == (\'|\"|\`| ) ]] && _ap-can-skip-p $KEYS $rchar; then 171 | zle forward-char 172 | elif _ap-can-pair-p; then 173 | _ap-self-insert $KEYS $rchar 174 | elif [[ $rchar == " " ]]; then 175 | zle ${AUTOPAIR_SPC_WIDGET:-self-insert} 176 | else 177 | zle self-insert 178 | fi 179 | } 180 | 181 | autopair-close() { 182 | if _ap-can-skip-p "$(_ap-get-pair "" $KEYS)" $KEYS; then 183 | zle forward-char 184 | else 185 | zle self-insert 186 | fi 187 | } 188 | 189 | autopair-delete() { 190 | _ap-can-delete-p && RBUFFER=${RBUFFER:1} 191 | zle ${AUTOPAIR_BKSPC_WIDGET:-backward-delete-char} 192 | } 193 | 194 | autopair-delete-word() { 195 | _ap-can-delete-p && RBUFFER=${RBUFFER:1} 196 | zle ${AUTOPAIR_DELWORD_WIDGET:-backward-delete-word} 197 | } 198 | 199 | 200 | ### Initialization ##################### 201 | 202 | autopair-init() { 203 | zle -N autopair-insert 204 | zle -N autopair-close 205 | zle -N autopair-delete 206 | zle -N autopair-delete-word 207 | 208 | local p 209 | for p in ${(@k)AUTOPAIR_PAIRS}; do 210 | bindkey "$p" autopair-insert 211 | bindkey -M isearch "$p" self-insert 212 | 213 | local rchar="$(_ap-get-pair $p)" 214 | if [[ $p != $rchar ]]; then 215 | bindkey "$rchar" autopair-close 216 | bindkey -M isearch "$rchar" self-insert 217 | fi 218 | done 219 | 220 | bindkey "^?" autopair-delete 221 | bindkey "^h" autopair-delete 222 | bindkey "^w" autopair-delete-word 223 | bindkey -M isearch "^?" backward-delete-char 224 | bindkey -M isearch "^h" backward-delete-char 225 | bindkey -M isearch "^w" backward-delete-word 226 | } 227 | [[ -n $AUTOPAIR_INHIBIT_INIT ]] || autopair-init 228 | -------------------------------------------------------------------------------- /tests/_support/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zsh 2 | source autopair.zsh 3 | export KEYS= 4 | export LBUFFER= 5 | export RBUFFER= 6 | 7 | assert_true() { 8 | run $@ 9 | assert $state equals 0 10 | } 11 | 12 | assert_false() { 13 | run $@ 14 | assert $state equals 1 15 | } 16 | -------------------------------------------------------------------------------- /tests/balanced-p.zunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zunit 2 | @test 'basic boundary tests' { 3 | LBUFFER="abc" 4 | RBUFFER="123" 5 | assert_false _ap-boundary-p " " " " 6 | assert_true _ap-boundary-p "[^3]" "[^a]" 7 | assert_false _ap-boundary-p "[^c]" "[^1]" 8 | assert_true _ap-boundary-p "c" " " 9 | assert_true _ap-boundary-p " " "1" 10 | } 11 | 12 | @test 'no boundary on blank line' { 13 | LBUFFER= 14 | RBUFFER= 15 | assert_false _ap-next-to-boundary-p "{" 16 | } 17 | -------------------------------------------------------------------------------- /tests/can-delete-p.zunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zunit 2 | @test 'delete if next to space and pair' { 3 | LBUFFER="[ " RBUFFER=" ]" assert_true _ap-can-delete-p 4 | } 5 | 6 | @test 'delete if next to homogeneous counter-pair' { 7 | LBUFFER="'" RBUFFER="'" assert_true _ap-can-delete-p 8 | } 9 | 10 | @test 'delete if next to heterogeneous counter-pair' { 11 | LBUFFER="(" RBUFFER=")" assert_true _ap-can-delete-p 12 | } 13 | 14 | @test 'do not delete if at eol' { 15 | LBUFFER="'" assert_false _ap-can-delete-p 16 | } 17 | 18 | @test 'do not delete if within too many spaces' { 19 | LBUFFER="[ " RBUFFER=" ]" assert_false _ap-can-delete-p 20 | } 21 | 22 | @test 'do not delete if only next to space' { 23 | LBUFFER=" " RBUFFER=" " assert_false _ap-can-delete-p 24 | } 25 | -------------------------------------------------------------------------------- /tests/can-pair-p.zunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zunit 2 | @test 'pair if blank line' { 3 | KEYS="'" LBUFFER="" RBUFFER="" assert_true _ap-can-pair-p 4 | } 5 | 6 | @test 'pair if next to balanced delimiter' { # {{|}} 7 | KEYS="{" LBUFFER="{" RBUFFER="}" assert_true _ap-can-pair-p 8 | } 9 | 10 | @test 'pair space if next to brackets' { # { | } 11 | KEYS=" " 12 | LBUFFER="{" RBUFFER="}" assert_true _ap-can-pair-p 13 | LBUFFER="[" RBUFFER="]" assert_true _ap-can-pair-p 14 | LBUFFER="(" RBUFFER=")" assert_true _ap-can-pair-p 15 | } 16 | 17 | @test 'pair brackets at the end of a word' { # abc{|} 18 | KEYS='{' LBUFFER="hello" assert_true _ap-can-pair-p 19 | } 20 | 21 | @test 'do not pair brackets at the beginning of a word' { # {|abc 22 | KEYS='{' RBUFFER="hello" assert_false _ap-can-pair-p 23 | } 24 | 25 | @test 'do not pair quotes next to a word' { 26 | KEYS="'" 27 | LBUFFER="hello" assert_false _ap-can-pair-p # abc"| 28 | RBUFFER="hello" assert_false _ap-can-pair-p # "|abc 29 | } 30 | 31 | @test 'do not pair the same quotes from inside quotes' { # ""|" 32 | KEYS='"' LBUFFER='"' RBUFFER='"' assert_false _ap-can-pair-p 33 | } 34 | 35 | @test 'do not pair if delimiter is invalid' { 36 | KEYS="<" LBUFFER="<" RBUFFER=">" assert_false _ap-can-pair-p 37 | } 38 | 39 | @test 'do not pair if next to unbalanced delimiter' { # {|} 40 | KEYS="{" LBUFFER="" RBUFFER="}" assert_false _ap-can-pair-p 41 | } 42 | 43 | @test 'do not pair if next to unbalanced delimiter after space' { # {| } 44 | AUTOPAIR_BETWEEN_WHITESPACE= 45 | KEYS="{" LBUFFER="" RBUFFER=" }" assert_false _ap-can-pair-p 46 | KEYS="{" LBUFFER="" RBUFFER=" }" assert_false _ap-can-pair-p 47 | KEYS="{" LBUFFER=" " RBUFFER=" }" assert_false _ap-can-pair-p 48 | } 49 | 50 | @test 'do not pair space if next to non-brackets/spaces' { 51 | KEYS=" " 52 | LBUFFER="'" RBUFFER="'" assert_false _ap-can-pair-p # ' |' 53 | LBUFFER="abc" RBUFFER="xyz" assert_false _ap-can-pair-p # abc |xyz' 54 | LBUFFER="[ " RBUFFER=" ]" assert_false _ap-can-pair-p # [ | ] 55 | } 56 | 57 | @test 'AUTOPAIR_BETWEEN_WHITESPACE=1' { 58 | AUTOPAIR_BETWEEN_WHITESPACE=1 KEYS='{' LBUFFER=" " RBUFFER=" }" assert_true _ap-can-pair-p 59 | } 60 | -------------------------------------------------------------------------------- /tests/can-skip-p.zunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zunit 2 | 3 | @test 'fail skip-check on blank line' { 4 | LBUFFER='' RBUFFER='' assert_false _ap-can-skip-p '{' '}' 5 | } 6 | 7 | @test 'fail skip-check on wrong pair' { 8 | LBUFFER='"' RBUFFER='"' assert_false _ap-can-skip-p '{' '}' 9 | } 10 | 11 | @test 'skip if next to homogeneous counter-pair' { 12 | LBUFFER='"' RBUFFER='"' assert_true _ap-can-skip-p '"' '"' 13 | } 14 | 15 | @test 'skip if next to heterogeneous counter-pair' { 16 | LBUFFER='{' RBUFFER='}' assert_true _ap-can-skip-p '{' '}' 17 | } 18 | 19 | @test 'do not skip if next to unbalanced, homogeneous counter-pair' { 20 | LBUFFER='' RBUFFER='"' assert_false _ap-can-skip-p '"' '"' 21 | } 22 | 23 | @test 'do not skip if next to unbalanced, heterogeneous counter-pair' { 24 | LBUFFER='' RBUFFER='}' assert_false _ap-can-skip-p '{' '}' 25 | } 26 | -------------------------------------------------------------------------------- /tests/get-pair.zunit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zunit 2 | @test 'existing pair' { 3 | assert $(_ap-get-pair "{") same_as "}" 4 | } 5 | 6 | @test 'existing right-pair' { 7 | assert $(_ap-get-pair "" "}") same_as "{" 8 | } 9 | 10 | @test 'non-existent pair' { 11 | assert $(_ap-get-pair "<") same_as "" 12 | } 13 | 14 | @test 'non-existent right-pair' { 15 | assert $(_ap-get-pair "" ">") same_as "" 16 | } 17 | 18 | @test 'all default pairs' { 19 | assert '"' in ${(@k)AUTOPAIR_PAIRS} 20 | assert "'" in ${(@k)AUTOPAIR_PAIRS} 21 | assert '`' in ${(@k)AUTOPAIR_PAIRS} 22 | assert '{' in ${(@k)AUTOPAIR_PAIRS} 23 | assert '[' in ${(@k)AUTOPAIR_PAIRS} 24 | assert '(' in ${(@k)AUTOPAIR_PAIRS} 25 | assert ' ' in ${(@k)AUTOPAIR_PAIRS} 26 | } 27 | -------------------------------------------------------------------------------- /zsh-autopair.plugin.zsh: -------------------------------------------------------------------------------- 1 | # 2 | 0="${${ZERO:-${0:#$ZSH_ARGZERO}}:-${(%):-%N}}" 3 | 0="${${(M)0:#/*}:-$PWD/$0}" 4 | source "${0:A:h}/autopair.zsh" 5 | --------------------------------------------------------------------------------