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..1435c99 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 @@ -39,10 +39,13 @@ Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion ### 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/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/strategies/completion_spec.rb b/spec/strategies/completion_spec.rb new file mode 100644 index 0000000..bd2c72d --- /dev/null +++ b/spec/strategies/completion_spec.rb @@ -0,0 +1,26 @@ +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 }'). + 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 + + 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 + end +end + diff --git a/src/config.zsh b/src/config.zsh index 2f46aed..1ab878b 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -85,3 +85,7 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- yank-pop ) } + +# 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..bf4ed9a --- /dev/null +++ b/src/strategies/completion.zsh @@ -0,0 +1,109 @@ + +#--------------------------------------------------------------------# +# 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() { + 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]} + + # 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() { + # 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() { + kill -KILL $$ + sleep 1 # Block for long enough for the signal to come through + } + 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' + + # On older versions of zsh, we sometimes get extra bytes after the + # second null byte, so trim those off the end + suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" + } always { + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME + } +} diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 645d00f..f6f6a84 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -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 # #--------------------------------------------------------------------# @@ -122,6 +112,10 @@ typeset -g ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- ) } +# 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 # #--------------------------------------------------------------------# @@ -486,6 +480,115 @@ _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() { + 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]} + + # 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() { + # 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() { + kill -KILL $$ + sleep 1 # Block for long enough for the signal to come through + } + 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' + + # On older versions of zsh, we sometimes get extra bytes after the + # second null byte, so trim those off the end + suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}" + } always { + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME + } +} + #--------------------------------------------------------------------# # History Suggestion Strategy # #--------------------------------------------------------------------# @@ -589,7 +692,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 } @@ -683,4 +789,5 @@ _zsh_autosuggest_start() { } # Start the autosuggestion widgets on the next precmd +autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start