diff --git a/CHANGELOG.md b/CHANGELOG.md index 036c1e3..5dc699f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v0.6.0 +- Added `completion` suggestion strategy powered by completion system (#111) +- Allow setting `ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE` to an empty string (#422) +- Don't fetch suggestions after copy-earlier-word (#439) +- Allow users to unignore zle-\* widgets (e.g. zle-line-init) (#432) + + ## v0.5.2 - Allow disabling automatic widget re-binding for better performance (#418) - Fix async suggestions when `SH_WORD_SPLIT` is set @@ -46,7 +53,7 @@ - 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 (#206) +- Avoid wrapping any zle-\* widgets (#206) - Remove support for deprecated options from v0.0.x - Handle history entries that begin with dashes - Gracefully handle being sourced multiple times (#126) diff --git a/LICENSE b/LICENSE index bcbc8b9..ef7cfb6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2013 Thiago de Arruda -Copyright (c) 2016-2018 Eric Freese +Copyright (c) 2016-2019 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 93b8d94..f6d13a7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ SRC_DIR := ./src SRC_FILES := \ - $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ $(SRC_DIR)/util.zsh \ $(SRC_DIR)/bind.zsh \ diff --git a/README.md b/README.md index f5a57e0..1dac45c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ _[Fish](http://fishshell.com/)-like fast/unobtrusive autosuggestions for zsh._ -It suggests commands as you type, based on command history. +It suggests commands as you type. Requirements: Zsh v4.3.11 or later @@ -34,15 +34,26 @@ You may want to override the default global config variables. Default values of ### Suggestion Highlight Style -Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion is shown with. The default is `fg=8`. +Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion is shown with. The default is `fg=8`, which will set the foreground color to color 8 from the 256-color palette. If your terminal only supports 8 colors, you will need to use a number between 0 and 7. + +Background color can also be set, and the suggestion can be styled bold, underlined, or standout. For example, this would show suggestions with bold, underlined, pink text on a cyan background: + +```sh +ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#ff00ff,bg=cyan,bold,underline" +``` + +For more info, read the Character Highlighting section of the zsh manual: `man zshzle` or [online](http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#Character-Highlighting). ### Suggestion Strategy -`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently two built-in strategies to choose from: +`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently three built-in strategies to choose from: - `history`: Chooses the most recent match from history. - `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches the most recently executed command ([more info](src/strategies/match_prev_cmd.zsh)). Note that this strategy won't work as expected with ZSH options that don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`. +- `completion`: (experimental) Chooses a suggestion based on what tab-completion would suggest. (requires `zpty` module) + +For example, setting `ZSH_AUTOSUGGEST_STRATEGY=(history completion)` will first try to find a suggestion from your history, but, if it can't find a match, will find a suggestion from the completion engine. ### Widget Mapping diff --git a/VERSION b/VERSION index b0c2058..60f6343 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.5.2 +v0.6.0 diff --git a/spec/async_spec.rb b/spec/async_spec.rb index c4029a1..0af7232 100644 --- a/spec/async_spec.rb +++ b/spec/async_spec.rb @@ -27,6 +27,29 @@ context 'with asynchronous suggestions enabled' do end end + describe '`copy-earlier-word`' do + let(:before_sourcing) do + -> do + session. + run_command('autoload -Uz copy-earlier-word'). + run_command('zle -N copy-earlier-word'). + run_command('bindkey "^N" copy-earlier-word') + end + end + + it 'should cycle through previous words in the buffer' do + session.clear_screen + session.send_string('foo bar baz') + sleep 0.5 + session.send_keys('C-n') + wait_for { session.content }.to eq('foo bar bazbaz') + session.send_keys('C-n') + wait_for { session.content }.to eq('foo bar bazbar') + session.send_keys('C-n') + wait_for { session.content }.to eq('foo bar bazfoo') + end + end + describe 'pressing ^C after fetching a suggestion' do before do skip 'Workaround does not work below v5.0.8' if session.zsh_version < Gem::Version.new('5.0.8') @@ -34,9 +57,9 @@ context 'with asynchronous suggestions enabled' do it 'terminates the prompt and begins a new one' do session.send_keys('e') - sleep 0.1 + sleep 0.5 session.send_keys('C-c') - sleep 0.1 + sleep 0.5 session.send_keys('echo') wait_for { session.content }.to eq("e\necho") diff --git a/spec/integrations/bracketed_paste_magic_spec.rb b/spec/integrations/bracketed_paste_magic_spec.rb index 64092ad..41ff267 100644 --- a/spec/integrations/bracketed_paste_magic_spec.rb +++ b/spec/integrations/bracketed_paste_magic_spec.rb @@ -24,4 +24,20 @@ describe 'pasting using bracketed-paste-magic' do end end end + + context 'with `bracketed-paste` added to the list of widgets that clear the suggestion' do + let(:options) { ['ZSH_AUTOSUGGEST_CLEAR_WIDGETS+=(bracketed-paste)'] } + + it 'does not retain an old suggestion' do + with_history ('echo foo') do + session.send_string('echo ') + wait_for { session.content }.to eq('echo foo') + session.paste_string('bar') + wait_for { session.content }.to eq('echo bar') + session.send_keys('C-a') # Any cursor movement works + sleep 1 + expect(session.content).to eq('echo bar') + end + end + end end diff --git a/spec/integrations/client_zpty_spec.rb b/spec/integrations/client_zpty_spec.rb new file mode 100644 index 0000000..b8abb37 --- /dev/null +++ b/spec/integrations/client_zpty_spec.rb @@ -0,0 +1,14 @@ +describe 'a running zpty command' do + let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } } + + context 'when using `completion` strategy' do + let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=completion"] } + + it 'is not affected' do + session.send_keys('a').send_keys('C-h') + session.run_command('zpty -t kitty; echo $?') + + wait_for { session.content }.to end_with("\n0") + end + end +end diff --git a/spec/line_init_spec.rb b/spec/line_init_spec.rb new file mode 100644 index 0000000..826277f --- /dev/null +++ b/spec/line_init_spec.rb @@ -0,0 +1,17 @@ +context 'with zle-line-init unignored' do + let(:after_sourcing) do + -> do + session. + run_command('setopt extendedglob'). + run_command('ZSH_AUTOSUGGEST_IGNORE_WIDGETS=(${(@)ZSH_AUTOSUGGEST_IGNORE_WIDGETS:#zle-\*} zle-\^line-init)'). + run_command('zle-line-init() { BUFFER="echo" }') + end + end + + it 'should fetch a suggestion on each line initialization' do + with_history('echo foo') do + session.run_command('zle -N zle-line-init') + wait_for { session.content }.to end_with('echo foo') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index abea917..cb149ef 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,8 +11,7 @@ RSpec.shared_context 'terminal session' do around do |example| before_sourcing.call - session.run_command(options.join('; ')) - session.run_command('source zsh-autosuggestions.zsh') + session.run_command(['source zsh-autosuggestions.zsh', *options].join('; ')) after_sourcing.call session.clear_screen diff --git a/spec/strategies/completion_spec.rb b/spec/strategies/completion_spec.rb new file mode 100644 index 0000000..e8cc8ce --- /dev/null +++ b/spec/strategies/completion_spec.rb @@ -0,0 +1,38 @@ +describe 'the `completion` suggestion strategy' do + let(:options) { ['ZSH_AUTOSUGGEST_STRATEGY=completion'] } + let(:before_sourcing) do + -> do + session. + run_command('autoload compinit && compinit'). + run_command('_foo() { compadd bar; compadd bat }'). + run_command('compdef _foo baz') + end + end + + it 'suggests the first completion result' do + session.send_string('baz ') + wait_for { session.content }.to eq('baz bar') + end + + it 'does not add extra carriage returns when prefix has a line feed' do + skip '`stty` does not work inside zpty below zsh version 5.0.3' if session.zsh_version < Gem::Version.new('5.0.3') + session.send_string('baz \\').send_keys('C-v', 'C-j') + wait_for { session.content }.to eq("baz \\\nbar") + end + + context 'when async mode is enabled' do + let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=completion'] } + + it 'suggests the first completion result' do + session.send_string('baz ') + wait_for { session.content }.to eq('baz bar') + end + + it 'does not add extra carriage returns when prefix has a line feed' do + skip '`stty` does not work inside zpty below zsh version 5.0.3' if session.zsh_version < Gem::Version.new('5.0.3') + session.send_string('baz \\').send_keys('C-v', 'C-j') + wait_for { session.content }.to eq("baz \\\nbar") + end + end +end + diff --git a/src/async.zsh b/src/async.zsh index b038cb0..d7e9fe8 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -56,9 +56,12 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { emulate -L zsh + local suggestion + if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + IFS='' read -rd '' -u $1 suggestion + zle autosuggest-suggest -- "$suggestion" # Close the fd exec {1}<&- diff --git a/src/bind.zsh b/src/bind.zsh index a2e86e1..fc2da9e 100644 --- a/src/bind.zsh +++ b/src/bind.zsh @@ -69,7 +69,6 @@ _zsh_autosuggest_bind_widgets() { ignore_widgets=( .\* _\* - zle-\* autosuggest-\* $ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX\* $ZSH_AUTOSUGGEST_IGNORE_WIDGETS diff --git a/src/config.zsh b/src/config.zsh index 2f46aed..5a0ebd8 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -35,6 +35,7 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- up-line-or-history down-line-or-history accept-line + copy-earlier-word ) } @@ -83,5 +84,10 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- which-command yank yank-pop + zle-\* ) } + +# Pty name for capturing completions for completion suggestion strategy +(( ! ${+ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME} )) && +typeset -g ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty diff --git a/src/fetch.zsh b/src/fetch.zsh index 1517018..fef2715 100644 --- a/src/fetch.zsh +++ b/src/fetch.zsh @@ -18,7 +18,10 @@ _zsh_autosuggest_fetch_suggestion() { # Try to get a suggestion from this strategy _zsh_autosuggest_strategy_$strategy "$1" - # Break once we've found a suggestion + # Ensure the suggestion matches the prefix + [[ "$suggestion" != "$1"* ]] && unset suggestion + + # Break once we've found a valid suggestion [[ -n "$suggestion" ]] && break done } diff --git a/src/setup.zsh b/src/setup.zsh deleted file mode 100644 index c74489f..0000000 --- a/src/setup.zsh +++ /dev/null @@ -1,10 +0,0 @@ - -#--------------------------------------------------------------------# -# Setup # -#--------------------------------------------------------------------# - -# Precmd hooks for initializing the library and starting pty's -autoload -Uz add-zsh-hook - -# Asynchronous suggestions are generated in a pty -zmodload zsh/zpty diff --git a/src/start.zsh b/src/start.zsh index 0125ab8..5991039 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -19,4 +19,5 @@ _zsh_autosuggest_start() { } # Start the autosuggestion widgets on the next precmd +autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start diff --git a/src/strategies/completion.zsh b/src/strategies/completion.zsh new file mode 100644 index 0000000..4c60e90 --- /dev/null +++ b/src/strategies/completion.zsh @@ -0,0 +1,130 @@ + +#--------------------------------------------------------------------# +# Completion Suggestion Strategy # +#--------------------------------------------------------------------# +# Fetches a suggestion from the completion engine +# + +_zsh_autosuggest_capture_postcompletion() { + # Always insert the first completion into the buffer + compstate[insert]=1 + + # Don't list completions + unset 'compstate[list]' +} + +_zsh_autosuggest_capture_completion_widget() { + # Add a post-completion hook to be called after all completions have been + # gathered. The hook can modify compstate to affect what is done with the + # gathered completions. + local -a +h comppostfuncs + comppostfuncs=(_zsh_autosuggest_capture_postcompletion) + + # Only capture completions at the end of the buffer + CURSOR=$#BUFFER + + # Run the original widget wrapping `.complete-word` so we don't + # recursively try to fetch suggestions, since our pty is forked + # after autosuggestions is initialized. + zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} + + if is-at-least 5.0.3; then + # Don't do any cr/lf transformations. We need to do this immediately before + # output because if we do it in setup, onlcr will be re-enabled when we enter + # vared in the async code path. There is a bug in zpty module in older versions + # where the tty is not properly attached to the pty slave, resulting in stty + # getting stopped with a SIGTTOU. See zsh-workers thread 31660 and upstream + # commit f75904a38 + stty -onlcr -ocrnl -F /dev/tty + fi + + # The completion has been added, print the buffer as the suggestion + echo -nE - $'\0'$BUFFER$'\0' +} + +zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget + +_zsh_autosuggest_capture_setup() { + autoload -Uz is-at-least + + # There is a bug in zpty module in older zsh versions by which a + # zpty that exits will kill all zpty processes that were forked + # before it. Here we set up a zsh exit hook to SIGKILL the zpty + # process immediately, before it has a chance to kill any other + # zpty processes. + if ! is-at-least 5.4; then + zshexit() { + # The zsh builtin `kill` fails sometimes in older versions + # https://unix.stackexchange.com/a/477647/156673 + kill -KILL $$ 2>&- || command kill -KILL $$ + + # Block for long enough for the signal to come through + sleep 1 + } + fi + + # Try to avoid any suggestions that wouldn't match the prefix + zstyle ':completion:*' matcher-list '' + zstyle ':completion:*' path-completion false + zstyle ':completion:*' max-errors 0 not-numeric + + bindkey '^I' autosuggest-capture-completion +} + +_zsh_autosuggest_capture_completion_sync() { + _zsh_autosuggest_capture_setup + + zle autosuggest-capture-completion +} + +_zsh_autosuggest_capture_completion_async() { + _zsh_autosuggest_capture_setup + + zmodload zsh/parameter 2>/dev/null || return # For `$functions` + + # Make vared completion work as if for a normal command line + # https://stackoverflow.com/a/7057118/154703 + autoload +X _complete + functions[_original_complete]=$functions[_complete] + _complete () { + unset 'compstate[vared]' + _original_complete "$@" + } + + # Open zle with buffer set so we can capture completions for it + vared 1 +} + +_zsh_autosuggest_strategy_completion() { + typeset -g suggestion + local line REPLY + + # Exit if we don't have completions + whence compdef >/dev/null || return + + # Exit if we don't have zpty + zmodload zsh/zpty 2>/dev/null || return + + # Zle will be inactive if we are in async mode + if zle; then + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync + else + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$1" + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' + fi + + { + # The completion result is surrounded by null bytes, so read the + # content between the first two null bytes. + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0' + + # Extract the suggestion from between the null bytes. On older + # versions of zsh (older than 5.3), we sometimes get extra bytes after + # the second null byte, so trim those off the end. + # See http://www.zsh.org/mla/workers/2015/msg03290.html + suggestion="${${line#*$'\0'}%$'\0'*}" + } always { + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME + } +} diff --git a/src/widgets.zsh b/src/widgets.zsh index 7fb1183..450ed3c 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -85,7 +85,7 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if (( $#BUFFER > 0 )); then - if (( ! ${+ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE} )) || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then + if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi fi diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 645d00f..f214147 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -1,6 +1,6 @@ # Fish-like fast/unobtrusive autosuggestions for zsh. # https://github.com/zsh-users/zsh-autosuggestions -# v0.5.2 +# v0.6.0 # Copyright (c) 2013 Thiago de Arruda # Copyright (c) 2016-2018 Eric Freese # @@ -25,16 +25,6 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -#--------------------------------------------------------------------# -# Setup # -#--------------------------------------------------------------------# - -# Precmd hooks for initializing the library and starting pty's -autoload -Uz add-zsh-hook - -# Asynchronous suggestions are generated in a pty -zmodload zsh/zpty - #--------------------------------------------------------------------# # Global Configuration Variables # #--------------------------------------------------------------------# @@ -71,6 +61,7 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- up-line-or-history down-line-or-history accept-line + copy-earlier-word ) } @@ -119,9 +110,14 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- which-command yank yank-pop + zle-\* ) } +# Pty name for capturing completions for completion suggestion strategy +(( ! ${+ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME} )) && +typeset -g ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty + #--------------------------------------------------------------------# # Utility Functions # #--------------------------------------------------------------------# @@ -203,7 +199,6 @@ _zsh_autosuggest_bind_widgets() { ignore_widgets=( .\* _\* - zle-\* autosuggest-\* $ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX\* $ZSH_AUTOSUGGEST_IGNORE_WIDGETS @@ -352,7 +347,7 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if (( $#BUFFER > 0 )); then - if (( ! ${+ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE} )) || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then + if [[ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]] || (( $#BUFFER <= $ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE )); then _zsh_autosuggest_fetch fi fi @@ -486,6 +481,136 @@ _zsh_autosuggest_partial_accept() { zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle } +#--------------------------------------------------------------------# +# Completion Suggestion Strategy # +#--------------------------------------------------------------------# +# Fetches a suggestion from the completion engine +# + +_zsh_autosuggest_capture_postcompletion() { + # Always insert the first completion into the buffer + compstate[insert]=1 + + # Don't list completions + unset 'compstate[list]' +} + +_zsh_autosuggest_capture_completion_widget() { + # Add a post-completion hook to be called after all completions have been + # gathered. The hook can modify compstate to affect what is done with the + # gathered completions. + local -a +h comppostfuncs + comppostfuncs=(_zsh_autosuggest_capture_postcompletion) + + # Only capture completions at the end of the buffer + CURSOR=$#BUFFER + + # Run the original widget wrapping `.complete-word` so we don't + # recursively try to fetch suggestions, since our pty is forked + # after autosuggestions is initialized. + zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]} + + if is-at-least 5.0.3; then + # Don't do any cr/lf transformations. We need to do this immediately before + # output because if we do it in setup, onlcr will be re-enabled when we enter + # vared in the async code path. There is a bug in zpty module in older versions + # where the tty is not properly attached to the pty slave, resulting in stty + # getting stopped with a SIGTTOU. See zsh-workers thread 31660 and upstream + # commit f75904a38 + stty -onlcr -ocrnl -F /dev/tty + fi + + # The completion has been added, print the buffer as the suggestion + echo -nE - $'\0'$BUFFER$'\0' +} + +zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget + +_zsh_autosuggest_capture_setup() { + autoload -Uz is-at-least + + # There is a bug in zpty module in older zsh versions by which a + # zpty that exits will kill all zpty processes that were forked + # before it. Here we set up a zsh exit hook to SIGKILL the zpty + # process immediately, before it has a chance to kill any other + # zpty processes. + if ! is-at-least 5.4; then + zshexit() { + # The zsh builtin `kill` fails sometimes in older versions + # https://unix.stackexchange.com/a/477647/156673 + kill -KILL $$ 2>&- || command kill -KILL $$ + + # Block for long enough for the signal to come through + sleep 1 + } + fi + + # Try to avoid any suggestions that wouldn't match the prefix + zstyle ':completion:*' matcher-list '' + zstyle ':completion:*' path-completion false + zstyle ':completion:*' max-errors 0 not-numeric + + bindkey '^I' autosuggest-capture-completion +} + +_zsh_autosuggest_capture_completion_sync() { + _zsh_autosuggest_capture_setup + + zle autosuggest-capture-completion +} + +_zsh_autosuggest_capture_completion_async() { + _zsh_autosuggest_capture_setup + + zmodload zsh/parameter 2>/dev/null || return # For `$functions` + + # Make vared completion work as if for a normal command line + # https://stackoverflow.com/a/7057118/154703 + autoload +X _complete + functions[_original_complete]=$functions[_complete] + _complete () { + unset 'compstate[vared]' + _original_complete "$@" + } + + # Open zle with buffer set so we can capture completions for it + vared 1 +} + +_zsh_autosuggest_strategy_completion() { + typeset -g suggestion + local line REPLY + + # Exit if we don't have completions + whence compdef >/dev/null || return + + # Exit if we don't have zpty + zmodload zsh/zpty 2>/dev/null || return + + # Zle will be inactive if we are in async mode + if zle; then + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync + else + zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$1" + zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t' + fi + + { + # The completion result is surrounded by null bytes, so read the + # content between the first two null bytes. + zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0' + + # Extract the suggestion from between the null bytes. On older + # versions of zsh (older than 5.3), we sometimes get extra bytes after + # the second null byte, so trim those off the end. + # See http://www.zsh.org/mla/workers/2015/msg03290.html + suggestion="${${line#*$'\0'}%$'\0'*}" + } always { + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME + } +} + #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# @@ -589,7 +714,10 @@ _zsh_autosuggest_fetch_suggestion() { # Try to get a suggestion from this strategy _zsh_autosuggest_strategy_$strategy "$1" - # Break once we've found a suggestion + # Ensure the suggestion matches the prefix + [[ "$suggestion" != "$1"* ]] && unset suggestion + + # Break once we've found a valid suggestion [[ -n "$suggestion" ]] && break done } @@ -651,9 +779,12 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { emulate -L zsh + local suggestion + if [[ -z "$2" || "$2" == "hup" ]]; then # Read everything from the fd and give it as a suggestion - zle autosuggest-suggest -- "$(cat <&$1)" + IFS='' read -rd '' -u $1 suggestion + zle autosuggest-suggest -- "$suggestion" # Close the fd exec {1}<&- @@ -683,4 +814,5 @@ _zsh_autosuggest_start() { } # Start the autosuggestion widgets on the next precmd +autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start