Compare commits

...

10 Commits

Author SHA1 Message Date
Henré Botha
15bcfd7126 Don't fetch suggestions after copy-earlier-word
Like {up,down}-line-or-beginning-search, this widget relies on
`$LASTWIDGET` being set to function correctly on subsequent invocations.

When asynchronous suggestions are enabled, and the widget triggers a
suggestion to be fetched, `autosuggest-suggest` will be called and
$LASTWIDGET will be set to it.
2019-06-14 13:19:07 -06:00
Eric Freese
9ceeacc797 Try to make async ^C spec less flaky
Was intermittently failing, sleep for a little longer to increase
chances of false negatives.
2019-06-14 13:19:07 -06:00
Eric Freese
de266574d3 Merge pull request #434 from zsh-users/meta/improve-highlight-style-docs
Improve documentation for configuring highlight style
2019-05-29 01:40:20 -06:00
Eric Freese
48d04447cc Merge pull request #433 from zsh-users/fixes/ignore-empty-max-size
Allow setting max size to empty string (GitHub #422)
2019-05-29 01:39:24 -06:00
Eric Freese
b714f6667c Improve documentation for configuring highlight style
Supersedes PR #430 and gives additional info.

https://stackoverflow.com/questions/47310537/how-to-change-zsh-autosuggestions-color
2019-05-27 14:56:19 -06:00
Eric Freese
474c577f3c Allow setting max size to empty string (GitHub #422)
Some people have gotten used to setting ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE
to an empty string rather than leaving it unset. This lets them continue
to do that without getting any warnings (e.g. "bad math expression:
operand expected at end of string").
2019-05-27 14:18:21 -06:00
Eric Freese
e9d8231ba7 Merge pull request #401 from zsh-users/features/completion-suggestions
Features/completion suggestions
2019-04-15 12:55:35 -06:00
Eric Freese
ce6ee94f30 Add spec to demonstrate bracketed-paste-magic bug workaround
Add `bracketed-paste` to list of widgets that clear the suggestion as a
workaround for GitHub #351
2019-04-15 12:47:21 -06:00
Eric Freese
e7c7efe2e2 Set options immediately after sourcing the plugin before next precmd
So that you can access defaults to e.g. add elements to an existing
array.
2019-04-15 12:46:21 -06:00
Eric Freese
c1910348c7 Implement completion suggestion strategy (#111)
Based on https://github.com/Valodim/zsh-capture-completion

`zpty -r` with a pattern seems to have some funky behavior on older
versions, giving unpredictable results

Don't use `-s` option to `zmodload`. It is not available in zsh versions
older than 5.3

If running in sync mode and a completion takes a long time, the user can
^C out of it. We need to use `always` in the strategy function or the
pty will not be destroyed in this case and the next time we go to create
it, it will fail, making the shell unusable.

User can have many different completion styles set that will modify what
they've already typed. These styles will result in suggestions that
don't match what the user has already typed. We try our best to unset
some of the more problematic ones, but add some code to fetch to
invalidate suggestions that don't match what the user's already typed.
2019-04-11 09:53:55 -06:00
14 changed files with 336 additions and 32 deletions

View File

@@ -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 \

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
)
}
@@ -85,3 +86,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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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
)
}
@@ -122,6 +113,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 #
#--------------------------------------------------------------------#
@@ -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,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 +693,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 +790,5 @@ _zsh_autosuggest_start() {
}
# Start the autosuggestion widgets on the next precmd
autoload -Uz add-zsh-hook
add-zsh-hook precmd _zsh_autosuggest_start