diff --git a/CHANGELOG.md b/CHANGELOG.md index ac0f8e6..f017be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,34 @@ # Changelog +## v0.4.3 +- Avoid bell when accepting suggestions with `autosuggest-accept` (#228) +- Don't fetch suggestions after [up,down]-line-or-beginning-search (#227, #241) +- We are now running CI against new 5.5.1 version +- Fix partial-accept in vi mode (#188) +- Fix suggestion disappearing on fast movement after switching to `vicmd` mode (#290) +- Fix issue rotating through kill ring with `yank-pop` (#301) +- Fix issue creating new pty for async mode when previous pty is not properly cleaned up (#249) + +## v0.4.2 +- Fix bug in zsh versions older than 5.0.8 (#296) +- Officially support back to zsh v4.3.11 + +## v0.4.1 +- Switch to [[ and (( conditionals instead of [ (#257) +- Avoid warnnestedvar warnings with `typeset -g` (#275) +- Replace tabs with spaces in yaml (#268) +- Clean up and fix escaping of special characters (#267) +- Add `emacs-forward-word` to default list of partial accept widgets (#246) + ## v0.4.0 - High-level integration tests using RSpec and tmux - Add continuous integration with Circle CI -- Experimental support for asynchronous suggestions (#) -- Fix problems with multi-line suggestions (#) +- Experimental support for asynchronous suggestions (#170) +- Fix problems with multi-line suggestions (#225) - Optimize case where manually typing in suggestion -- Avoid wrapping any zle-* widgets (#) +- Avoid wrapping any zle-* widgets (#206) - Remove support for deprecated options from v0.0.x -- Handle history entries that begin with dashes (#) +- Handle history entries that begin with dashes - Gracefully handle being sourced multiple times (#126) - Add enable/disable/toggle widgets to disable/enable suggestions (#219) diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..945cec7 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,67 @@ +## Installation + +### Manual (Git Clone) + +1. Clone this repository somewhere on your machine. This guide will assume `~/.zsh/zsh-autosuggestions`. + + ```sh + git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions + ``` + +2. Add the following to your `.zshrc`: + + ```sh + source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh + ``` + +3. Start a new terminal session. + +### Oh My Zsh + +1. Clone this repository into `$ZSH_CUSTOM/plugins` (by default `~/.oh-my-zsh/custom/plugins`) + + ```sh + git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions + ``` + +2. Add the plugin to the list of plugins for Oh My Zsh to load: + + ```sh + plugins=(zsh-autosuggestions) + ``` + +3. Start a new terminal session. + +### Arch Linux + +1. Install [`zsh-autosuggestions`](https://www.archlinux.org/packages/community/any/zsh-autosuggestions/) from the `community` repository. + + ```sh + pacman -S zsh-autosuggestions + ``` + + or, to use a package based on the `master` branch, install [`zsh-autosuggestions-git`](https://aur.archlinux.org/packages/zsh-autosuggestions-git/) from the [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository). + +2. Add the following to your `.zshrc`: + + ```sh + source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh + ``` + +3. Start a new terminal session. + +### macOS via Homebrew +1. Install the `zsh-autosuggestions` package using [Homebrew](https://brew.sh/). + + ```sh + brew install zsh-autosuggestions + ``` + +2. Add the following to your `.zshrc`: + + ```sh + source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh + ``` + +3. Start a new terminal session. + diff --git a/LICENSE b/LICENSE index ad6594e..bcbc8b9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2013 Thiago de Arruda -Copyright (c) 2016-2017 Eric Freese +Copyright (c) 2016-2018 Eric Freese Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/Makefile b/Makefile index 5402b7f..d5d162c 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,7 @@ SRC_DIR := ./src SRC_FILES := \ $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ + $(SRC_DIR)/util.zsh \ $(SRC_DIR)/features.zsh \ $(SRC_DIR)/bind.zsh \ $(SRC_DIR)/highlight.zsh \ diff --git a/README.md b/README.md index 8aedb31..4ad07d8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ _[Fish](http://fishshell.com/)-like fast/unobtrusive autosuggestions for zsh._ It suggests commands as you type, based on command history. +Requirements: Zsh v4.3.11 or later + [![CircleCI](https://circleci.com/gh/zsh-users/zsh-autosuggestions.svg?style=svg)](https://circleci.com/gh/zsh-users/zsh-autosuggestions) @@ -11,72 +13,8 @@ It suggests commands as you type, based on command history. ## Installation -### Manual +See [INSTALL.md](INSTALL.md). -1. Clone this repository somewhere on your machine. This guide will assume `~/.zsh/zsh-autosuggestions`. - - ```sh - git clone https://github.com/zsh-users/zsh-autosuggestions ~/.zsh/zsh-autosuggestions - ``` - -2. Add the following to your `.zshrc`: - - ```sh - source ~/.zsh/zsh-autosuggestions/zsh-autosuggestions.zsh - ``` - -3. Start a new terminal session. - - -### Oh My Zsh - -1. Clone this repository into `$ZSH_CUSTOM/plugins` (by default `~/.oh-my-zsh/custom/plugins`) - - ```sh - git clone https://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions - ``` - -2. Add the plugin to the list of plugins for Oh My Zsh to load: - - ```sh - plugins=(zsh-autosuggestions) - ``` - -3. Start a new terminal session. - -### Arch Linux via the AUR -1. Install the [`zsh-autosuggestions`](https://aur.archlinux.org/packages/zsh-autosuggestions/) or the [`zsh-autosuggestions-git`](https://aur.archlinux.org/packages/zsh-autosuggestions-git/) packages from the [AUR](https://wiki.archlinux.org/index.php/Arch_User_Repository). - - ```sh - pacaur -S zsh-autosuggestions - ``` - or - ``` - pacaur -S zsh-autosuggestions-git - ``` - -2. Add the following to your `.zshrc`: - - ```sh - source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh - ``` - -3. Start a new terminal session. - -### macOS via Homebrew -1. Install the `zsh-autosuggestions` package using [Homebrew](https://brew.sh/). - - ```sh - brew install zsh-autosuggestions - ``` - -2. Add the following to your `.zshrc`: - - ```sh - source /usr/local/share/zsh-autosuggestions/zsh-autosuggestions.zsh - ``` - -3. Start a new terminal session. ## Usage diff --git a/VERSION b/VERSION index fb7a04c..f87d474 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.4.0 +v0.4.3 diff --git a/circle.yml b/circle.yml index 5e3a6f6..c804ae1 100644 --- a/circle.yml +++ b/circle.yml @@ -1,12 +1,12 @@ machine: environment: - ZSH_VERSIONS: 5.0.8 5.1.1 5.2 5.3.1 + ZSH_VERSIONS: zsh-dev/4.3.11 zsh/5.0.2 zsh/5.0.8 zsh/5.1.1 zsh/5.2 zsh/5.3.1 zsh/5.4.2 zsh/5.5.1 dependencies: pre: - - for v in $(echo $ZSH_VERSIONS | awk "{ for (i=$((1+CIRCLE_NODE_INDEX));i<=NF;i+=$CIRCLE_NODE_TOTAL) print \$i }"); do wget https://sourceforge.net/projects/zsh/files/zsh/$v/zsh-$v.tar.gz && tar xzf zsh-$v.tar.gz && cd zsh-$v && ./configure && sudo make install || exit 1; done + - for v in $(echo $ZSH_VERSIONS | awk "{ for (i=$((1+CIRCLE_NODE_INDEX));i<=NF;i+=$CIRCLE_NODE_TOTAL) print \$i }"); do wget https://sourceforge.net/projects/zsh/files/$v/zsh-${v#*/}.tar.gz && tar xzf zsh-${v#*/}.tar.gz && pushd zsh-${v#*/} && ./configure && sudo make install && popd || exit 1; done test: override: - - for v in $(echo $ZSH_VERSIONS | awk "{ for (i=$((1+CIRCLE_NODE_INDEX));i<=NF;i+=$CIRCLE_NODE_TOTAL) print \$i }"); do TEST_ZSH_BIN=/usr/local/bin/zsh-$v make test || exit 1; done: + - for v in $(echo $ZSH_VERSIONS | awk "{ for (i=$((1+CIRCLE_NODE_INDEX));i<=NF;i+=$CIRCLE_NODE_TOTAL) print \$i }"); do TEST_ZSH_BIN=/usr/local/bin/zsh-${v#*/} make test || exit 1; done: parallel: true diff --git a/spec/async_spec.rb b/spec/async_spec.rb new file mode 100644 index 0000000..152adde --- /dev/null +++ b/spec/async_spec.rb @@ -0,0 +1,84 @@ +context 'with asynchronous suggestions enabled' do + before do + skip 'Async mode not supported below v5.0.8' if session.zsh_version < Gem::Version.new('5.0.8') + end + + let(:options) { ["ZSH_AUTOSUGGEST_USE_ASYNC="] } + + describe '`up-line-or-beginning-search`' do + let(:before_sourcing) do + -> do + session. + run_command('autoload -U up-line-or-beginning-search'). + run_command('zle -N up-line-or-beginning-search'). + send_string('bindkey "'). + send_keys('C-v').send_keys('up'). + send_string('" up-line-or-beginning-search'). + send_keys('enter') + end + end + + it 'should show previous history entries' do + with_history( + 'echo foo', + 'echo bar', + 'echo baz' + ) do + session.clear_screen + 3.times { session.send_keys('up') } + wait_for { session.content }.to eq("echo foo") + end + end + end + + it 'should not add extra carriage returns before newlines' do + session. + send_string('echo "'). + send_keys('escape'). + send_keys('enter'). + send_string('"'). + send_keys('enter') + + session.clear_screen + + session.send_string('echo') + wait_for { session.content }.to eq("echo \"\n\"") + end + + it 'should treat carriage returns and newlines as separate characters' do + session. + send_string('echo "'). + send_keys('C-v'). + send_keys('enter'). + send_string('foo"'). + send_keys('enter') + + session. + send_string('echo "'). + send_keys('control'). + send_keys('enter'). + send_string('bar"'). + send_keys('enter') + + session.clear_screen + + session. + send_string('echo "'). + send_keys('C-v'). + send_keys('enter') + + wait_for { session.content }.to eq('echo "^Mfoo"') + end + + describe 'exiting a subshell' do + it 'should not cause error messages to be printed' do + session.run_command('$(exit)') + + sleep 1 + + expect(session.content).to eq('$(exit)') + end + end +end + + diff --git a/spec/integrations/rebound_bracket_spec.rb b/spec/integrations/rebound_bracket_spec.rb new file mode 100644 index 0000000..8b420f0 --- /dev/null +++ b/spec/integrations/rebound_bracket_spec.rb @@ -0,0 +1,13 @@ +describe 'rebinding [' do + context 'initialized before sourcing the plugin' do + before do + session.run_command("function [ { $commands[\\[] \"$@\" }") + session.clear_screen + end + + it 'executes the custom behavior and the built-in behavior' do + session.send_string('asdf') + wait_for { session.content }.to eq('asdf') + end + end +end diff --git a/spec/integrations/vi_mode_spec.rb b/spec/integrations/vi_mode_spec.rb new file mode 100644 index 0000000..cf471b5 --- /dev/null +++ b/spec/integrations/vi_mode_spec.rb @@ -0,0 +1,67 @@ +describe 'when using vi mode' do + let(:before_sourcing) do + -> do + session.run_command('bindkey -v') + end + end + + describe 'moving the cursor after exiting insert mode' do + it 'should not clear the current suggestion' do + with_history('foobar foo') do + session. + send_string('foo'). + send_keys('escape'). + send_keys('h') + + wait_for { session.content }.to eq('foobar foo') + end + end + end + + describe '`vi-forward-word-end`' do + it 'should accept through the end of the current word' do + with_history('foobar foo') do + session. + send_string('foo'). + send_keys('escape'). + send_keys('e'). # vi-forward-word-end + send_keys('a'). # vi-add-next + send_string('baz') + + wait_for { session.content }.to eq('foobarbaz') + end + end + end + + describe '`vi-forward-word`' do + it 'should accept through the first character of the next word' do + with_history('foobar foo') do + session. + send_string('foo'). + send_keys('escape'). + send_keys('w'). # vi-forward-word + send_keys('a'). # vi-add-next + send_string('az') + + wait_for { session.content }.to eq('foobar faz') + end + end + end + + describe '`vi-find-next-char`' do + it 'should accept through the next occurrence of the character' do + with_history('foobar foo') do + session. + send_string('foo'). + send_keys('escape'). + send_keys('f'). # vi-find-next-char + send_keys('o'). + send_keys('a'). # vi-add-next + send_string('b') + + wait_for { session.content }.to eq('foobar fob') + end + end + end +end + diff --git a/spec/kill_ring_spec.rb b/spec/kill_ring_spec.rb new file mode 100644 index 0000000..4d0178f --- /dev/null +++ b/spec/kill_ring_spec.rb @@ -0,0 +1,23 @@ +context 'with some items in the kill ring' do + before do + session. + send_string('echo foo'). + send_keys('C-u'). + send_string('echo bar'). + send_keys('C-u') + end + + describe '`yank-pop`' do + it 'should cycle through all items in the kill ring' do + session.send_keys('C-y') + wait_for { session.content }.to eq('echo bar') + + session.send_keys('escape').send_keys('y') + wait_for { session.content }.to eq('echo foo') + + session.send_keys('escape').send_keys('y') + wait_for { session.content }.to eq('echo bar') + end + end +end + diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index 82705d3..f91ee6c 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -18,6 +18,10 @@ class TerminalSession tmux_command("new-session -d -x #{opts[:width]} -y #{opts[:height]} '#{cmd}'") end + def zsh_version + @zsh_version ||= Gem::Version.new(`#{ZSH_BIN} -c 'echo -n $ZSH_VERSION'`) + end + def tmux_socket_name @tmux_socket_name ||= SecureRandom.hex(6) end @@ -77,6 +81,10 @@ class TerminalSession map(&:to_i) end + def attach! + tmux_command('attach-session') + end + private attr_reader :opts diff --git a/src/async.zsh b/src/async.zsh index 124c9ac..62e3329 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -17,13 +17,15 @@ _zsh_autosuggest_async_server() { sleep 1 # Block for long enough for the signal to come through } - # Output only newlines (not carriage return + newline) + # Don't add any extra carriage returns stty -onlcr + # Don't translate carriage returns to newlines + stty -icrnl + # Silence any error messages exec 2>/dev/null - local strategy=$1 local last_pid while IFS='' read -r -d $'\0' query; do @@ -63,14 +65,14 @@ _zsh_autosuggest_async_pty_create() { typeset -h REPLY # If we won't get a fd back from zpty, try to guess it - if [ $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD -eq 0 ]; then + if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); then integer -l zptyfd exec {zptyfd}>&1 # Open a new file descriptor (above 10). exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi # Fork a zpty process running the server function - zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "_zsh_autosuggest_async_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server # Store the fd so we can remove the handler later if (( REPLY )); then @@ -84,13 +86,11 @@ _zsh_autosuggest_async_pty_create() { } _zsh_autosuggest_async_pty_destroy() { - if zpty -t $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null; then - # Remove the input handler - zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null - # Destroy the zpty - zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null - fi + # Destroy the zpty + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null } _zsh_autosuggest_async_pty_recreate() { diff --git a/src/bind.zsh b/src/bind.zsh index be0d164..42a0dd0 100644 --- a/src/bind.zsh +++ b/src/bind.zsh @@ -88,13 +88,13 @@ _zsh_autosuggest_bind_widgets() { # Find every widget we might want to bind and bind it appropriately for widget in ${${(f)"$(builtin zle -la)"}:#${(j:|:)~ignore_widgets}}; do - if [ ${ZSH_AUTOSUGGEST_CLEAR_WIDGETS[(r)$widget]} ]; then + if [[ -n ${ZSH_AUTOSUGGEST_CLEAR_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget clear - elif [ ${ZSH_AUTOSUGGEST_ACCEPT_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget accept - elif [ ${ZSH_AUTOSUGGEST_EXECUTE_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_EXECUTE_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget execute - elif [ ${ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget partial_accept else # Assume any unspecified widget might modify the buffer @@ -106,13 +106,13 @@ _zsh_autosuggest_bind_widgets() { # Given the name of an original widget and args, invoke it, if it exists _zsh_autosuggest_invoke_original_widget() { # Do nothing unless called with at least one arg - [ $# -gt 0 ] || return + (( $# )) || return 0 local original_widget_name="$1" shift - if [ $widgets[$original_widget_name] ]; then + if (( ${+widgets[$original_widget_name]} )); then zle $original_widget_name -- $@ fi } diff --git a/src/config.zsh b/src/config.zsh index ba694c0..597307f 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -21,6 +21,8 @@ ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( history-beginning-search-backward history-substring-search-up history-substring-search-down + up-line-or-beginning-search + down-line-or-beginning-search up-line-or-history down-line-or-history accept-line @@ -47,6 +49,8 @@ ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS=( vi-forward-word-end vi-forward-blank-word vi-forward-blank-word-end + vi-find-next-char + vi-find-next-char-skip ) # Widgets that should be ignored (globbing supported but must be escaped) @@ -57,6 +61,7 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( set-local-history which-command yank + yank-pop ) # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. diff --git a/src/highlight.zsh b/src/highlight.zsh index aa5a427..273c03d 100644 --- a/src/highlight.zsh +++ b/src/highlight.zsh @@ -7,7 +7,7 @@ _zsh_autosuggest_highlight_reset() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT - if [ -n "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT" ]; then + if [[ -n "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT" ]]; then region_highlight=("${(@)region_highlight:#$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT}") unset _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT fi @@ -17,7 +17,7 @@ _zsh_autosuggest_highlight_reset() { _zsh_autosuggest_highlight_apply() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT - if [ $#POSTDISPLAY -gt 0 ]; then + if (( $#POSTDISPLAY )); then typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT="$#BUFFER $(($#BUFFER + $#POSTDISPLAY)) $ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" region_highlight+=("$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT") else diff --git a/src/start.zsh b/src/start.zsh index 6fa8ce9..6f48ab6 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -15,7 +15,7 @@ _zsh_autosuggest_start() { # to the widget list variables to take effect on the next precmd. add-zsh-hook precmd _zsh_autosuggest_bind_widgets - if [ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]; then + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then _zsh_autosuggest_async_start fi } diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index 17bf65d..0e85fb5 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -7,9 +7,19 @@ # _zsh_autosuggest_strategy_default() { - local prefix="$1" + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB + + # Escape backslashes and all of the glob operators so we can use + # this string as a pattern to search the $history associative array. + # - (#m) globbing flag enables setting references for match data + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 + local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)${(b)prefix}*]}" + typeset -g suggestion="${history[(r)${prefix}*]}" } diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh index 8a84728..f76d3c1 100644 --- a/src/strategies/match_prev_cmd.zsh +++ b/src/strategies/match_prev_cmd.zsh @@ -21,18 +21,25 @@ # `HIST_EXPIRE_DUPS_FIRST`. _zsh_autosuggest_strategy_match_prev_cmd() { - local prefix="$1" + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB + + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 + local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get all history event numbers that correspond to history # entries that match pattern $prefix* local history_match_keys - history_match_keys=(${(k)history[(R)${(b)prefix}*]}) + history_match_keys=(${(k)history[(R)$prefix*]}) # By default we use the first history number (most recent history entry) local histkey="${history_match_keys[1]}" # Get the previously executed command - local prev_cmd="${history[$((HISTCMD-1))]}" + local prev_cmd="$(_zsh_autosuggest_escape_command "${history[$((HISTCMD-1))]}")" # Iterate up to the first 200 history event numbers that match $prefix for key in "${(@)history_match_keys[1,200]}"; do diff --git a/src/util.zsh b/src/util.zsh new file mode 100644 index 0000000..1f55d36 --- /dev/null +++ b/src/util.zsh @@ -0,0 +1,11 @@ + +#--------------------------------------------------------------------# +# Utility Functions # +#--------------------------------------------------------------------# + +_zsh_autosuggest_escape_command() { + setopt localoptions EXTENDED_GLOB + + # Escape special chars in the string (requires EXTENDED_GLOB) + echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" +} diff --git a/src/widgets.zsh b/src/widgets.zsh index 1ccf2c2..607685c 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -13,14 +13,14 @@ _zsh_autosuggest_disable() { _zsh_autosuggest_enable() { unset _ZSH_AUTOSUGGEST_DISABLED - if [ $#BUFFER -gt 0 ]; then + if (( $#BUFFER )); then _zsh_autosuggest_fetch fi } # Toggle suggestions (enable/disable) _zsh_autosuggest_toggle() { - if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then + if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then _zsh_autosuggest_enable else _zsh_autosuggest_disable @@ -54,35 +54,36 @@ _zsh_autosuggest_modify() { retval=$? # Don't fetch a new suggestion if there's more input to be read immediately - if [[ $PENDING > 0 ]] || [[ $KEYS_QUEUED_COUNT > 0 ]]; then + if (( $PENDING > 0 )) || (( $KEYS_QUEUED_COUNT > 0 )); then + POSTDISPLAY="$orig_postdisplay" return $retval fi # Optimize if manually typing in the suggestion - if [ $#BUFFER -gt $#orig_buffer ]; then + if (( $#BUFFER > $#orig_buffer )); then local added=${BUFFER#$orig_buffer} # If the string added matches the beginning of the postdisplay - if [ "$added" = "${orig_postdisplay:0:$#added}" ]; then + if [[ "$added" = "${orig_postdisplay:0:$#added}" ]]; then POSTDISPLAY="${orig_postdisplay:$#added}" return $retval fi fi # Don't fetch a new suggestion if the buffer hasn't changed - if [ "$BUFFER" = "$orig_buffer" ]; then + if [[ "$BUFFER" = "$orig_buffer" ]]; then POSTDISPLAY="$orig_postdisplay" return $retval fi # Bail out if suggestions are disabled - if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then + if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then return $? fi # Get a new suggestion if the buffer is not empty after modification - if [ $#BUFFER -gt 0 ]; then - if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then + if (( $#BUFFER > 0 )); then + if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi fi @@ -105,7 +106,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { local suggestion="$1" - if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then + if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" else unset POSTDISPLAY @@ -124,12 +125,12 @@ _zsh_autosuggest_accept() { # Only accept if the cursor is at the end of the buffer if [[ $CURSOR = $max_cursor_pos ]]; then - # Accepting the whole line is basically a specific case of - # accepting partially with "end-of-line" widget - # NB : we can't directly call end-of-line because since we wrap it, - # it would cause a recursive call to it - # TODO : find a non-hardcoded way to call this widget - _zsh_autosuggest_partial_accept "autosuggest-orig-1-end-of-line" + # Accepting the whole line is basically a specific case of + # accepting partially with "end-of-line" widget + # NB : we can't directly call end-of-line because since we wrap it, + # it would cause a recursive call to it + # TODO : find a non-hardcoded way to call this widget + _zsh_autosuggest_partial_accept "autosuggest-orig-1-end-of-line" fi _zsh_autosuggest_invoke_original_widget $@ @@ -150,7 +151,7 @@ _zsh_autosuggest_execute() { # Partially accept the suggestion _zsh_autosuggest_partial_accept() { - local -i retval + local -i retval cursor_loc # Save the contents of the buffer so we can restore later if needed local original_buffer="$BUFFER" @@ -162,13 +163,19 @@ _zsh_autosuggest_partial_accept() { _zsh_autosuggest_invoke_original_widget $@ retval=$? + # Normalize cursor location across vi/emacs modes + cursor_loc=$CURSOR + if [[ "$KEYMAP" = "vicmd" ]]; then + cursor_loc=$((cursor_loc + 1)) + fi + # If we've moved past the end of the original buffer - if [ $CURSOR -gt $#original_buffer ]; then + if (( $cursor_loc > $#original_buffer )); then # Set POSTDISPLAY to text right of the cursor - POSTDISPLAY="$RBUFFER" + POSTDISPLAY="${BUFFER[$(($cursor_loc + 1)),$#BUFFER]}" # Clip the buffer at the cursor - BUFFER="$LBUFFER" + BUFFER="${BUFFER[1,$cursor_loc]}" else # Restore the original buffer BUFFER="$original_buffer" diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 8524f7d..e101da0 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -1,8 +1,8 @@ # Fish-like fast/unobtrusive autosuggestions for zsh. # https://github.com/zsh-users/zsh-autosuggestions -# v0.4.0 +# v0.4.3 # Copyright (c) 2013 Thiago de Arruda -# Copyright (c) 2016-2017 Eric Freese +# Copyright (c) 2016-2018 Eric Freese # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation @@ -57,6 +57,8 @@ ZSH_AUTOSUGGEST_CLEAR_WIDGETS=( history-beginning-search-backward history-substring-search-up history-substring-search-down + up-line-or-beginning-search + down-line-or-beginning-search up-line-or-history down-line-or-history accept-line @@ -83,6 +85,8 @@ ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS=( vi-forward-word-end vi-forward-blank-word vi-forward-blank-word-end + vi-find-next-char + vi-find-next-char-skip ) # Widgets that should be ignored (globbing supported but must be escaped) @@ -93,6 +97,7 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( set-local-history which-command yank + yank-pop ) # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. @@ -101,6 +106,17 @@ ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Pty name for calculating autosuggestions asynchronously ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty +#--------------------------------------------------------------------# +# Utility Functions # +#--------------------------------------------------------------------# + +_zsh_autosuggest_escape_command() { + setopt localoptions EXTENDED_GLOB + + # Escape special chars in the string (requires EXTENDED_GLOB) + echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" +} + #--------------------------------------------------------------------# # Feature Detection # #--------------------------------------------------------------------# @@ -209,13 +225,13 @@ _zsh_autosuggest_bind_widgets() { # Find every widget we might want to bind and bind it appropriately for widget in ${${(f)"$(builtin zle -la)"}:#${(j:|:)~ignore_widgets}}; do - if [ ${ZSH_AUTOSUGGEST_CLEAR_WIDGETS[(r)$widget]} ]; then + if [[ -n ${ZSH_AUTOSUGGEST_CLEAR_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget clear - elif [ ${ZSH_AUTOSUGGEST_ACCEPT_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget accept - elif [ ${ZSH_AUTOSUGGEST_EXECUTE_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_EXECUTE_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget execute - elif [ ${ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS[(r)$widget]} ]; then + elif [[ -n ${ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS[(r)$widget]} ]]; then _zsh_autosuggest_bind_widget $widget partial_accept else # Assume any unspecified widget might modify the buffer @@ -227,13 +243,13 @@ _zsh_autosuggest_bind_widgets() { # Given the name of an original widget and args, invoke it, if it exists _zsh_autosuggest_invoke_original_widget() { # Do nothing unless called with at least one arg - [ $# -gt 0 ] || return + (( $# )) || return 0 local original_widget_name="$1" shift - if [ $widgets[$original_widget_name] ]; then + if (( ${+widgets[$original_widget_name]} )); then zle $original_widget_name -- $@ fi } @@ -246,7 +262,7 @@ _zsh_autosuggest_invoke_original_widget() { _zsh_autosuggest_highlight_reset() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT - if [ -n "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT" ]; then + if [[ -n "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT" ]]; then region_highlight=("${(@)region_highlight:#$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT}") unset _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT fi @@ -256,7 +272,7 @@ _zsh_autosuggest_highlight_reset() { _zsh_autosuggest_highlight_apply() { typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT - if [ $#POSTDISPLAY -gt 0 ]; then + if (( $#POSTDISPLAY )); then typeset -g _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT="$#BUFFER $(($#BUFFER + $#POSTDISPLAY)) $ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" region_highlight+=("$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT") else @@ -278,14 +294,14 @@ _zsh_autosuggest_disable() { _zsh_autosuggest_enable() { unset _ZSH_AUTOSUGGEST_DISABLED - if [ $#BUFFER -gt 0 ]; then + if (( $#BUFFER )); then _zsh_autosuggest_fetch fi } # Toggle suggestions (enable/disable) _zsh_autosuggest_toggle() { - if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then + if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then _zsh_autosuggest_enable else _zsh_autosuggest_disable @@ -319,35 +335,36 @@ _zsh_autosuggest_modify() { retval=$? # Don't fetch a new suggestion if there's more input to be read immediately - if [[ $PENDING > 0 ]] || [[ $KEYS_QUEUED_COUNT > 0 ]]; then + if (( $PENDING > 0 )) || (( $KEYS_QUEUED_COUNT > 0 )); then + POSTDISPLAY="$orig_postdisplay" return $retval fi # Optimize if manually typing in the suggestion - if [ $#BUFFER -gt $#orig_buffer ]; then + if (( $#BUFFER > $#orig_buffer )); then local added=${BUFFER#$orig_buffer} # If the string added matches the beginning of the postdisplay - if [ "$added" = "${orig_postdisplay:0:$#added}" ]; then + if [[ "$added" = "${orig_postdisplay:0:$#added}" ]]; then POSTDISPLAY="${orig_postdisplay:$#added}" return $retval fi fi # Don't fetch a new suggestion if the buffer hasn't changed - if [ "$BUFFER" = "$orig_buffer" ]; then + if [[ "$BUFFER" = "$orig_buffer" ]]; then POSTDISPLAY="$orig_postdisplay" return $retval fi # Bail out if suggestions are disabled - if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then + if [[ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]]; then return $? fi # Get a new suggestion if the buffer is not empty after modification - if [ $#BUFFER -gt 0 ]; then - if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then + if (( $#BUFFER > 0 )); then + if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi fi @@ -370,7 +387,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { local suggestion="$1" - if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then + if [[ -n "$suggestion" ]] && (( $#BUFFER )); then POSTDISPLAY="${suggestion#$BUFFER}" else unset POSTDISPLAY @@ -389,12 +406,12 @@ _zsh_autosuggest_accept() { # Only accept if the cursor is at the end of the buffer if [[ $CURSOR = $max_cursor_pos ]]; then - # Accepting the whole line is basically a specific case of - # accepting partially with "end-of-line" widget - # NB : we can't directly call end-of-line because since we wrap it, - # it would cause a recursive call to it - # TODO : find a non-hardcoded way to call this widget - _zsh_autosuggest_partial_accept "autosuggest-orig-1-end-of-line" + # Accepting the whole line is basically a specific case of + # accepting partially with "end-of-line" widget + # NB : we can't directly call end-of-line because since we wrap it, + # it would cause a recursive call to it + # TODO : find a non-hardcoded way to call this widget + _zsh_autosuggest_partial_accept "autosuggest-orig-1-end-of-line" fi _zsh_autosuggest_invoke_original_widget $@ @@ -415,7 +432,7 @@ _zsh_autosuggest_execute() { # Partially accept the suggestion _zsh_autosuggest_partial_accept() { - local -i retval + local -i retval cursor_loc # Save the contents of the buffer so we can restore later if needed local original_buffer="$BUFFER" @@ -427,13 +444,19 @@ _zsh_autosuggest_partial_accept() { _zsh_autosuggest_invoke_original_widget $@ retval=$? + # Normalize cursor location across vi/emacs modes + cursor_loc=$CURSOR + if [[ "$KEYMAP" = "vicmd" ]]; then + cursor_loc=$((cursor_loc + 1)) + fi + # If we've moved past the end of the original buffer - if [ $CURSOR -gt $#original_buffer ]; then + if (( $cursor_loc > $#original_buffer )); then # Set POSTDISPLAY to text right of the cursor - POSTDISPLAY="$RBUFFER" + POSTDISPLAY="${BUFFER[$(($cursor_loc + 1)),$#BUFFER]}" # Clip the buffer at the cursor - BUFFER="$LBUFFER" + BUFFER="${BUFFER[1,$cursor_loc]}" else # Restore the original buffer BUFFER="$original_buffer" @@ -476,11 +499,21 @@ zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle # _zsh_autosuggest_strategy_default() { - local prefix="$1" + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB + + # Escape backslashes and all of the glob operators so we can use + # this string as a pattern to search the $history associative array. + # - (#m) globbing flag enables setting references for match data + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 + local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get the history items that match # - (r) subscript flag makes the pattern match on values - typeset -g suggestion="${history[(r)${(b)prefix}*]}" + typeset -g suggestion="${history[(r)${prefix}*]}" } #--------------------------------------------------------------------# @@ -505,18 +538,25 @@ _zsh_autosuggest_strategy_default() { # `HIST_EXPIRE_DUPS_FIRST`. _zsh_autosuggest_strategy_match_prev_cmd() { - local prefix="$1" + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh + + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB + + # TODO: Use (b) flag when we can drop support for zsh older than v5.0.8 + local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}" # Get all history event numbers that correspond to history # entries that match pattern $prefix* local history_match_keys - history_match_keys=(${(k)history[(R)${(b)prefix}*]}) + history_match_keys=(${(k)history[(R)$prefix*]}) # By default we use the first history number (most recent history entry) local histkey="${history_match_keys[1]}" # Get the previously executed command - local prev_cmd="${history[$((HISTCMD-1))]}" + local prev_cmd="$(_zsh_autosuggest_escape_command "${history[$((HISTCMD-1))]}")" # Iterate up to the first 200 history event numbers that match $prefix for key in "${(@)history_match_keys[1,200]}"; do @@ -553,13 +593,15 @@ _zsh_autosuggest_async_server() { sleep 1 # Block for long enough for the signal to come through } - # Output only newlines (not carriage return + newline) + # Don't add any extra carriage returns stty -onlcr + # Don't translate carriage returns to newlines + stty -icrnl + # Silence any error messages exec 2>/dev/null - local strategy=$1 local last_pid while IFS='' read -r -d $'\0' query; do @@ -599,14 +641,14 @@ _zsh_autosuggest_async_pty_create() { typeset -h REPLY # If we won't get a fd back from zpty, try to guess it - if [ $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD -eq 0 ]; then + if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); then integer -l zptyfd exec {zptyfd}>&1 # Open a new file descriptor (above 10). exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi # Fork a zpty process running the server function - zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "_zsh_autosuggest_async_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server # Store the fd so we can remove the handler later if (( REPLY )); then @@ -620,13 +662,11 @@ _zsh_autosuggest_async_pty_create() { } _zsh_autosuggest_async_pty_destroy() { - if zpty -t $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null; then - # Remove the input handler - zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null - # Destroy the zpty - zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null - fi + # Destroy the zpty + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null } _zsh_autosuggest_async_pty_recreate() { @@ -660,7 +700,7 @@ _zsh_autosuggest_start() { # to the widget list variables to take effect on the next precmd. add-zsh-hook precmd _zsh_autosuggest_bind_widgets - if [ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]; then + if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then _zsh_autosuggest_async_start fi }