diff --git a/.editorconfig b/.editorconfig
index 51c4765..ddabb17 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -8,3 +8,11 @@ indent_size = 4
[*.md]
indent_style = space
+
+[*.rb]
+indent_style = space
+indent_size = 2
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/.gitmodules b/.gitmodules
deleted file mode 100644
index b45eb46..0000000
--- a/.gitmodules
+++ /dev/null
@@ -1,6 +0,0 @@
-[submodule "vendor/shunit2"]
- path = vendor/shunit2
- url = https://github.com/kward/shunit2
-[submodule "vendor/stub.sh"]
- path = vendor/stub.sh
- url = https://github.com/ericfreese/stub.sh
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..43ae203
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,3 @@
+--color
+--require spec_helper
+--format documentation
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..9e0792f
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,30 @@
+# Rails:
+# Enabled: true
+
+AllCops:
+ TargetRubyVersion: 2.3
+ Include:
+ - '**/Rakefile'
+ - '**/config.ru'
+ - '**/Gemfile'
+
+Metrics/LineLength:
+ Max: 120
+
+Style/Documentation:
+ Enabled: false
+
+Style/DotPosition:
+ EnforcedStyle: trailing
+
+Style/FrozenStringLiteralComment:
+ Enabled: false
+
+Style/Lambda:
+ Enabled: false
+
+Style/MultilineMethodCallIndentation:
+ EnforcedStyle: indented
+
+Style/TrailingUnderscoreVariable:
+ Enabled: false
diff --git a/.ruby-version b/.ruby-version
new file mode 100644
index 0000000..2bf1c1c
--- /dev/null
+++ b/.ruby-version
@@ -0,0 +1 @@
+2.3.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50a1e0a..ac0f8e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
# Changelog
+## 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 (#)
+- Optimize case where manually typing in suggestion
+- Avoid wrapping any zle-* widgets (#)
+- Remove support for deprecated options from v0.0.x
+- Handle history entries that begin with dashes (#)
+- Gracefully handle being sourced multiple times (#126)
+- Add enable/disable/toggle widgets to disable/enable suggestions (#219)
+
+
## v0.3.3
- Switch from $history array to fc builtin for better performance with large HISTFILEs (#164)
- Fix tilde handling when extended_glob is set (#168)
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..8b5deec
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,5 @@
+source 'https://rubygems.org'
+
+gem 'rspec'
+gem 'rspec-wait'
+gem 'pry-byebug'
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 0000000..63ee778
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,41 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ byebug (9.0.5)
+ coderay (1.1.1)
+ diff-lcs (1.3)
+ method_source (0.8.2)
+ pry (0.10.4)
+ coderay (~> 1.1.0)
+ method_source (~> 0.8.1)
+ slop (~> 3.4)
+ pry-byebug (3.4.0)
+ byebug (~> 9.0)
+ pry (~> 0.10)
+ rspec (3.5.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-core (3.5.4)
+ rspec-support (~> 3.5.0)
+ rspec-expectations (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-mocks (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-support (3.5.0)
+ rspec-wait (0.0.9)
+ rspec (>= 3, < 4)
+ slop (3.6.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ pry-byebug
+ rspec
+ rspec-wait
+
+BUNDLED WITH
+ 1.13.6
diff --git a/LICENSE b/LICENSE
index ee52ee2..ad6594e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
Copyright (c) 2013 Thiago de Arruda
-Copyright (c) 2016 Eric Freese
+Copyright (c) 2016-2017 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 fde3691..d5d162c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,14 +1,15 @@
SRC_DIR := ./src
-VENDOR_DIR := ./vendor
SRC_FILES := \
+ $(SRC_DIR)/setup.zsh \
$(SRC_DIR)/config.zsh \
- $(SRC_DIR)/deprecated.zsh \
+ $(SRC_DIR)/util.zsh \
+ $(SRC_DIR)/features.zsh \
$(SRC_DIR)/bind.zsh \
$(SRC_DIR)/highlight.zsh \
$(SRC_DIR)/widgets.zsh \
- $(SRC_DIR)/suggestion.zsh \
$(SRC_DIR)/strategies/*.zsh \
+ $(SRC_DIR)/async.zsh \
$(SRC_DIR)/start.zsh
HEADER_FILES := \
@@ -19,29 +20,17 @@ HEADER_FILES := \
PLUGIN_TARGET := zsh-autosuggestions.zsh
-SHUNIT2 := $(VENDOR_DIR)/shunit2/2.1.6
-STUB_SH := $(VENDOR_DIR)/stub.sh/stub.sh
-
-TEST_PREREQS := \
- $(SHUNIT2) \
- $(STUB_SH)
-
all: $(PLUGIN_TARGET)
$(PLUGIN_TARGET): $(HEADER_FILES) $(SRC_FILES)
cat $(HEADER_FILES) | sed -e 's/^/# /g' > $@
cat $(SRC_FILES) >> $@
-$(SHUNIT2):
- git submodule update --init vendor/shunit2
-
-$(STUB_SH):
- git submodule update --init vendor/stub.sh
-
.PHONY: clean
clean:
rm $(PLUGIN_TARGET)
.PHONY: test
-test: all $(TEST_PREREQS)
- script/test_runner.zsh $(TESTS)
+test: all
+ @test -n "$$TEST_ZSH_BIN" && echo "Testing zsh binary: $(TEST_ZSH_BIN)" || true
+ bundle exec rspec $(TESTS)
diff --git a/README.md b/README.md
index 3a5c3f3..ab5c82c 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.
+[![CircleCI](https://circleci.com/gh/zsh-users/zsh-autosuggestions.svg?style=svg)](https://circleci.com/gh/zsh-users/zsh-autosuggestions)
+
@@ -92,14 +94,22 @@ Widgets that modify the buffer and are not found in any of these arrays will fet
Set `ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE` to an integer value to disable autosuggestion for large buffers. The default is unset, which means that autosuggestion will be tried for any buffer size. Recommended value is 20.
This can be useful when pasting large amount of text in the terminal, to avoid triggering autosuggestion for too long strings.
+### Enable Asynchronous Mode
+
+As of `v0.4.0`, suggestions can be fetched asynchronously using the `zsh/zpty` module. To enable this behavior, set the `ZSH_AUTOSUGGEST_USE_ASYNC` variable (it can be set to anything).
+
### Key Bindings
-This plugin provides three widgets that you can use with `bindkey`:
+This plugin provides a few widgets that you can use with `bindkey`:
1. `autosuggest-accept`: Accepts the current suggestion.
2. `autosuggest-execute`: Accepts and executes the current suggestion.
3. `autosuggest-clear`: Clears the current suggestion.
+4. `autosuggest-fetch`: Fetches a suggestion (works even when suggestions are disabled).
+5. `autosuggest-disable`: Disables suggestions.
+6. `autosuggest-enable`: Re-enables suggestions.
+7. `autosuggest-toggle`: Toggles between enabled/disabled suggestions.
For example, this would bind ctrl + space to accept the current suggestion.
@@ -154,9 +164,9 @@ Pull requests are welcome! If you send a pull request, please:
### Testing
-Testing is performed with [`shunit2`](https://github.com/kward/shunit2) (v2.1.6). Documentation can be found [here](http://shunit2.googlecode.com/svn/trunk/source/2.1/doc/shunit2.html).
+Tests are written in ruby using the [`rspec`](http://rspec.info/) framework. They use [`tmux`](https://tmux.github.io/) to drive a pseudoterminal, sending simulated keystrokes and making assertions on the terminal content.
-The test script lives at `script/test_runner.zsh`. To run the tests, run `make test`.
+Test files live in `spec/`. To run the tests, run `make test`. To run a specific test, run `TESTS=spec/some_spec.rb make test`. You can also specify a `zsh` binary to use by setting the `TEST_ZSH_BIN` environment variable (ex: `TEST_ZSH_BIN=/bin/zsh make test`).
## License
diff --git a/VERSION b/VERSION
index 600e6fd..fb7a04c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-v0.3.3
+v0.4.0
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..5e3a6f6
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,12 @@
+machine:
+ environment:
+ ZSH_VERSIONS: 5.0.8 5.1.1 5.2 5.3.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
+
+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:
+ parallel: true
diff --git a/script/test_runner.zsh b/script/test_runner.zsh
deleted file mode 100755
index 0ff4173..0000000
--- a/script/test_runner.zsh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env zsh
-
-DIR="${0:a:h}"
-ROOT_DIR="$DIR/.."
-TEST_DIR="$ROOT_DIR/test"
-
-header() {
- local message="$1"
-
- cat <<-EOF
-
-#====================================================================#
-# $message
-#====================================================================#
- EOF
-}
-
-# ZSH binary to use
-local zsh_bin="zsh"
-
-while getopts ":z:" opt; do
- case $opt in
- z)
- zsh_bin="$OPTARG"
- ;;
- \?)
- echo "Invalid option: -$OPTARG" >&2
- exit 1
- ;;
- :)
- echo "Option -$OPTARG requires an argument" >&2
- exit 1
- ;;
- esac
-done
-
-shift $((OPTIND -1))
-
-# Test suites to run
-local -a tests
-if [ $#@ -gt 0 ]; then
- tests=($@)
-else
- tests=($TEST_DIR/**/*_test.zsh)
-fi
-
-local -i retval=0
-
-for suite in $tests; do
- header "${suite#"$ROOT_DIR/"}"
- "$zsh_bin" -f "$suite" || retval=$?
-done
-
-exit $retval
diff --git a/spec/integrations/bracketed_paste_magic_spec.rb b/spec/integrations/bracketed_paste_magic_spec.rb
new file mode 100644
index 0000000..f01b0e0
--- /dev/null
+++ b/spec/integrations/bracketed_paste_magic_spec.rb
@@ -0,0 +1,36 @@
+describe 'pasting using bracketed-paste-magic' do
+ let(:before_sourcing) do
+ -> do
+ session.
+ run_command('autoload -Uz bracketed-paste-magic').
+ run_command('zle -N bracketed-paste bracketed-paste-magic')
+ end
+ end
+
+ context 'with suggestions disabled while pasting' do
+ before do
+ session.
+ run_command('bpm_init() { zle autosuggest-disable }').
+ run_command('bpm_finish() { zle autosuggest-enable }').
+ run_command('zstyle :bracketed-paste-magic paste-init bpm_init').
+ run_command('zstyle :bracketed-paste-magic paste-finish bpm_finish')
+ end
+
+ it 'does not show an incorrect suggestion' do
+ with_history('echo hello') do
+ session.paste_string("echo #{'a' * 60}")
+ sleep 1
+ expect(session.content).to eq("echo #{'a' * 60}")
+ end
+ end
+
+ it 'shows a suggestion after a non-modifying keystroke' do
+ with_history('echo hello') do
+ session.paste_string('echo')
+ sleep 1
+ session.send_keys('left')
+ wait_for { session.content }.to eq('echo hello')
+ 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..8f1550e
--- /dev/null
+++ b/spec/integrations/client_zpty_spec.rb
@@ -0,0 +1,10 @@
+describe 'a running zpty command' do
+ let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } }
+
+ it 'is not affected by running zsh-autosuggestions' do
+ sleep 1 # Give a little time for precmd hooks to run
+ session.run_command('zpty -t kitty; echo $?')
+
+ wait_for { session.content }.to end_with("\n0")
+ end
+end
diff --git a/spec/integrations/wrapped_widget_spec.rb b/spec/integrations/wrapped_widget_spec.rb
new file mode 100644
index 0000000..61dfc2d
--- /dev/null
+++ b/spec/integrations/wrapped_widget_spec.rb
@@ -0,0 +1,39 @@
+describe 'a wrapped widget' do
+ let(:widget) { 'backward-delete-char' }
+
+ context 'initialized before sourcing the plugin' do
+ let(:before_sourcing) do
+ -> do
+ session.
+ run_command("_orig_#{widget}() { zle .#{widget} }").
+ run_command("zle -N orig-#{widget} _orig_#{widget}").
+ run_command("#{widget}-magic() { zle orig-#{widget}; BUFFER+=b }").
+ run_command("zle -N #{widget} #{widget}-magic")
+ end
+ end
+
+ it 'executes the custom behavior and the built-in behavior' do
+ with_history('foobar', 'foodar') do
+ session.send_string('food').send_keys('C-h')
+ wait_for { session.content }.to eq('foobar')
+ end
+ end
+ end
+
+ context 'initialized after sourcing the plugin' do
+ before do
+ session.
+ run_command("zle -N orig-#{widget} ${widgets[#{widget}]#*:}").
+ run_command("#{widget}-magic() { zle orig-#{widget}; BUFFER+=b }").
+ run_command("zle -N #{widget} #{widget}-magic").
+ clear_screen
+ end
+
+ it 'executes the custom behavior and the built-in behavior' do
+ with_history('foobar', 'foodar') do
+ session.send_string('food').send_keys('C-h')
+ wait_for { session.content }.to eq('foobar')
+ end
+ end
+ end
+end
diff --git a/spec/integrations/zle_input_stack_spec.rb b/spec/integrations/zle_input_stack_spec.rb
new file mode 100644
index 0000000..8a2c990
--- /dev/null
+++ b/spec/integrations/zle_input_stack_spec.rb
@@ -0,0 +1,24 @@
+describe 'using `zle -U`' do
+ let(:before_sourcing) do
+ -> do
+ session.
+ run_command('_zsh_autosuggest_strategy_test() { sleep 1; _zsh_autosuggest_strategy_default "$1" }').
+ run_command('foo() { zle -U - "echo hello" }; zle -N foo; bindkey ^B foo')
+ end
+ end
+
+ let(:options) { ['unset ZSH_AUTOSUGGEST_USE_ASYNC', 'ZSH_AUTOSUGGEST_STRATEGY=test'] }
+
+ # TODO: This is only possible with the $KEYS_QUEUED_COUNT widget parameter, coming soon...
+ xit 'does not fetch a suggestion for every inserted character' do
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('echo hello')
+ end
+
+ it 'shows a suggestion when the widget completes' do
+ with_history('echo hello world') do
+ session.send_keys('C-b')
+ wait_for { session.content(esc_seqs: true) }.to match(/\Aecho hello\e\[[0-9]+m world/)
+ end
+ end
+end
diff --git a/spec/multi_line_spec.rb b/spec/multi_line_spec.rb
new file mode 100644
index 0000000..4ff2ae1
--- /dev/null
+++ b/spec/multi_line_spec.rb
@@ -0,0 +1,13 @@
+describe 'a multi-line suggestion' do
+ it 'should be displayed on multiple lines' do
+ with_history(-> {
+ session.send_string('echo "')
+ session.send_keys('enter')
+ session.send_string('"')
+ session.send_keys('enter')
+ }) do
+ session.send_keys('e')
+ wait_for { session.content }.to eq("echo \"\n\"")
+ end
+ end
+end
diff --git a/spec/options/async_zpty_name_spec.rb b/spec/options/async_zpty_name_spec.rb
new file mode 100644
index 0000000..407ee70
--- /dev/null
+++ b/spec/options/async_zpty_name_spec.rb
@@ -0,0 +1,19 @@
+context 'when async suggestions are enabled' do
+ let(:options) { ["ZSH_AUTOSUGGEST_USE_ASYNC="] }
+
+ describe 'the zpty for async suggestions' do
+ it 'is created with the default name' do
+ session.run_command('zpty -t zsh_autosuggest_pty &>/dev/null; echo $?')
+ wait_for { session.content }.to end_with("\n0")
+ end
+
+ context 'when ZSH_AUTOSUGGEST_ASYNC_PTY_NAME is set' do
+ let(:options) { super() + ['ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=foo_pty'] }
+
+ it 'is created with the specified name' do
+ session.run_command('zpty -t foo_pty &>/dev/null; echo $?')
+ wait_for { session.content }.to end_with("\n0")
+ end
+ end
+ end
+end
diff --git a/spec/options/buffer_max_size_spec.rb b/spec/options/buffer_max_size_spec.rb
new file mode 100644
index 0000000..29ca8bc
--- /dev/null
+++ b/spec/options/buffer_max_size_spec.rb
@@ -0,0 +1,30 @@
+describe 'a suggestion' do
+ let(:term_opts) { { width: 200 } }
+ let(:long_command) { "echo #{'a' * 100}" }
+
+ around do |example|
+ with_history(long_command) { example.run }
+ end
+
+ it 'is provided for any buffer length' do
+ session.send_string(long_command[0...-1])
+ wait_for { session.content }.to eq(long_command)
+ end
+
+ context 'when ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE is specified' do
+ let(:buffer_max_size) { 10 }
+ let(:options) { ["ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=#{buffer_max_size}"] }
+
+ it 'is provided when the buffer is shorter than the specified length' do
+ session.send_string(long_command[0...(buffer_max_size - 1)])
+ wait_for { session.content }.to eq(long_command)
+ end
+
+ it 'is provided when the buffer is equal to the specified length' do
+ session.send_string(long_command[0...(buffer_max_size)])
+ wait_for { session.content }.to eq(long_command)
+ end
+
+ it 'is not provided when the buffer is longer than the specified length'
+ end
+end
diff --git a/spec/options/highlight_style_spec.rb b/spec/options/highlight_style_spec.rb
new file mode 100644
index 0000000..a7e39b3
--- /dev/null
+++ b/spec/options/highlight_style_spec.rb
@@ -0,0 +1,7 @@
+describe 'a displayed suggestion' do
+ it 'is shown in the default style'
+
+ describe 'when ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE is set to a zle_highlight string' do
+ it 'is shown in the specified style'
+ end
+end
diff --git a/spec/options/original_widget_prefix_spec.rb b/spec/options/original_widget_prefix_spec.rb
new file mode 100644
index 0000000..a4b6e98
--- /dev/null
+++ b/spec/options/original_widget_prefix_spec.rb
@@ -0,0 +1,7 @@
+describe 'an original zle widget' do
+ context 'is accessible with the default prefix'
+
+ context 'when ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX is set' do
+ it 'is accessible with the specified prefix'
+ end
+end
diff --git a/spec/options/strategy_spec.rb b/spec/options/strategy_spec.rb
new file mode 100644
index 0000000..c9f01e1
--- /dev/null
+++ b/spec/options/strategy_spec.rb
@@ -0,0 +1,20 @@
+describe 'a suggestion for a given prefix' do
+ let(:options) { ['_zsh_autosuggest_strategy_default() { suggestion="echo foo" }'] }
+
+ it 'is determined by calling the default strategy function' do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo foo')
+ end
+
+ context 'when ZSH_AUTOSUGGEST_STRATEGY is set' do
+ let(:options) { [
+ '_zsh_autosuggest_strategy_custom() { suggestion="echo foo" }',
+ 'ZSH_AUTOSUGGEST_STRATEGY=custom'
+ ] }
+
+ it 'is determined by calling the specified strategy function' do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo foo')
+ end
+ end
+end
diff --git a/spec/options/use_async_spec.rb b/spec/options/use_async_spec.rb
new file mode 100644
index 0000000..420dcc3
--- /dev/null
+++ b/spec/options/use_async_spec.rb
@@ -0,0 +1,7 @@
+describe 'suggestion fetching' do
+ it 'is performed synchronously'
+
+ context 'when ZSH_AUTOSUGGEST_USE_ASYNC is set' do
+ it 'is performed asynchronously'
+ end
+end
diff --git a/spec/options/widget_lists_spec.rb b/spec/options/widget_lists_spec.rb
new file mode 100644
index 0000000..c207c0c
--- /dev/null
+++ b/spec/options/widget_lists_spec.rb
@@ -0,0 +1,97 @@
+describe 'a zle widget' do
+ let(:widget) { 'my-widget' }
+ let(:before_sourcing) { -> { session.run_command("#{widget}() {}; zle -N #{widget}; bindkey ^B #{widget}") } }
+
+ context 'when added to ZSH_AUTOSUGGEST_ACCEPT_WIDGETS' do
+ let(:options) { ["ZSH_AUTOSUGGEST_ACCEPT_WIDGETS+=(#{widget})"] }
+
+ it 'accepts the suggestion when invoked' do
+ with_history('echo hello') do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo hello')
+ session.send_keys('C-b')
+ wait_for { session.content(esc_seqs: true) }.to eq('echo hello')
+ end
+ end
+ end
+
+ context 'when added to ZSH_AUTOSUGGEST_CLEAR_WIDGETS' do
+ let(:options) { ["ZSH_AUTOSUGGEST_CLEAR_WIDGETS+=(#{widget})"] }
+
+ it 'clears the suggestion when invoked' do
+ with_history('echo hello') do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo hello')
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('e')
+ end
+ end
+ end
+
+ context 'when added to ZSH_AUTOSUGGEST_EXECUTE_WIDGETS' do
+ let(:options) { ["ZSH_AUTOSUGGEST_EXECUTE_WIDGETS+=(#{widget})"] }
+
+ it 'executes the suggestion when invoked' do
+ with_history('echo hello') do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo hello')
+ session.send_keys('C-b')
+ wait_for { session.content }.to end_with("\nhello")
+ end
+ end
+ end
+
+ context 'when added to ZSH_AUTOSUGGEST_IGNORE_WIDGETS' do
+ let(:options) { ["ZSH_AUTOSUGGEST_IGNORE_WIDGETS=(#{widget})"] }
+
+ it 'should not be wrapped with an autosuggest widget' do
+ session.run_command("echo $widgets[#{widget}]")
+ wait_for { session.content }.to end_with("\nuser:#{widget}")
+ end
+ end
+
+ context 'that moves the cursor forward' do
+ before { session.run_command("#{widget}() { zle forward-char }") }
+
+ context 'when added to ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS' do
+ let(:options) { ["ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS=(#{widget})"] }
+
+ it 'accepts the suggestion as far as the cursor is moved when invoked' do
+ with_history('echo hello') do
+ session.send_string('e')
+ wait_for { session.content }.to start_with('echo hello')
+ session.send_keys('C-b')
+ wait_for { session.content(esc_seqs: true) }.to match(/\Aec\e\[[0-9]+mho hello/)
+ end
+ end
+ end
+ end
+
+ context 'that modifies the buffer' do
+ before { session.run_command("#{widget}() { BUFFER=\"foo\" }") }
+
+ context 'when not added to any of the widget lists' do
+ it 'modifies the buffer and fetches a new suggestion' do
+ with_history('foobar') do
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('foobar')
+ end
+ end
+ end
+ end
+end
+
+describe 'a modification to the widget lists' do
+ let(:widget) { 'my-widget' }
+ let(:before_sourcing) { -> { session.run_command("#{widget}() {}; zle -N #{widget}; bindkey ^B #{widget}") } }
+ before { session.run_command("ZSH_AUTOSUGGEST_ACCEPT_WIDGETS+=(#{widget})") }
+
+ it 'takes effect on the next cmd line' do
+ with_history('echo hello') do
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo hello')
+ session.send_keys('C-b')
+ wait_for { session.content(esc_seqs: true) }.to eq('echo hello')
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..64115d2
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,50 @@
+require 'pry'
+require 'rspec/wait'
+require 'terminal_session'
+
+RSpec.shared_context 'terminal session' do
+ let(:term_opts) { {} }
+ let(:session) { TerminalSession.new(term_opts) }
+ let(:before_sourcing) { -> {} }
+ let(:options) { [] }
+
+ around do |example|
+ before_sourcing.call
+
+ session.run_command((['source zsh-autosuggestions.zsh'] + options).join('; '))
+ session.clear_screen
+
+ example.run
+
+ session.destroy
+ end
+
+ def with_history(*commands, &block)
+ session.run_command('fc -p')
+
+ commands.each do |c|
+ c.respond_to?(:call) ? c.call : session.run_command(c)
+ end
+
+ session.clear_screen
+
+ yield block
+
+ session.send_keys('C-c')
+ session.run_command('fc -P')
+ end
+end
+
+RSpec.configure do |config|
+ config.expect_with :rspec do |expectations|
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
+ end
+
+ config.mock_with :rspec do |mocks|
+ mocks.verify_partial_doubles = true
+ end
+
+ config.wait_timeout = 2
+
+ config.include_context 'terminal session'
+end
diff --git a/spec/special_characters_spec.rb b/spec/special_characters_spec.rb
new file mode 100644
index 0000000..ce7810b
--- /dev/null
+++ b/spec/special_characters_spec.rb
@@ -0,0 +1,60 @@
+describe 'a special character in the buffer' do
+ it 'should be treated like any other character' do
+ with_history('echo "hello*"', 'echo "hello."') do
+ session.send_string('echo "hello*')
+ wait_for { session.content }.to eq('echo "hello*"')
+ end
+
+ with_history('echo "hello?"', 'echo "hello."') do
+ session.send_string('echo "hello?')
+ wait_for { session.content }.to eq('echo "hello?"')
+ end
+
+ with_history('echo "hello\nworld"') do
+ session.send_string('echo "hello\\')
+ wait_for { session.content }.to eq('echo "hello\nworld"')
+ end
+
+ with_history('echo "\\\\"') do
+ session.send_string('echo "\\\\')
+ wait_for { session.content }.to eq('echo "\\\\"')
+ end
+
+ with_history('echo ~/foo') do
+ session.send_string('echo ~')
+ wait_for { session.content }.to eq('echo ~/foo')
+ end
+
+ with_history('echo "$(ls foo)"') do
+ session.send_string('echo "$(')
+ wait_for { session.content }.to eq('echo "$(ls foo)"')
+ end
+
+ with_history('echo "$history[123]"') do
+ session.send_string('echo "$history[')
+ wait_for { session.content }.to eq('echo "$history[123]"')
+ session.send_string('123]')
+ wait_for { session.content }.to eq('echo "$history[123]"')
+ end
+
+ with_history('echo "#yolo"') do
+ session.send_string('echo "#')
+ wait_for { session.content }.to eq('echo "#yolo"')
+ end
+
+ with_history('echo "#foo"', 'echo $#abc') do
+ session.send_string('echo "#')
+ wait_for { session.content }.to eq('echo "#foo"')
+ end
+
+ with_history('echo "^A"', 'echo "^B"') do
+ session.send_string('echo "^A')
+ wait_for { session.content }.to eq('echo "^A"')
+ end
+
+ with_history('-foo() {}') do
+ session.send_string('-')
+ wait_for { session.content }.to eq('-foo() {}')
+ end
+ end
+end
diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb
new file mode 100644
index 0000000..94f3450
--- /dev/null
+++ b/spec/strategies/default_spec.rb
@@ -0,0 +1,8 @@
+describe 'the default suggestion strategy' do
+ it 'suggests the last matching history entry' do
+ with_history('ls foo', 'ls bar', 'echo baz') do
+ session.send_string('ls')
+ wait_for { session.content }.to eq('ls bar')
+ end
+ end
+end
diff --git a/spec/strategies/match_prev_cmd_spec.rb b/spec/strategies/match_prev_cmd_spec.rb
new file mode 100644
index 0000000..21be712
--- /dev/null
+++ b/spec/strategies/match_prev_cmd_spec.rb
@@ -0,0 +1,17 @@
+describe 'the match_prev_cmd strategy' do
+ let(:options) { ['ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd'] }
+
+ it 'suggests the last matching history entry after the previous command' do
+ with_history(
+ 'echo what',
+ 'ls foo',
+ 'echo what',
+ 'ls bar',
+ 'ls baz',
+ 'echo what'
+ ) do
+ session.send_string('ls')
+ wait_for { session.content }.to eq('ls bar')
+ end
+ end
+end
diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb
new file mode 100644
index 0000000..82705d3
--- /dev/null
+++ b/spec/terminal_session.rb
@@ -0,0 +1,91 @@
+require 'securerandom'
+
+class TerminalSession
+ ZSH_BIN = ENV['TEST_ZSH_BIN'] || 'zsh'
+
+ def initialize(opts = {})
+ opts = {
+ width: 80,
+ height: 24,
+ prompt: '',
+ term: 'xterm-256color',
+ zsh_bin: ZSH_BIN
+ }.merge(opts)
+
+ @opts = opts
+
+ cmd="PS1=\"#{opts[:prompt]}\" TERM=#{opts[:term]} #{ZSH_BIN} -f"
+ tmux_command("new-session -d -x #{opts[:width]} -y #{opts[:height]} '#{cmd}'")
+ end
+
+ def tmux_socket_name
+ @tmux_socket_name ||= SecureRandom.hex(6)
+ end
+
+ def run_command(command)
+ send_string(command)
+ send_keys('enter')
+
+ self
+ end
+
+ def send_string(str)
+ tmux_command("send-keys -t 0 -l -- '#{str.gsub("'", "\\'")}'")
+
+ self
+ end
+
+ def send_keys(*keys)
+ tmux_command("send-keys -t 0 #{keys.join(' ')}")
+
+ self
+ end
+
+ def paste_string(str)
+ tmux_command("set-buffer -- '#{str}'")
+ tmux_command("paste-buffer -dpr -t 0")
+
+ self
+ end
+
+ def content(esc_seqs: false)
+ cmd = 'capture-pane -p -t 0'
+ cmd += ' -e' if esc_seqs
+ tmux_command(cmd).strip
+ end
+
+ def clear_screen
+ send_keys('C-l')
+
+ i = 0
+ until content == opts[:prompt] || i > 20 do
+ sleep(0.1)
+ i = i + 1
+ end
+
+ self
+ end
+
+ def destroy
+ tmux_command('kill-session')
+ end
+
+ def cursor
+ tmux_command("display-message -t 0 -p '\#{cursor_x},\#{cursor_y}'").
+ strip.
+ split(',').
+ map(&:to_i)
+ end
+
+ private
+
+ attr_reader :opts
+
+ def tmux_command(cmd)
+ out = `tmux -u -L #{tmux_socket_name} #{cmd}`
+
+ raise("tmux error running: '#{cmd}'") unless $?.success?
+
+ out
+ end
+end
diff --git a/spec/widgets/disable_spec.rb b/spec/widgets/disable_spec.rb
new file mode 100644
index 0000000..b387a59
--- /dev/null
+++ b/spec/widgets/disable_spec.rb
@@ -0,0 +1,19 @@
+describe 'the `autosuggest-disable` widget' do
+ before do
+ session.run_command('bindkey ^B autosuggest-disable')
+ end
+
+ it 'disables suggestions and clears the suggestion' do
+ with_history('echo hello') do
+ session.send_string('echo')
+ wait_for { session.content }.to eq('echo hello')
+
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('echo')
+
+ session.send_string(' h')
+ sleep 1
+ expect(session.content).to eq('echo h')
+ end
+ end
+end
diff --git a/spec/widgets/enable_spec.rb b/spec/widgets/enable_spec.rb
new file mode 100644
index 0000000..3ad35a8
--- /dev/null
+++ b/spec/widgets/enable_spec.rb
@@ -0,0 +1,42 @@
+describe 'the `autosuggest-enable` widget' do
+ before do
+ session.
+ run_command('typeset -g _ZSH_AUTOSUGGEST_DISABLED').
+ run_command('bindkey ^B autosuggest-enable')
+ end
+
+ it 'enables suggestions and fetches a suggestion' do
+ with_history('echo hello') do
+ session.send_string('e')
+ sleep 1
+ expect(session.content).to eq('e')
+
+ session.send_keys('C-b')
+ session.send_string('c')
+ wait_for { session.content }.to eq('echo hello')
+ end
+ end
+
+ context 'invoked on an empty buffer' do
+ it 'does not fetch a suggestion' do
+ with_history('echo hello') do
+ session.send_keys('C-b')
+ sleep 1
+ expect(session.content).to eq('')
+ end
+ end
+ end
+
+ context 'invoked on a non-empty buffer' do
+ it 'fetches a suggestion' do
+ with_history('echo hello') do
+ session.send_string('e')
+ sleep 1
+ expect(session.content).to eq('e')
+
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('echo hello')
+ end
+ end
+ end
+end
diff --git a/spec/widgets/fetch_spec.rb b/spec/widgets/fetch_spec.rb
new file mode 100644
index 0000000..eb8f2ba
--- /dev/null
+++ b/spec/widgets/fetch_spec.rb
@@ -0,0 +1,24 @@
+describe 'the `autosuggest-fetch` widget' do
+ context 'when suggestions are disabled' do
+ before do
+ session.
+ run_command('bindkey ^B autosuggest-disable').
+ run_command('bindkey ^F autosuggest-fetch').
+ send_keys('C-b')
+ end
+
+ it 'will fetch and display a suggestion' do
+ with_history('echo hello') do
+ session.send_string('echo h')
+ sleep 1
+ expect(session.content).to eq('echo h')
+
+ session.send_keys('C-f')
+ wait_for { session.content }.to eq('echo hello')
+
+ session.send_string('e')
+ wait_for { session.content }.to eq('echo hello')
+ end
+ end
+ end
+end
diff --git a/spec/widgets/toggle_spec.rb b/spec/widgets/toggle_spec.rb
new file mode 100644
index 0000000..8f9f3c3
--- /dev/null
+++ b/spec/widgets/toggle_spec.rb
@@ -0,0 +1,26 @@
+describe 'the `autosuggest-toggle` widget' do
+ before do
+ session.run_command('bindkey ^B autosuggest-toggle')
+ end
+
+ it 'toggles suggestions' do
+ with_history('echo world', 'echo hello') do
+ session.send_string('echo')
+ wait_for { session.content }.to eq('echo hello')
+
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('echo')
+
+ session.send_string(' h')
+ sleep 1
+ expect(session.content).to eq('echo h')
+
+ session.send_keys('C-b')
+ wait_for { session.content }.to eq('echo hello')
+
+ session.send_keys('C-h')
+ session.send_string('w')
+ wait_for { session.content }.to eq('echo world')
+ end
+ end
+end
diff --git a/src/async.zsh b/src/async.zsh
new file mode 100644
index 0000000..124c9ac
--- /dev/null
+++ b/src/async.zsh
@@ -0,0 +1,109 @@
+
+#--------------------------------------------------------------------#
+# Async #
+#--------------------------------------------------------------------#
+
+# Zpty process is spawned running this function
+_zsh_autosuggest_async_server() {
+ emulate -R zsh
+
+ # There is a bug in zpty module (fixed in zsh/master) 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.
+ zshexit() {
+ kill -KILL $$
+ sleep 1 # Block for long enough for the signal to come through
+ }
+
+ # Output only newlines (not carriage return + newline)
+ stty -onlcr
+
+ # Silence any error messages
+ exec 2>/dev/null
+
+ local strategy=$1
+ local last_pid
+
+ while IFS='' read -r -d $'\0' query; do
+ # Kill last bg process
+ kill -KILL $last_pid &>/dev/null
+
+ # Run suggestion search in the background
+ (
+ local suggestion
+ _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query"
+ echo -n -E "$suggestion"$'\0'
+ ) &
+
+ last_pid=$!
+ done
+}
+
+_zsh_autosuggest_async_request() {
+ # Write the query to the zpty process to fetch a suggestion
+ zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0'
+}
+
+# Called when new data is ready to be read from the pty
+# First arg will be fd ready for reading
+# Second arg will be passed in case of error
+_zsh_autosuggest_async_response() {
+ setopt LOCAL_OPTIONS EXTENDED_GLOB
+
+ local suggestion
+
+ zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null
+ zle autosuggest-suggest -- "${suggestion%%$'\0'##}"
+}
+
+_zsh_autosuggest_async_pty_create() {
+ # With newer versions of zsh, REPLY stores the fd to read from
+ 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
+ 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"
+
+ # Store the fd so we can remove the handler later
+ if (( REPLY )); then
+ _ZSH_AUTOSUGGEST_PTY_FD=$REPLY
+ else
+ _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd
+ fi
+
+ # Set up input handler from the zpty
+ zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response
+}
+
+_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
+
+ # Destroy the zpty
+ zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null
+ fi
+}
+
+_zsh_autosuggest_async_pty_recreate() {
+ _zsh_autosuggest_async_pty_destroy
+ _zsh_autosuggest_async_pty_create
+}
+
+_zsh_autosuggest_async_start() {
+ typeset -g _ZSH_AUTOSUGGEST_PTY_FD
+
+ _zsh_autosuggest_feature_detect_zpty_returns_fd
+ _zsh_autosuggest_async_pty_recreate
+
+ # We recreate the pty to get a fresh list of history events
+ add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate
+}
diff --git a/src/bind.zsh b/src/bind.zsh
index 49763e8..d8629f1 100644
--- a/src/bind.zsh
+++ b/src/bind.zsh
@@ -3,12 +3,34 @@
# Widget Helpers #
#--------------------------------------------------------------------#
+_zsh_autosuggest_incr_bind_count() {
+ if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then
+ ((_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]++))
+ else
+ _ZSH_AUTOSUGGEST_BIND_COUNTS[$1]=1
+ fi
+
+ bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]
+}
+
+_zsh_autosuggest_get_bind_count() {
+ if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then
+ bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]
+ else
+ bind_count=0
+ fi
+}
+
# Bind a single widget to an autosuggest widget, saving a reference to the original widget
_zsh_autosuggest_bind_widget() {
+ typeset -gA _ZSH_AUTOSUGGEST_BIND_COUNTS
+
local widget=$1
local autosuggest_action=$2
local prefix=$ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX
+ local -i bind_count
+
# Save a reference to the original widget
case $widgets[$widget] in
# Already bound
@@ -16,33 +38,38 @@ _zsh_autosuggest_bind_widget() {
# User-defined widget
user:*)
- zle -N $prefix$widget ${widgets[$widget]#*:}
+ _zsh_autosuggest_incr_bind_count $widget
+ zle -N $prefix${bind_count}-$widget ${widgets[$widget]#*:}
;;
# Built-in widget
builtin)
+ _zsh_autosuggest_incr_bind_count $widget
eval "_zsh_autosuggest_orig_${(q)widget}() { zle .${(q)widget} }"
- zle -N $prefix$widget _zsh_autosuggest_orig_$widget
+ zle -N $prefix${bind_count}-$widget _zsh_autosuggest_orig_$widget
;;
# Completion widget
completion:*)
- eval "zle -C $prefix${(q)widget} ${${(s.:.)widgets[$widget]}[2,3]}"
+ _zsh_autosuggest_incr_bind_count $widget
+ eval "zle -C $prefix${bind_count}-${(q)widget} ${${(s.:.)widgets[$widget]}[2,3]}"
;;
esac
+ _zsh_autosuggest_get_bind_count $widget
+
# Pass the original widget's name explicitly into the autosuggest
# function. Use this passed in widget name to call the original
# widget instead of relying on the $WIDGET variable being set
# correctly. $WIDGET cannot be trusted because other plugins call
# zle without the `-w` flag (e.g. `zle self-insert` instead of
# `zle self-insert -w`).
- eval "_zsh_autosuggest_bound_${(q)widget}() {
- _zsh_autosuggest_widget_$autosuggest_action $prefix${(q)widget} \$@
+ eval "_zsh_autosuggest_bound_${bind_count}_${(q)widget}() {
+ _zsh_autosuggest_widget_$autosuggest_action $prefix$bind_count-${(q)widget} \$@
}"
# Create the bound widget
- zle -N $widget _zsh_autosuggest_bound_$widget
+ zle -N $widget _zsh_autosuggest_bound_${bind_count}_$widget
}
# Map all configured widgets to the right autosuggest widgets
@@ -53,7 +80,7 @@ _zsh_autosuggest_bind_widgets() {
ignore_widgets=(
.\*
_\*
- zle-line-\*
+ zle-\*
autosuggest-\*
$ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX\*
$ZSH_AUTOSUGGEST_IGNORE_WIDGETS
diff --git a/src/config.zsh b/src/config.zsh
index f519f6f..c7fc55a 100644
--- a/src/config.zsh
+++ b/src/config.zsh
@@ -60,3 +60,6 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=(
# Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound.
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=
+
+# Pty name for calculating autosuggestions asynchronously
+ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty
diff --git a/src/deprecated.zsh b/src/deprecated.zsh
deleted file mode 100644
index bcf0d74..0000000
--- a/src/deprecated.zsh
+++ /dev/null
@@ -1,36 +0,0 @@
-
-#--------------------------------------------------------------------#
-# Handle Deprecated Variables/Widgets #
-#--------------------------------------------------------------------#
-
-_zsh_autosuggest_deprecated_warning() {
- >&2 echo "zsh-autosuggestions: $@"
-}
-
-_zsh_autosuggest_check_deprecated_config() {
- if [ -n "$AUTOSUGGESTION_HIGHLIGHT_COLOR" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_HIGHLIGHT_COLOR is deprecated. Use ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE instead."
- [ -z "$ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" ] && ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE=$AUTOSUGGESTION_HIGHLIGHT_STYLE
- unset AUTOSUGGESTION_HIGHLIGHT_STYLE
- fi
-
- if [ -n "$AUTOSUGGESTION_HIGHLIGHT_CURSOR" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_HIGHLIGHT_CURSOR is deprecated."
- unset AUTOSUGGESTION_HIGHLIGHT_CURSOR
- fi
-
- if [ -n "$AUTOSUGGESTION_ACCEPT_RIGHT_ARROW" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_ACCEPT_RIGHT_ARROW is deprecated. The right arrow now accepts the suggestion by default."
- unset AUTOSUGGESTION_ACCEPT_RIGHT_ARROW
- fi
-}
-
-_zsh_autosuggest_deprecated_start_widget() {
- _zsh_autosuggest_deprecated_warning "The autosuggest-start widget is deprecated. For more info, see the README at https://github.com/zsh-users/zsh-autosuggestions."
- zle -D autosuggest-start
- eval "zle-line-init() {
- $(echo $functions[${widgets[zle-line-init]#*:}] | sed -e 's/zle autosuggest-start//g')
- }"
-}
-
-zle -N autosuggest-start _zsh_autosuggest_deprecated_start_widget
diff --git a/src/features.zsh b/src/features.zsh
new file mode 100644
index 0000000..7a5248f
--- /dev/null
+++ b/src/features.zsh
@@ -0,0 +1,19 @@
+
+#--------------------------------------------------------------------#
+# Feature Detection #
+#--------------------------------------------------------------------#
+
+_zsh_autosuggest_feature_detect_zpty_returns_fd() {
+ typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD
+ typeset -h REPLY
+
+ zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }'
+
+ if (( REPLY )); then
+ _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1
+ else
+ _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0
+ fi
+
+ zpty -d zsh_autosuggest_feature_detect
+}
diff --git a/src/setup.zsh b/src/setup.zsh
new file mode 100644
index 0000000..c74489f
--- /dev/null
+++ b/src/setup.zsh
@@ -0,0 +1,10 @@
+
+#--------------------------------------------------------------------#
+# 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 54f5bb8..6fa8ce9 100644
--- a/src/start.zsh
+++ b/src/start.zsh
@@ -5,9 +5,20 @@
# Start the autosuggestion widgets
_zsh_autosuggest_start() {
- _zsh_autosuggest_check_deprecated_config
+ add-zsh-hook -d precmd _zsh_autosuggest_start
+
_zsh_autosuggest_bind_widgets
+
+ # Re-bind widgets on every precmd to ensure we wrap other wrappers.
+ # Specifically, highlighting breaks if our widgets are wrapped by
+ # zsh-syntax-highlighting widgets. This also allows modifications
+ # 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
+ _zsh_autosuggest_async_start
+ fi
}
-autoload -Uz add-zsh-hook
+# Start the autosuggestion widgets on the next precmd
add-zsh-hook precmd _zsh_autosuggest_start
diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh
index 29333f5..60c0494 100644
--- a/src/strategies/default.zsh
+++ b/src/strategies/default.zsh
@@ -7,5 +7,19 @@
#
_zsh_autosuggest_strategy_default() {
- fc -lnrm "$1*" 1 2>/dev/null | head -n 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
+ local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}"
+
+ # Get the history items that match
+ # - (r) subscript flag makes the pattern match on values
+ suggestion="${history[(r)$prefix*]}"
+
}
diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh
index bf8bdd9..ee26346 100644
--- a/src/strategies/match_prev_cmd.zsh
+++ b/src/strategies/match_prev_cmd.zsh
@@ -21,7 +21,7 @@
# `HIST_EXPIRE_DUPS_FIRST`.
_zsh_autosuggest_strategy_match_prev_cmd() {
- local prefix="$1"
+ local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}"
# Get all history event numbers that correspond to history
# entries that match pattern $prefix*
@@ -47,6 +47,6 @@ _zsh_autosuggest_strategy_match_prev_cmd() {
fi
done
- # Echo the matched history entry
- echo -E "$history[$histkey]"
+ # Give back the matched history entry
+ suggestion="$history[$histkey]"
}
diff --git a/src/suggestion.zsh b/src/suggestion.zsh
deleted file mode 100644
index 31a9f76..0000000
--- a/src/suggestion.zsh
+++ /dev/null
@@ -1,21 +0,0 @@
-
-#--------------------------------------------------------------------#
-# Suggestion #
-#--------------------------------------------------------------------#
-
-# Delegate to the selected strategy to determine a suggestion
-_zsh_autosuggest_suggestion() {
- local escaped_prefix="$(_zsh_autosuggest_escape_command "$1")"
- local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY"
-
- if [ -n "$functions[$strategy_function]" ]; then
- echo -E "$($strategy_function "$escaped_prefix")"
- fi
-}
-
-_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/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 57f378e..aa3f248 100644
--- a/src/widgets.zsh
+++ b/src/widgets.zsh
@@ -3,6 +3,30 @@
# Autosuggest Widget Implementations #
#--------------------------------------------------------------------#
+# Disable suggestions
+_zsh_autosuggest_disable() {
+ typeset -g _ZSH_AUTOSUGGEST_DISABLED
+ _zsh_autosuggest_clear
+}
+
+# Enable suggestions
+_zsh_autosuggest_enable() {
+ unset _ZSH_AUTOSUGGEST_DISABLED
+
+ if [ $#BUFFER -gt 0 ]; then
+ _zsh_autosuggest_fetch
+ fi
+}
+
+# Toggle suggestions (enable/disable)
+_zsh_autosuggest_toggle() {
+ if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then
+ _zsh_autosuggest_enable
+ else
+ _zsh_autosuggest_disable
+ fi
+}
+
# Clear the suggestion
_zsh_autosuggest_clear() {
# Remove the suggestion
@@ -15,39 +39,79 @@ _zsh_autosuggest_clear() {
_zsh_autosuggest_modify() {
local -i retval
+ # Only added to zsh very recently
+ local -i KEYS_QUEUED_COUNT
+
# Save the contents of the buffer/postdisplay
local orig_buffer="$BUFFER"
local orig_postdisplay="$POSTDISPLAY"
- # Clear suggestion while original widget runs
+ # Clear suggestion while waiting for next one
unset POSTDISPLAY
# Original widget may modify the buffer
_zsh_autosuggest_invoke_original_widget $@
retval=$?
+ # Don't fetch a new suggestion if there's more input to be read immediately
+ if [[ $PENDING > 0 ]] || [[ $KEYS_QUEUED_COUNT > 0 ]]; then
+ return $retval
+ fi
+
+ # Optimize if manually typing in the suggestion
+ if [ $#BUFFER -gt $#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
+ 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
POSTDISPLAY="$orig_postdisplay"
return $retval
fi
+ # Bail out if suggestions are disabled
+ if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then
+ return $?
+ fi
+
# Get a new suggestion if the buffer is not empty after modification
- local suggestion
if [ $#BUFFER -gt 0 ]; then
- if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then
- suggestion="$(_zsh_autosuggest_suggestion "$BUFFER")"
+ if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then
+ _zsh_autosuggest_fetch
fi
fi
- # Add the suggestion to the POSTDISPLAY
- if [ -n "$suggestion" ]; then
- POSTDISPLAY="${suggestion#$BUFFER}"
- fi
-
return $retval
}
+# Fetch a new suggestion based on what's currently in the buffer
+_zsh_autosuggest_fetch() {
+ if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then
+ _zsh_autosuggest_async_request "$BUFFER"
+ else
+ local suggestion
+ _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER"
+ _zsh_autosuggest_suggest "$suggestion"
+ fi
+}
+
+# Offer a suggestion
+_zsh_autosuggest_suggest() {
+ local suggestion="$1"
+
+ if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then
+ POSTDISPLAY="${suggestion#$BUFFER}"
+ else
+ unset POSTDISPLAY
+ fi
+}
+
# Accept the entire suggestion
_zsh_autosuggest_accept() {
local -i max_cursor_pos=$#BUFFER
@@ -115,7 +179,7 @@ _zsh_autosuggest_partial_accept() {
return $retval
}
-for action in clear modify accept partial_accept execute; do
+for action in clear modify fetch suggest accept partial_accept execute enable disable toggle; do
eval "_zsh_autosuggest_widget_$action() {
local -i retval
@@ -126,10 +190,17 @@ for action in clear modify accept partial_accept execute; do
_zsh_autosuggest_highlight_apply
+ zle -R
+
return \$retval
}"
done
+zle -N autosuggest-fetch _zsh_autosuggest_widget_fetch
+zle -N autosuggest-suggest _zsh_autosuggest_widget_suggest
zle -N autosuggest-accept _zsh_autosuggest_widget_accept
zle -N autosuggest-clear _zsh_autosuggest_widget_clear
zle -N autosuggest-execute _zsh_autosuggest_widget_execute
+zle -N autosuggest-enable _zsh_autosuggest_widget_enable
+zle -N autosuggest-disable _zsh_autosuggest_widget_disable
+zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle
diff --git a/test/bind_test.zsh b/test/bind_test.zsh
deleted file mode 100644
index f73ea7f..0000000
--- a/test/bind_test.zsh
+++ /dev/null
@@ -1,45 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-testInvokeOriginalWidgetDefined() {
- stub_and_eval \
- zle \
- 'return 1'
-
- _zsh_autosuggest_invoke_original_widget 'self-insert'
-
- assertEquals \
- '1' \
- "$?"
-
- assertTrue \
- 'zle was not invoked' \
- 'stub_called zle'
-
- restore zle
-}
-
-testInvokeOriginalWidgetUndefined() {
- stub_and_eval \
- zle \
- 'return 1'
-
- _zsh_autosuggest_invoke_original_widget 'some-undefined-widget'
-
- assertEquals \
- '0' \
- "$?"
-
- assertFalse \
- 'zle was invoked' \
- 'stub_called zle'
-
- restore zle
-}
-
-run_tests "$0"
diff --git a/test/highlight_test.zsh b/test/highlight_test.zsh
deleted file mode 100644
index 7268af8..0000000
--- a/test/highlight_test.zsh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-testHighlightDefaultStyle() {
- assertEquals \
- 'fg=8' \
- "$ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE"
-}
-
-testHighlightApplyWithSuggestion() {
- local orig_style=ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE
- ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=4'
-
- BUFFER='ec'
- POSTDISPLAY='ho hello'
- region_highlight=('0 2 fg=1')
-
- _zsh_autosuggest_highlight_apply
-
- assertEquals \
- 'highlight did not use correct style' \
- "0 2 fg=1 2 10 $ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" \
- "$region_highlight"
-
- assertEquals \
- 'higlight was not saved to be removed later' \
- "2 10 $ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" \
- "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT"
-
- ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE=orig_style
-}
-
-testHighlightApplyWithoutSuggestion() {
- BUFFER='echo hello'
- POSTDISPLAY=''
- region_highlight=('0 4 fg=1')
-
- _zsh_autosuggest_highlight_apply
-
- assertEquals \
- 'region_highlight was modified' \
- '0 4 fg=1' \
- "$region_highlight"
-
- assertNull \
- 'last highlight region was not cleared' \
- "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT"
-}
-
-testHighlightReset() {
- BUFFER='ec'
- POSTDISPLAY='ho hello'
- region_highlight=('0 1 fg=1' '2 10 fg=8' '1 2 fg=1')
- _ZSH_AUTOSUGGEST_LAST_HIGHLIGHT='2 10 fg=8'
-
- _zsh_autosuggest_highlight_reset
-
- assertEquals \
- 'last highlight region was not removed' \
- '0 1 fg=1 1 2 fg=1' \
- "$region_highlight"
-
- assertNull \
- 'last highlight variable was not cleared' \
- "$_ZSH_AUTOSUGGEST_LAST_HIGHLIGHT"
-}
-
-run_tests "$0"
diff --git a/test/strategies/default_test.zsh b/test/strategies/default_test.zsh
deleted file mode 100755
index f5200e6..0000000
--- a/test/strategies/default_test.zsh
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-testNoMatch() {
- set_history <<-'EOF'
- ls foo
- ls bar
- EOF
-
- assertSuggestion \
- 'foo' \
- ''
-
- assertSuggestion \
- 'ls q' \
- ''
-}
-
-testBasicMatches() {
- set_history <<-'EOF'
- ls foo
- ls bar
- EOF
-
- assertSuggestion \
- 'ls f' \
- 'ls foo'
-
- assertSuggestion \
- 'ls b' \
- 'ls bar'
-}
-
-testMostRecentMatch() {
- set_history <<-'EOF'
- ls foo
- cd bar
- ls baz
- cd quux
- EOF
-
- assertSuggestion \
- 'ls' \
- 'ls baz'
-
- assertSuggestion \
- 'cd' \
- 'cd quux'
-}
-
-run_tests "$0"
diff --git a/test/strategies/match_prev_cmd_test.zsh b/test/strategies/match_prev_cmd_test.zsh
deleted file mode 100755
index bf3fc64..0000000
--- a/test/strategies/match_prev_cmd_test.zsh
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-
- ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd
-}
-
-testNoMatch() {
- set_history <<-'EOF'
- ls foo
- ls bar
- EOF
-
- assertSuggestion \
- 'foo' \
- ''
-
- assertSuggestion \
- 'ls q' \
- ''
-}
-
-testBasicMatches() {
- set_history <<-'EOF'
- ls foo
- ls bar
- EOF
-
- assertSuggestion \
- 'ls f' \
- 'ls foo'
-
- assertSuggestion \
- 'ls b' \
- 'ls bar'
-}
-
-testMostRecentMatch() {
- set_history <<-'EOF'
- ls foo
- cd bar
- ls baz
- cd quux
- EOF
-
- assertSuggestion \
- 'ls' \
- 'ls baz'
-
- assertSuggestion \
- 'cd' \
- 'cd quux'
-}
-
-testMatchMostRecentAfterPreviousCmd() {
- set_history <<-'EOF'
- echo what
- ls foo
- ls bar
- echo what
- ls baz
- ls quux
- echo what
- EOF
-
- assertSuggestion \
- 'ls' \
- 'ls baz'
-}
-
-run_tests "$0"
diff --git a/test/strategies_test.zsh b/test/strategies_test.zsh
deleted file mode 100644
index 0a937f4..0000000
--- a/test/strategies_test.zsh
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-assertBackslashSuggestion() {
- set_history <<-'EOF'
- echo "hello\nworld"
- EOF
-
- assertSuggestion \
- 'echo "hello\' \
- 'echo "hello\nworld"'
-}
-
-assertDoubleBackslashSuggestion() {
- set_history <<-'EOF'
- echo "\\"
- EOF
-
- assertSuggestion \
- 'echo "\\' \
- 'echo "\\"'
-}
-
-assertTildeSuggestion() {
- set_history <<-'EOF'
- cd ~/something
- EOF
-
- assertSuggestion \
- 'cd' \
- 'cd ~/something'
-
- assertSuggestion \
- 'cd ~' \
- 'cd ~/something'
-
- assertSuggestion \
- 'cd ~/s' \
- 'cd ~/something'
-}
-
-assertTildeSuggestionWithExtendedGlob() {
- setopt local_options extended_glob
-
- assertTildeSuggestion
-}
-
-assertParenthesesSuggestion() {
- set_history <<-'EOF'
- echo "$(ls foo)"
- EOF
-
- assertSuggestion \
- 'echo "$(' \
- 'echo "$(ls foo)"'
-}
-
-assertSquareBracketsSuggestion() {
- set_history <<-'EOF'
- echo "$history[123]"
- EOF
-
- assertSuggestion \
- 'echo "$history[' \
- 'echo "$history[123]"'
-}
-
-assertHashSuggestion() {
- set_history <<-'EOF'
- echo "#yolo"
- EOF
-
- assertSuggestion \
- 'echo "#' \
- 'echo "#yolo"'
-}
-
-testSpecialCharsForAllStrategies() {
- local strategies
- strategies=(
- "default"
- "match_prev_cmd"
- )
-
- for s in $strategies; do
- ZSH_AUTOSUGGEST_STRATEGY="$s"
-
- assertBackslashSuggestion
- assertDoubleBackslashSuggestion
- assertTildeSuggestion
- assertTildeSuggestionWithExtendedGlob
- assertParenthesesSuggestion
- assertSquareBracketsSuggestion
- done
-}
-
-run_tests "$0"
diff --git a/test/suggestion_test.zsh b/test/suggestion_test.zsh
deleted file mode 100644
index fc6330d..0000000
--- a/test/suggestion_test.zsh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-testEscapeCommand() {
- assertEquals \
- 'Did not escape single backslash' \
- '\\' \
- "$(_zsh_autosuggest_escape_command '\')"
-
- assertEquals \
- 'Did not escape two backslashes' \
- '\\\\' \
- "$(_zsh_autosuggest_escape_command '\\')"
-
- assertEquals \
- 'Did not escape parentheses' \
- '\(\)' \
- "$(_zsh_autosuggest_escape_command '()')"
-
- assertEquals \
- 'Did not escape square brackets' \
- '\[\]' \
- "$(_zsh_autosuggest_escape_command '[]')"
-
- assertEquals \
- 'Did not escape pipe' \
- '\|' \
- "$(_zsh_autosuggest_escape_command '|')"
-
- assertEquals \
- 'Did not escape star' \
- '\*' \
- "$(_zsh_autosuggest_escape_command '*')"
-
- assertEquals \
- 'Did not escape question mark' \
- '\?' \
- "$(_zsh_autosuggest_escape_command '?')"
-}
-
-run_tests "$0"
diff --git a/test/test_helper.zsh b/test/test_helper.zsh
deleted file mode 100644
index 7e7dbc0..0000000
--- a/test/test_helper.zsh
+++ /dev/null
@@ -1,60 +0,0 @@
-DIR="${0:a:h}"
-ROOT_DIR="$DIR/.."
-VENDOR_DIR="$ROOT_DIR/vendor"
-
-# Use stub.sh for stubbing/mocking
-source "$VENDOR_DIR/stub.sh/stub.sh"
-
-#--------------------------------------------------------------------#
-# Helper Functions #
-#--------------------------------------------------------------------#
-
-# Source the autosuggestions plugin file
-source_autosuggestions() {
- source "$ROOT_DIR/zsh-autosuggestions.zsh"
-}
-
-# Set history list from stdin
-set_history() {
- # Make a tmp file in shunit's tmp dir
- local tmp=$(mktemp "$SHUNIT_TMPDIR/hist.XXX")
-
- # Write from stdin to the tmp file
- > "$tmp"
-
- # Write an extra line to simulate history active mode
- # See https://github.com/zsh-users/zsh/blob/ca3bc0d95d7deab4f5381f12b15047de748c0814/Src/hist.c#L69-L82
- echo >> "$tmp"
-
- # Clear history and re-read from the tmp file
- fc -P; fc -p; fc -R "$tmp"
-
- rm "$tmp"
-}
-
-# Should be called at the bottom of every test suite file
-# Pass in the name of the test script ($0) for shunit
-run_tests() {
- local test_script="$1"
- shift
-
- # Required for shunit to work with zsh
- setopt localoptions shwordsplit
- SHUNIT_PARENT="$test_script"
-
- source "$VENDOR_DIR/shunit2/2.1.6/src/shunit2"
-}
-
-#--------------------------------------------------------------------#
-# Custom Assertions #
-#--------------------------------------------------------------------#
-
-assertSuggestion() {
- local prefix="$1"
- local expected_suggestion="$2"
-
- assertEquals \
- "Did not get correct suggestion for prefix:<$prefix> using strategy <$ZSH_AUTOSUGGEST_STRATEGY>" \
- "$expected_suggestion" \
- "$(_zsh_autosuggest_suggestion "$prefix")"
-}
diff --git a/test/widgets/accept_test.zsh b/test/widgets/accept_test.zsh
deleted file mode 100644
index f126091..0000000
--- a/test/widgets/accept_test.zsh
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-setUp() {
- BUFFER=''
- POSTDISPLAY=''
- CURSOR=0
- KEYMAP='main'
-}
-
-tearDown() {
- restore _zsh_autosuggest_invoke_original_widget
-}
-
-testCursorAtEnd() {
- BUFFER='echo'
- POSTDISPLAY=' hello'
- CURSOR=4
-
- stub _zsh_autosuggest_invoke_original_widget
-
- _zsh_autosuggest_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was not modified' \
- 'echo hello' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was not cleared' \
- '' \
- "$POSTDISPLAY"
-}
-
-testCursorNotAtEnd() {
- BUFFER='echo'
- POSTDISPLAY=' hello'
- CURSOR=2
-
- stub _zsh_autosuggest_invoke_original_widget
-
- _zsh_autosuggest_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was modified' \
- 'echo' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was modified' \
- ' hello' \
- "$POSTDISPLAY"
-}
-
-testViCursorAtEnd() {
- BUFFER='echo'
- POSTDISPLAY=' hello'
- CURSOR=3
- KEYMAP='vicmd'
-
- stub _zsh_autosuggest_invoke_original_widget
-
- _zsh_autosuggest_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was not modified' \
- 'echo hello' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was not cleared' \
- '' \
- "$POSTDISPLAY"
-}
-
-testViCursorNotAtEnd() {
- BUFFER='echo'
- POSTDISPLAY=' hello'
- CURSOR=2
- KEYMAP='vicmd'
-
- stub _zsh_autosuggest_invoke_original_widget
-
- _zsh_autosuggest_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was modified' \
- 'echo' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was modified' \
- ' hello' \
- "$POSTDISPLAY"
-}
-
-testRetval() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'return 1'
-
- _zsh_autosuggest_widget_accept 'original-widget'
-
- assertEquals \
- 'Did not return correct value from original widget' \
- '1' \
- "$?"
-}
-
-testWidget() {
- stub _zsh_autosuggest_highlight_reset
- stub _zsh_autosuggest_accept
- stub _zsh_autosuggest_highlight_apply
-
- # Call the function pointed to by the widget since we can't call
- # the widget itself when zle is not active
- ${widgets[autosuggest-accept]#*:} 'original-widget'
-
- assertTrue \
- 'autosuggest-accept widget does not exist' \
- 'zle -l autosuggest-accept'
-
- assertTrue \
- 'highlight_reset was not called' \
- 'stub_called _zsh_autosuggest_highlight_reset'
-
- assertTrue \
- 'widget function was not called' \
- 'stub_called _zsh_autosuggest_accept'
-
- assertTrue \
- 'highlight_apply was not called' \
- 'stub_called _zsh_autosuggest_highlight_apply'
-
- restore _zsh_autosuggest_highlight_reset
- restore _zsh_autosuggest_accept
- restore _zsh_autosuggest_highlight_apply
-}
-
-run_tests "$0"
diff --git a/test/widgets/clear_test.zsh b/test/widgets/clear_test.zsh
deleted file mode 100644
index f0500c5..0000000
--- a/test/widgets/clear_test.zsh
+++ /dev/null
@@ -1,77 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-setUp() {
- BUFFER=''
- POSTDISPLAY=''
-}
-
-tearDown() {
- restore _zsh_autosuggest_invoke_original_widget
-}
-
-testClear() {
- BUFFER='ec'
- POSTDISPLAY='ho hello'
-
- _zsh_autosuggest_clear 'original-widget'
-
- assertEquals \
- 'BUFFER was modified' \
- 'ec' \
- "$BUFFER"
-
- assertNull \
- 'POSTDISPLAY was not cleared' \
- "$POSTDISPLAY"
-}
-
-testRetval() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'return 1'
-
- _zsh_autosuggest_widget_clear 'original-widget'
-
- assertEquals \
- 'Did not return correct value from original widget' \
- '1' \
- "$?"
-}
-
-testWidget() {
- stub _zsh_autosuggest_highlight_reset
- stub _zsh_autosuggest_clear
- stub _zsh_autosuggest_highlight_apply
-
- # Call the function pointed to by the widget since we can't call
- # the widget itself when zle is not active
- ${widgets[autosuggest-clear]#*:} 'original-widget'
-
- assertTrue \
- 'autosuggest-clear widget does not exist' \
- 'zle -l autosuggest-clear'
-
- assertTrue \
- 'highlight_reset was not called' \
- 'stub_called _zsh_autosuggest_highlight_reset'
-
- assertTrue \
- 'widget function was not called' \
- 'stub_called _zsh_autosuggest_clear'
-
- assertTrue \
- 'highlight_apply was not called' \
- 'stub_called _zsh_autosuggest_highlight_apply'
-
- restore _zsh_autosuggest_highlight_reset
- restore _zsh_autosuggest_clear
- restore _zsh_autosuggest_highlight_apply
-}
-
-run_tests "$0"
diff --git a/test/widgets/execute_test.zsh b/test/widgets/execute_test.zsh
deleted file mode 100644
index cb346db..0000000
--- a/test/widgets/execute_test.zsh
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-tearDown() {
- restore _zsh_autosuggest_invoke_original_widget
-}
-
-testRetval() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'return 1'
-
- _zsh_autosuggest_widget_execute 'original-widget'
-
- assertEquals \
- 'Did not return correct value from original widget' \
- '1' \
- "$?"
-}
-
-run_tests "$0"
diff --git a/test/widgets/modify_test.zsh b/test/widgets/modify_test.zsh
deleted file mode 100644
index 7ed6346..0000000
--- a/test/widgets/modify_test.zsh
+++ /dev/null
@@ -1,88 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-setUp() {
- BUFFER=''
- POSTDISPLAY=''
- ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=''
-}
-
-tearDown() {
- restore _zsh_autosuggest_invoke_original_widget
- restore _zsh_autosuggest_suggestion
-}
-
-testModify() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'BUFFER+="e"'
-
- stub_and_echo \
- _zsh_autosuggest_suggestion \
- 'echo hello'
-
- _zsh_autosuggest_modify 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was not modified' \
- 'e' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY does not contain suggestion' \
- 'cho hello' \
- "$POSTDISPLAY"
-}
-
-testModifyBufferTooLarge() {
-
- ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE='20'
-
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'BUFFER+="012345678901234567890"'
-
- stub_and_echo \
- _zsh_autosuggest_suggestion \
- '012345678901234567890123456789'
-
- _zsh_autosuggest_modify 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was not modified' \
- '012345678901234567890' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY does not contain suggestion' \
- '' \
- "$POSTDISPLAY"
-}
-
-testRetval() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'return 1'
-
- _zsh_autosuggest_widget_modify 'original-widget'
-
- assertEquals \
- 'Did not return correct value from original widget' \
- '1' \
- "$?"
-}
-
-run_tests "$0"
diff --git a/test/widgets/partial_accept_test.zsh b/test/widgets/partial_accept_test.zsh
deleted file mode 100644
index 60c78a6..0000000
--- a/test/widgets/partial_accept_test.zsh
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env zsh
-
-source "${0:a:h}/../test_helper.zsh"
-
-oneTimeSetUp() {
- source_autosuggestions
-}
-
-setUp() {
- BUFFER=''
- POSTDISPLAY=''
- CURSOR=0
-}
-
-tearDown() {
- restore _zsh_autosuggest_invoke_original_widget
-}
-
-testCursorMovesOutOfBuffer() {
- BUFFER='ec'
- POSTDISPLAY='ho hello'
- CURSOR=1
-
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'CURSOR=5; LBUFFER="echo "; RBUFFER="hello"'
-
- _zsh_autosuggest_partial_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was not modified correctly' \
- 'echo ' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was not modified correctly' \
- 'hello' \
- "$POSTDISPLAY"
-}
-
-testCursorStaysInBuffer() {
- BUFFER='echo hello'
- POSTDISPLAY=' world'
- CURSOR=1
-
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'CURSOR=5; LBUFFER="echo "; RBUFFER="hello"'
-
- _zsh_autosuggest_partial_accept 'original-widget'
-
- assertTrue \
- 'original widget not invoked' \
- 'stub_called _zsh_autosuggest_invoke_original_widget'
-
- assertEquals \
- 'BUFFER was modified' \
- 'echo hello' \
- "$BUFFER"
-
- assertEquals \
- 'POSTDISPLAY was modified' \
- ' world' \
- "$POSTDISPLAY"
-}
-
-testRetval() {
- stub_and_eval \
- _zsh_autosuggest_invoke_original_widget \
- 'return 1'
-
- _zsh_autosuggest_widget_partial_accept 'original-widget'
-
- assertEquals \
- 'Did not return correct value from original widget' \
- '1' \
- "$?"
-}
-
-run_tests "$0"
diff --git a/vendor/shunit2 b/vendor/shunit2
deleted file mode 160000
index 46973db..0000000
--- a/vendor/shunit2
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 46973db9df87bd5fdadea74cb472a99f212a0d3a
diff --git a/vendor/stub.sh b/vendor/stub.sh
deleted file mode 160000
index bd6f3c4..0000000
--- a/vendor/stub.sh
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit bd6f3c4666cd2a702e388e09d77b8543a1f6b672
diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh
index 3761efe..0d389ec 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.3.3
+# v0.4.0
# Copyright (c) 2013 Thiago de Arruda
-# Copyright (c) 2016 Eric Freese
+# Copyright (c) 2016-2017 Eric Freese
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
@@ -25,6 +25,16 @@
# 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 #
#--------------------------------------------------------------------#
@@ -87,52 +97,71 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=(
# Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound.
ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=
+# Pty name for calculating autosuggestions asynchronously
+ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty
+
#--------------------------------------------------------------------#
-# Handle Deprecated Variables/Widgets #
+# Utility Functions #
#--------------------------------------------------------------------#
-_zsh_autosuggest_deprecated_warning() {
- >&2 echo "zsh-autosuggestions: $@"
+_zsh_autosuggest_escape_command() {
+ setopt localoptions EXTENDED_GLOB
+
+ # Escape special chars in the string (requires EXTENDED_GLOB)
+ echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}"
}
-_zsh_autosuggest_check_deprecated_config() {
- if [ -n "$AUTOSUGGESTION_HIGHLIGHT_COLOR" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_HIGHLIGHT_COLOR is deprecated. Use ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE instead."
- [ -z "$ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE" ] && ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE=$AUTOSUGGESTION_HIGHLIGHT_STYLE
- unset AUTOSUGGESTION_HIGHLIGHT_STYLE
+#--------------------------------------------------------------------#
+# Feature Detection #
+#--------------------------------------------------------------------#
+
+_zsh_autosuggest_feature_detect_zpty_returns_fd() {
+ typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD
+ typeset -h REPLY
+
+ zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }'
+
+ if (( REPLY )); then
+ _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1
+ else
+ _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0
fi
- if [ -n "$AUTOSUGGESTION_HIGHLIGHT_CURSOR" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_HIGHLIGHT_CURSOR is deprecated."
- unset AUTOSUGGESTION_HIGHLIGHT_CURSOR
- fi
-
- if [ -n "$AUTOSUGGESTION_ACCEPT_RIGHT_ARROW" ]; then
- _zsh_autosuggest_deprecated_warning "AUTOSUGGESTION_ACCEPT_RIGHT_ARROW is deprecated. The right arrow now accepts the suggestion by default."
- unset AUTOSUGGESTION_ACCEPT_RIGHT_ARROW
- fi
+ zpty -d zsh_autosuggest_feature_detect
}
-_zsh_autosuggest_deprecated_start_widget() {
- _zsh_autosuggest_deprecated_warning "The autosuggest-start widget is deprecated. For more info, see the README at https://github.com/zsh-users/zsh-autosuggestions."
- zle -D autosuggest-start
- eval "zle-line-init() {
- $(echo $functions[${widgets[zle-line-init]#*:}] | sed -e 's/zle autosuggest-start//g')
- }"
-}
-
-zle -N autosuggest-start _zsh_autosuggest_deprecated_start_widget
-
#--------------------------------------------------------------------#
# Widget Helpers #
#--------------------------------------------------------------------#
+_zsh_autosuggest_incr_bind_count() {
+ if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then
+ ((_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]++))
+ else
+ _ZSH_AUTOSUGGEST_BIND_COUNTS[$1]=1
+ fi
+
+ bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]
+}
+
+_zsh_autosuggest_get_bind_count() {
+ if ((${+_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]})); then
+ bind_count=$_ZSH_AUTOSUGGEST_BIND_COUNTS[$1]
+ else
+ bind_count=0
+ fi
+}
+
# Bind a single widget to an autosuggest widget, saving a reference to the original widget
_zsh_autosuggest_bind_widget() {
+ typeset -gA _ZSH_AUTOSUGGEST_BIND_COUNTS
+
local widget=$1
local autosuggest_action=$2
local prefix=$ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX
+ local -i bind_count
+
# Save a reference to the original widget
case $widgets[$widget] in
# Already bound
@@ -140,33 +169,38 @@ _zsh_autosuggest_bind_widget() {
# User-defined widget
user:*)
- zle -N $prefix$widget ${widgets[$widget]#*:}
+ _zsh_autosuggest_incr_bind_count $widget
+ zle -N $prefix${bind_count}-$widget ${widgets[$widget]#*:}
;;
# Built-in widget
builtin)
+ _zsh_autosuggest_incr_bind_count $widget
eval "_zsh_autosuggest_orig_${(q)widget}() { zle .${(q)widget} }"
- zle -N $prefix$widget _zsh_autosuggest_orig_$widget
+ zle -N $prefix${bind_count}-$widget _zsh_autosuggest_orig_$widget
;;
# Completion widget
completion:*)
- eval "zle -C $prefix${(q)widget} ${${(s.:.)widgets[$widget]}[2,3]}"
+ _zsh_autosuggest_incr_bind_count $widget
+ eval "zle -C $prefix${bind_count}-${(q)widget} ${${(s.:.)widgets[$widget]}[2,3]}"
;;
esac
+ _zsh_autosuggest_get_bind_count $widget
+
# Pass the original widget's name explicitly into the autosuggest
# function. Use this passed in widget name to call the original
# widget instead of relying on the $WIDGET variable being set
# correctly. $WIDGET cannot be trusted because other plugins call
# zle without the `-w` flag (e.g. `zle self-insert` instead of
# `zle self-insert -w`).
- eval "_zsh_autosuggest_bound_${(q)widget}() {
- _zsh_autosuggest_widget_$autosuggest_action $prefix${(q)widget} \$@
+ eval "_zsh_autosuggest_bound_${bind_count}_${(q)widget}() {
+ _zsh_autosuggest_widget_$autosuggest_action $prefix$bind_count-${(q)widget} \$@
}"
# Create the bound widget
- zle -N $widget _zsh_autosuggest_bound_$widget
+ zle -N $widget _zsh_autosuggest_bound_${bind_count}_$widget
}
# Map all configured widgets to the right autosuggest widgets
@@ -177,7 +211,7 @@ _zsh_autosuggest_bind_widgets() {
ignore_widgets=(
.\*
_\*
- zle-line-\*
+ zle-\*
autosuggest-\*
$ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX\*
$ZSH_AUTOSUGGEST_IGNORE_WIDGETS
@@ -244,6 +278,30 @@ _zsh_autosuggest_highlight_apply() {
# Autosuggest Widget Implementations #
#--------------------------------------------------------------------#
+# Disable suggestions
+_zsh_autosuggest_disable() {
+ typeset -g _ZSH_AUTOSUGGEST_DISABLED
+ _zsh_autosuggest_clear
+}
+
+# Enable suggestions
+_zsh_autosuggest_enable() {
+ unset _ZSH_AUTOSUGGEST_DISABLED
+
+ if [ $#BUFFER -gt 0 ]; then
+ _zsh_autosuggest_fetch
+ fi
+}
+
+# Toggle suggestions (enable/disable)
+_zsh_autosuggest_toggle() {
+ if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then
+ _zsh_autosuggest_enable
+ else
+ _zsh_autosuggest_disable
+ fi
+}
+
# Clear the suggestion
_zsh_autosuggest_clear() {
# Remove the suggestion
@@ -256,39 +314,79 @@ _zsh_autosuggest_clear() {
_zsh_autosuggest_modify() {
local -i retval
+ # Only added to zsh very recently
+ local -i KEYS_QUEUED_COUNT
+
# Save the contents of the buffer/postdisplay
local orig_buffer="$BUFFER"
local orig_postdisplay="$POSTDISPLAY"
- # Clear suggestion while original widget runs
+ # Clear suggestion while waiting for next one
unset POSTDISPLAY
# Original widget may modify the buffer
_zsh_autosuggest_invoke_original_widget $@
retval=$?
+ # Don't fetch a new suggestion if there's more input to be read immediately
+ if [[ $PENDING > 0 ]] || [[ $KEYS_QUEUED_COUNT > 0 ]]; then
+ return $retval
+ fi
+
+ # Optimize if manually typing in the suggestion
+ if [ $#BUFFER -gt $#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
+ 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
POSTDISPLAY="$orig_postdisplay"
return $retval
fi
+ # Bail out if suggestions are disabled
+ if [ -n "${_ZSH_AUTOSUGGEST_DISABLED+x}" ]; then
+ return $?
+ fi
+
# Get a new suggestion if the buffer is not empty after modification
- local suggestion
if [ $#BUFFER -gt 0 ]; then
- if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then
- suggestion="$(_zsh_autosuggest_suggestion "$BUFFER")"
+ if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then
+ _zsh_autosuggest_fetch
fi
fi
- # Add the suggestion to the POSTDISPLAY
- if [ -n "$suggestion" ]; then
- POSTDISPLAY="${suggestion#$BUFFER}"
- fi
-
return $retval
}
+# Fetch a new suggestion based on what's currently in the buffer
+_zsh_autosuggest_fetch() {
+ if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then
+ _zsh_autosuggest_async_request "$BUFFER"
+ else
+ local suggestion
+ _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$BUFFER"
+ _zsh_autosuggest_suggest "$suggestion"
+ fi
+}
+
+# Offer a suggestion
+_zsh_autosuggest_suggest() {
+ local suggestion="$1"
+
+ if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then
+ POSTDISPLAY="${suggestion#$BUFFER}"
+ else
+ unset POSTDISPLAY
+ fi
+}
+
# Accept the entire suggestion
_zsh_autosuggest_accept() {
local -i max_cursor_pos=$#BUFFER
@@ -356,7 +454,7 @@ _zsh_autosuggest_partial_accept() {
return $retval
}
-for action in clear modify accept partial_accept execute; do
+for action in clear modify fetch suggest accept partial_accept execute enable disable toggle; do
eval "_zsh_autosuggest_widget_$action() {
local -i retval
@@ -367,34 +465,20 @@ for action in clear modify accept partial_accept execute; do
_zsh_autosuggest_highlight_apply
+ zle -R
+
return \$retval
}"
done
+zle -N autosuggest-fetch _zsh_autosuggest_widget_fetch
+zle -N autosuggest-suggest _zsh_autosuggest_widget_suggest
zle -N autosuggest-accept _zsh_autosuggest_widget_accept
zle -N autosuggest-clear _zsh_autosuggest_widget_clear
zle -N autosuggest-execute _zsh_autosuggest_widget_execute
-
-#--------------------------------------------------------------------#
-# Suggestion #
-#--------------------------------------------------------------------#
-
-# Delegate to the selected strategy to determine a suggestion
-_zsh_autosuggest_suggestion() {
- local escaped_prefix="$(_zsh_autosuggest_escape_command "$1")"
- local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY"
-
- if [ -n "$functions[$strategy_function]" ]; then
- echo -E "$($strategy_function "$escaped_prefix")"
- fi
-}
-
-_zsh_autosuggest_escape_command() {
- setopt localoptions EXTENDED_GLOB
-
- # Escape special chars in the string (requires EXTENDED_GLOB)
- echo -E "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}"
-}
+zle -N autosuggest-enable _zsh_autosuggest_widget_enable
+zle -N autosuggest-disable _zsh_autosuggest_widget_disable
+zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle
#--------------------------------------------------------------------#
# Default Suggestion Strategy #
@@ -404,7 +488,21 @@ _zsh_autosuggest_escape_command() {
#
_zsh_autosuggest_strategy_default() {
- fc -lnrm "$1*" 1 2>/dev/null | head -n 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
+ local prefix="${1//(#m)[\\*?[\]<>()|^~#]/\\$MATCH}"
+
+ # Get the history items that match
+ # - (r) subscript flag makes the pattern match on values
+ suggestion="${history[(r)$prefix*]}"
+
}
#--------------------------------------------------------------------#
@@ -429,7 +527,7 @@ _zsh_autosuggest_strategy_default() {
# `HIST_EXPIRE_DUPS_FIRST`.
_zsh_autosuggest_strategy_match_prev_cmd() {
- local prefix="$1"
+ local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}"
# Get all history event numbers that correspond to history
# entries that match pattern $prefix*
@@ -455,8 +553,117 @@ _zsh_autosuggest_strategy_match_prev_cmd() {
fi
done
- # Echo the matched history entry
- echo -E "$history[$histkey]"
+ # Give back the matched history entry
+ suggestion="$history[$histkey]"
+}
+
+#--------------------------------------------------------------------#
+# Async #
+#--------------------------------------------------------------------#
+
+# Zpty process is spawned running this function
+_zsh_autosuggest_async_server() {
+ emulate -R zsh
+
+ # There is a bug in zpty module (fixed in zsh/master) 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.
+ zshexit() {
+ kill -KILL $$
+ sleep 1 # Block for long enough for the signal to come through
+ }
+
+ # Output only newlines (not carriage return + newline)
+ stty -onlcr
+
+ # Silence any error messages
+ exec 2>/dev/null
+
+ local strategy=$1
+ local last_pid
+
+ while IFS='' read -r -d $'\0' query; do
+ # Kill last bg process
+ kill -KILL $last_pid &>/dev/null
+
+ # Run suggestion search in the background
+ (
+ local suggestion
+ _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query"
+ echo -n -E "$suggestion"$'\0'
+ ) &
+
+ last_pid=$!
+ done
+}
+
+_zsh_autosuggest_async_request() {
+ # Write the query to the zpty process to fetch a suggestion
+ zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0'
+}
+
+# Called when new data is ready to be read from the pty
+# First arg will be fd ready for reading
+# Second arg will be passed in case of error
+_zsh_autosuggest_async_response() {
+ setopt LOCAL_OPTIONS EXTENDED_GLOB
+
+ local suggestion
+
+ zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null
+ zle autosuggest-suggest -- "${suggestion%%$'\0'##}"
+}
+
+_zsh_autosuggest_async_pty_create() {
+ # With newer versions of zsh, REPLY stores the fd to read from
+ 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
+ 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"
+
+ # Store the fd so we can remove the handler later
+ if (( REPLY )); then
+ _ZSH_AUTOSUGGEST_PTY_FD=$REPLY
+ else
+ _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd
+ fi
+
+ # Set up input handler from the zpty
+ zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response
+}
+
+_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
+
+ # Destroy the zpty
+ zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null
+ fi
+}
+
+_zsh_autosuggest_async_pty_recreate() {
+ _zsh_autosuggest_async_pty_destroy
+ _zsh_autosuggest_async_pty_create
+}
+
+_zsh_autosuggest_async_start() {
+ typeset -g _ZSH_AUTOSUGGEST_PTY_FD
+
+ _zsh_autosuggest_feature_detect_zpty_returns_fd
+ _zsh_autosuggest_async_pty_recreate
+
+ # We recreate the pty to get a fresh list of history events
+ add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate
}
#--------------------------------------------------------------------#
@@ -465,9 +672,20 @@ _zsh_autosuggest_strategy_match_prev_cmd() {
# Start the autosuggestion widgets
_zsh_autosuggest_start() {
- _zsh_autosuggest_check_deprecated_config
+ add-zsh-hook -d precmd _zsh_autosuggest_start
+
_zsh_autosuggest_bind_widgets
+
+ # Re-bind widgets on every precmd to ensure we wrap other wrappers.
+ # Specifically, highlighting breaks if our widgets are wrapped by
+ # zsh-syntax-highlighting widgets. This also allows modifications
+ # 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
+ _zsh_autosuggest_async_start
+ fi
}
-autoload -Uz add-zsh-hook
+# Start the autosuggestion widgets on the next precmd
add-zsh-hook precmd _zsh_autosuggest_start