From af671fb406360bafba1d91173e2956c76912dec4 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 00:55:27 -0700 Subject: [PATCH 01/45] Add ruby settings to editor config --- .editorconfig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.editorconfig b/.editorconfig index 51c4765..b40bc96 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,7 @@ indent_size = 4 [*.md] indent_style = space + +[*.rb] +indent_style = space +indent_size = 2 From e6591d5de05dc51848e6680d6a009d23610a888e Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 22:33:17 -0700 Subject: [PATCH 02/45] Add RSpec for high-level integration testing --- .rspec | 2 ++ .ruby-version | 1 + Gemfile | 4 ++++ Gemfile.lock | 29 +++++++++++++++++++++++++++++ spec/spec_helper.rb | 13 +++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 .rspec create mode 100644 .ruby-version create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper 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/Gemfile b/Gemfile new file mode 100644 index 0000000..bb4eb7f --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'rspec' +gem 'rspec-wait' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..5ad7873 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,29 @@ +GEM + remote: https://rubygems.org/ + specs: + diff-lcs (1.3) + 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) + +PLATFORMS + ruby + +DEPENDENCIES + rspec + rspec-wait + +BUNDLED WITH + 1.12.5 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..38e3f56 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,13 @@ +require 'rspec/wait' + +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 +end From 07a6768fcb3c1d1696f48238fc033d38ebf6f377 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 22:33:31 -0700 Subject: [PATCH 03/45] Add TerminalSession helper for managing a tmux session --- spec/spec_helper.rb | 1 + spec/terminal_session.rb | 55 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 spec/terminal_session.rb diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 38e3f56..b3882d7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,5 @@ require 'rspec/wait' +require 'terminal_session' RSpec.configure do |config| config.expect_with :rspec do |expectations| diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb new file mode 100644 index 0000000..63eaf0d --- /dev/null +++ b/spec/terminal_session.rb @@ -0,0 +1,55 @@ +require 'securerandom' + +class TerminalSession + def initialize(width: 80, height: 24, prompt: '', term: 'xterm-256color') + tmux_command("new-session -d -x #{width} -y #{height} 'PS1=#{prompt} TERM=#{term} zsh -f'") + end + + def run_command(command) + send_string(command) + send_keys('enter') + end + + def send_string(str) + tmux_command("send-keys -t 0 -l '#{str.gsub("'", "\\'")}'") + end + + def send_keys(*keys) + tmux_command("send-keys -t 0 #{keys.join(' ')}") + end + + def content(esc_seqs: false) + cmd = 'capture-pane -p -t 0' + cmd += ' -e' if esc_seqs + tmux_command(cmd).strip + end + + def clear + send_keys('C-l') + 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 + + def socket_name + @socket_name ||= SecureRandom.hex(6) + end + + def tmux_command(cmd) + out = `tmux -u -L #{socket_name} #{cmd}` + + raise('tmux error') unless $?.success? + + out + end +end From c22ab0e399c66305b880a1a9e0b4c4b35d72eaac Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 00:59:06 -0700 Subject: [PATCH 04/45] Implement suggestion integration tests in RSpec + tmux --- spec/strategies/default_spec.rb | 132 ++++++++++++++++++++++++ spec/strategies/match_prev_cmd_spec.rb | 60 +++++++++++ test/strategies/default_test.zsh | 56 ---------- test/strategies/match_prev_cmd_test.zsh | 74 ------------- test/strategies_test.zsh | 102 ------------------ 5 files changed, 192 insertions(+), 232 deletions(-) create mode 100644 spec/strategies/default_spec.rb create mode 100644 spec/strategies/match_prev_cmd_spec.rb delete mode 100755 test/strategies/default_test.zsh delete mode 100755 test/strategies/match_prev_cmd_test.zsh delete mode 100644 test/strategies_test.zsh diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb new file mode 100644 index 0000000..36a1221 --- /dev/null +++ b/spec/strategies/default_spec.rb @@ -0,0 +1,132 @@ +describe 'default strategy' do + let(:session) { TerminalSession.new } + + before do + session.run_command('source zsh-autosuggestions.zsh') + session.run_command('fc -p') + session.clear + end + + after do + session.destroy + end + + context 'with some simple history entries' do + before do + session.run_command('ls foo') + session.run_command('ls bar') + + session.clear + end + + it 'suggests nothing if there is no match' do + session.send_string('ls q') + wait_for { session.content }.to eq('ls q') + end + + it 'suggests the most recent matching history item' do + session.send_string('ls') + wait_for { session.content }.to eq('ls bar') + end + end + + xcontext 'with a multiline hist entry' do + before do + session.send_string('echo "') + session.send_keys('enter') + session.send_string('"') + session.send_keys('enter') + + session.clear + end + + it do + session.send_keys('e') + wait_for { session.content }.to eq "echo \"\n\"" + end + end + + context 'with a hist entry with a backslash' do + before do + session.run_command('echo "hello\nworld"') + session.clear + end + + it do + session.send_string('echo "hello\\') + wait_for { session.content }.to eq('echo "hello\nworld"') + end + end + + context 'with a hist entry with double backslashes' do + before do + session.run_command('echo "\\\\"') + session.clear + end + + it do + session.send_string('echo "\\\\') + wait_for { session.content }.to eq('echo "\\\\"') + end + end + + context 'with a hist entry with a tilde' do + before do + session.run_command('ls ~/foo') + session.clear + end + + it do + session.send_string('ls ~') + wait_for { session.content }.to eq('ls ~/foo') + end + + context 'with extended_glob set' do + before do + session.run_command('setopt local_options extended_glob') + session.clear + end + + it do + session.send_string('ls ~') + wait_for { session.content }.to eq('ls ~/foo') + end + end + end + + context 'with a hist entry with parentheses' do + before do + session.run_command('echo "$(ls foo)"') + session.clear + end + + it do + session.send_string('echo "$(') + wait_for { session.content }.to eq('echo "$(ls foo)"') + end + end + + context 'with a hist entry with square brackets' do + before do + session.run_command('echo "$history[123]"') + session.clear + end + + it do + session.send_string('echo "$history[') + wait_for { session.content }.to eq('echo "$history[123]"') + end + end + + context 'with a hist entry with pound sign' do + before do + session.run_command('echo "#yolo"') + session.clear + end + + it do + session.send_string('echo "#') + wait_for { session.content }.to eq('echo "#yolo"') + 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..e038fc7 --- /dev/null +++ b/spec/strategies/match_prev_cmd_spec.rb @@ -0,0 +1,60 @@ +describe 'match_prev_cmd strategy' do + let(:session) { TerminalSession.new } + + before do + session.run_command('source zsh-autosuggestions.zsh') + session.run_command('ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd') + session.run_command('fc -p') + session.clear + end + + after do + session.destroy + end + + context 'with some history entries' do + before do + session.run_command('echo what') + session.run_command('ls foo') + session.run_command('echo what') + session.run_command('ls bar') + session.run_command('ls baz') + + session.clear + end + + it 'suggests nothing if prefix does not match' do + session.send_string('ls q') + wait_for { session.content }.to eq('ls q') + end + + it 'suggests the most recent matching history item' do + session.send_string('ls') + wait_for { session.content }.to eq('ls baz') + end + + it 'suggests the most recent after the previous command' do + session.run_command('echo what') + session.clear + + session.send_string('ls') + wait_for { session.content }.to eq('ls bar') + end + end + + context 'with a multiline hist entry' do + before do + session.send_string('echo "') + session.send_keys('enter') + session.send_string('"') + session.send_keys('enter') + + session.clear + end + + it do + session.send_keys('e') + wait_for { session.content }.to eq "echo \"\n\"" + end + end +end 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" From 48501198874fa93f2ef8e05c9238dfddc2cc2636 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 00:59:26 -0700 Subject: [PATCH 05/45] Add separate test task for RSpec --- Makefile | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index fde3691..27d02db 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ PLUGIN_TARGET := zsh-autosuggestions.zsh SHUNIT2 := $(VENDOR_DIR)/shunit2/2.1.6 STUB_SH := $(VENDOR_DIR)/stub.sh/stub.sh -TEST_PREREQS := \ +UNIT_TEST_PREREQS := \ $(SHUNIT2) \ $(STUB_SH) @@ -43,5 +43,10 @@ clean: rm $(PLUGIN_TARGET) .PHONY: test -test: all $(TEST_PREREQS) - script/test_runner.zsh $(TESTS) +test: rspec unit_test + +unit_test: all $(UNIT_TEST_PREREQS) + script/test_runner.zsh $(UNIT_TESTS) + +rspec: all + bundle exec rspec $(RSPEC_TESTS) From debbffc79a3d1a068c2be7e151abf4a7a1afab64 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 19 Jan 2017 22:29:26 -0700 Subject: [PATCH 06/45] Add rspec test around accepting suggestions --- spec/widgets/accept_spec.rb | 162 ++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 spec/widgets/accept_spec.rb diff --git a/spec/widgets/accept_spec.rb b/spec/widgets/accept_spec.rb new file mode 100644 index 0000000..42e1e99 --- /dev/null +++ b/spec/widgets/accept_spec.rb @@ -0,0 +1,162 @@ +describe 'accept widget' do + let(:session) { TerminalSession.new } + + before do + session.run_command('source zsh-autosuggestions.zsh') + session.run_command(select_keymap) + session.run_command('fc -p') + session.run_command('echo hello world') + + session.clear + + session.send_string('echo') + wait_for { session.content }.to start_with('echo') + end + + after do + session.destroy + end + + describe 'emacs keymap' do + let(:select_keymap) { 'bindkey -e' } + + context 'forward-char' do + subject { session.send_keys('right') } + + context 'when the cursor is at the end of the buffer' do + it 'accepts the suggestion' do + expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') + end + + it 'moves the cursor to the end of the buffer' do + expect { subject }.to change { session.cursor }.from([4, 0]).to([16, 0]) + end + end + + context 'when the cursor is not at the end of the buffer' do + before { 2.times { session.send_keys('left') } } + + it 'does not accept the suggestion' do + expect { subject }.not_to change { session.content(esc_seqs: true) } + end + + it 'moves the cursor forward one character' do + expect { subject }.to change { session.cursor }.from([2, 0]).to([3, 0]) + end + end + end + + context 'end-of-line' do + subject { session.send_keys('C-e') } + + context 'when the cursor is at the end of the buffer' do + it 'accepts the suggestion' do + expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') + end + + it 'moves the cursor to the end of the buffer' do + expect { subject }.to change { session.cursor }.from([4, 0]).to([16, 0]) + end + end + + context 'when the cursor is not at the end of the buffer' do + before { 2.times { session.send_keys('left') } } + + it 'does not accept the suggestion' do + expect { subject }.not_to change { session.content(esc_seqs: true) } + end + + it 'moves the cursor to the end of the line' do + expect { subject }.to change { session.cursor }.from([2, 0]).to([4, 0]) + end + end + end + end + + describe 'vi keymap' do + let(:select_keymap) { 'bindkey -v' } + + before { session.send_keys('escape') } + + context 'vi-forward-char' do + subject { session.send_keys('l') } + + context 'when the cursor is at the end of the buffer' do + it 'accepts the suggestion' do + expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') + end + + it 'moves the cursor to the end of the buffer' do + wait_for { session.cursor }.to eq([3, 0]) + expect { subject }.to change { session.cursor }.from([3, 0]).to([15, 0]) + end + end + + context 'when the cursor is not at the end of the buffer' do + before { 2.times { session.send_keys('h') } } + + it 'does not accept the suggestion' do + expect { subject }.not_to change { session.content(esc_seqs: true) } + end + + it 'moves the cursor forward one character' do + expect { subject }.to change { session.cursor }.from([1, 0]).to([2, 0]) + end + end + end + + context 'vi-end-of-line' do + subject { session.send_keys('$') } + + context 'when the cursor is at the end of the buffer' do + it 'accepts the suggestion' do + expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') + end + + it 'moves the cursor to the end of the buffer' do + wait_for { session.cursor }.to eq([3, 0]) + expect { subject }.to change { session.cursor }.from([3, 0]).to([15, 0]) + end + end + + context 'when the cursor is not at the end of the buffer' do + before { 2.times { session.send_keys('h') } } + + it 'does not accept the suggestion' do + expect { subject }.not_to change { session.content(esc_seqs: true) } + end + + it 'moves the cursor to the end of the line' do + expect { subject }.to change { session.cursor }.from([1, 0]).to([3, 0]) + end + end + end + + context 'vi-add-eol' do + subject { session.send_keys('A') } + + context 'when the cursor is at the end of the buffer' do + it 'accepts the suggestion' do + expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') + end + + it 'moves the cursor to the end of the buffer' do + wait_for { session.cursor }.to eq([3, 0]) + expect { subject }.to change { session.cursor }.from([3, 0]).to([16, 0]) + end + end + + context 'when the cursor is not at the end of the buffer' do + before { 2.times { session.send_keys('h') } } + + it 'does not accept the suggestion' do + expect { subject }.not_to change { session.content(esc_seqs: true) } + end + + it 'moves the cursor to the end of the line' do + expect { subject }.to change { session.cursor }.from([1, 0]).to([4, 0]) + end + end + end + end +end From ab8f29522522007fa22266538af7ef19e2331fa6 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 19 Jul 2016 21:04:18 -0600 Subject: [PATCH 07/45] First pass at async functionality --- Makefile | 4 +- src/async.zsh | 53 ++++++++++++++++++ src/config.zsh | 3 ++ src/setup.zsh | 10 ++++ src/start.zsh | 1 - src/suggestion.zsh | 21 -------- src/util.zsh | 11 ++++ src/widgets.zsh | 28 +++++++--- zsh-autosuggestions.zsh | 115 +++++++++++++++++++++++++++++++--------- 9 files changed, 191 insertions(+), 55 deletions(-) create mode 100644 src/async.zsh create mode 100644 src/setup.zsh delete mode 100644 src/suggestion.zsh create mode 100644 src/util.zsh diff --git a/Makefile b/Makefile index 27d02db..2ea2763 100644 --- a/Makefile +++ b/Makefile @@ -2,13 +2,15 @@ SRC_DIR := ./src VENDOR_DIR := ./vendor SRC_FILES := \ + $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ + $(SRC_DIR)/util.zsh \ $(SRC_DIR)/deprecated.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 := \ diff --git a/src/async.zsh b/src/async.zsh new file mode 100644 index 0000000..a87fd05 --- /dev/null +++ b/src/async.zsh @@ -0,0 +1,53 @@ + +#--------------------------------------------------------------------# +# Async # +#--------------------------------------------------------------------# + +_zsh_autosuggest_async_fetch_suggestion() { + local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + local prefix="$(_zsh_autosuggest_escape_command "$1")" + + # Send the suggestion command to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' +} + +_zsh_autosuggest_async_suggestion_worker() { + local last_pid + + while read -d $'\0' cmd; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null + + # Run suggestion search in the background + print -n -- "$(eval "$cmd")"$'\0' & + + # Save the bg process's id so we can kill later + last_pid=$! + done +} + +_zsh_autosuggest_async_suggestion_ready() { + # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do + while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do + zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" + done +} + +# Recreate the pty to get a fresh list of history events +_zsh_autosuggest_async_recreate_pty() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + # Kill the old pty + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + fi + + # Start a new pty + typeset -h REPLY + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready +} + +add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty diff --git a/src/config.zsh b/src/config.zsh index f519f6f..68d3b32 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -11,6 +11,9 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty + ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion 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..5314e1f 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -9,5 +9,4 @@ _zsh_autosuggest_start() { _zsh_autosuggest_bind_widgets } -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start 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..0dfb3f0 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -19,7 +19,7 @@ _zsh_autosuggest_modify() { 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 @@ -33,18 +33,12 @@ _zsh_autosuggest_modify() { 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")" + _zsh_autosuggest_async_fetch_suggestion "$BUFFER" fi fi - # Add the suggestion to the POSTDISPLAY - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - fi - return $retval } @@ -133,3 +127,21 @@ done zle -N autosuggest-accept _zsh_autosuggest_widget_accept zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute + +_zsh_autosuggest_show_suggestion() { + local suggestion=$1 + + _zsh_autosuggest_highlight_reset + + if [ -n "$suggestion" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY + fi + + _zsh_autosuggest_highlight_apply + + zle -R +} + +zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 3761efe..34d7771 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -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 # #--------------------------------------------------------------------# @@ -37,6 +47,9 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty + ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion @@ -87,6 +100,17 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= +#--------------------------------------------------------------------# +# Utility Functions # +#--------------------------------------------------------------------# + +_zsh_autosuggest_escape_command() { + setopt localoptions EXTENDED_GLOB + + # Escape special chars in the string (requires EXTENDED_GLOB) + echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" +} + #--------------------------------------------------------------------# # Handle Deprecated Variables/Widgets # #--------------------------------------------------------------------# @@ -260,7 +284,7 @@ _zsh_autosuggest_modify() { 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 @@ -274,18 +298,12 @@ _zsh_autosuggest_modify() { 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")" + _zsh_autosuggest_async_fetch_suggestion "$BUFFER" fi fi - # Add the suggestion to the POSTDISPLAY - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - fi - return $retval } @@ -375,26 +393,23 @@ 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 # -#--------------------------------------------------------------------# +_zsh_autosuggest_show_suggestion() { + local suggestion=$1 -# 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" + _zsh_autosuggest_highlight_reset - if [ -n "$functions[$strategy_function]" ]; then - echo -E "$($strategy_function "$escaped_prefix")" + if [ -n "$suggestion" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY fi + + _zsh_autosuggest_highlight_apply + + zle -R } -_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-show-suggestion _zsh_autosuggest_show_suggestion #--------------------------------------------------------------------# # Default Suggestion Strategy # @@ -459,6 +474,59 @@ _zsh_autosuggest_strategy_match_prev_cmd() { echo -E "$history[$histkey]" } +#--------------------------------------------------------------------# +# Async # +#--------------------------------------------------------------------# + +_zsh_autosuggest_async_fetch_suggestion() { + local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + local prefix="$(_zsh_autosuggest_escape_command "$1")" + + # Send the suggestion command to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' +} + +_zsh_autosuggest_async_suggestion_worker() { + local last_pid + + while read -d $'\0' cmd; do + # Kill last bg process + kill -KILL $last_pid &>/dev/null + + # Run suggestion search in the background + print -n -- "$(eval "$cmd")"$'\0' & + + # Save the bg process's id so we can kill later + last_pid=$! + done +} + +_zsh_autosuggest_async_suggestion_ready() { + # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do + while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do + zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" + done +} + +# Recreate the pty to get a fresh list of history events +_zsh_autosuggest_async_recreate_pty() { + typeset -g _ZSH_AUTOSUGGEST_PTY_FD + + # Kill the old pty + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + fi + + # Start a new pty + typeset -h REPLY + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready +} + +add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty + #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------# @@ -469,5 +537,4 @@ _zsh_autosuggest_start() { _zsh_autosuggest_bind_widgets } -autoload -Uz add-zsh-hook add-zsh-hook precmd _zsh_autosuggest_start From e72c2d87e54b819dd085694888f0448d03f488c0 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:53:26 -0700 Subject: [PATCH 08/45] add a bunch of comments --- src/async.zsh | 15 ++++++++++++++- zsh-autosuggestions.zsh | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index a87fd05..c4632d8 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -11,6 +11,7 @@ _zsh_autosuggest_async_fetch_suggestion() { zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' } +# Pty is spawned running this function _zsh_autosuggest_async_suggestion_worker() { local last_pid @@ -26,6 +27,9 @@ _zsh_autosuggest_async_suggestion_worker() { done } +# 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_suggestion_ready() { # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do @@ -39,14 +43,23 @@ _zsh_autosuggest_async_recreate_pty() { # Kill the old pty if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + # Remove the input handler zle -F $_ZSH_AUTOSUGGEST_PTY_FD + + # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null fi - # Start a new pty + # REPLY stores the fd to read from typeset -h REPLY + + # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + + # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + + # Set up input handler from the pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 34d7771..248687f 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -486,6 +486,7 @@ _zsh_autosuggest_async_fetch_suggestion() { zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' } +# Pty is spawned running this function _zsh_autosuggest_async_suggestion_worker() { local last_pid @@ -501,6 +502,9 @@ _zsh_autosuggest_async_suggestion_worker() { done } +# 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_suggestion_ready() { # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do @@ -514,14 +518,23 @@ _zsh_autosuggest_async_recreate_pty() { # Kill the old pty if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + # Remove the input handler zle -F $_ZSH_AUTOSUGGEST_PTY_FD + + # Destroy the pty zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null fi - # Start a new pty + # REPLY stores the fd to read from typeset -h REPLY + + # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + + # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + + # Set up input handler from the pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } From 0308ed797e1e100d7e091376efdba16e63f0a1ce Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:55:38 -0700 Subject: [PATCH 09/45] Rename worker to server --- src/async.zsh | 4 ++-- zsh-autosuggestions.zsh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index c4632d8..b330ccf 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -12,7 +12,7 @@ _zsh_autosuggest_async_fetch_suggestion() { } # Pty is spawned running this function -_zsh_autosuggest_async_suggestion_worker() { +_zsh_autosuggest_async_suggestion_server() { local last_pid while read -d $'\0' cmd; do @@ -54,7 +54,7 @@ _zsh_autosuggest_async_recreate_pty() { typeset -h REPLY # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_server # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 248687f..2b0465a 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -487,7 +487,7 @@ _zsh_autosuggest_async_fetch_suggestion() { } # Pty is spawned running this function -_zsh_autosuggest_async_suggestion_worker() { +_zsh_autosuggest_async_suggestion_server() { local last_pid while read -d $'\0' cmd; do @@ -529,7 +529,7 @@ _zsh_autosuggest_async_recreate_pty() { typeset -h REPLY # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_worker + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_server # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY From fba20b042e89fac0932e01c08a41602e77158ca2 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:56:34 -0700 Subject: [PATCH 10/45] Use %1 instead of tracking pid --- src/async.zsh | 7 +------ zsh-autosuggestions.zsh | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index b330ccf..91c3587 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -13,17 +13,12 @@ _zsh_autosuggest_async_fetch_suggestion() { # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { - local last_pid - while read -d $'\0' cmd; do # Kill last bg process - kill -KILL $last_pid &>/dev/null + kill -KILL %1 &>/dev/null # Run suggestion search in the background print -n -- "$(eval "$cmd")"$'\0' & - - # Save the bg process's id so we can kill later - last_pid=$! done } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 2b0465a..f99953b 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -488,17 +488,12 @@ _zsh_autosuggest_async_fetch_suggestion() { # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { - local last_pid - while read -d $'\0' cmd; do # Kill last bg process - kill -KILL $last_pid &>/dev/null + kill -KILL %1 &>/dev/null # Run suggestion search in the background print -n -- "$(eval "$cmd")"$'\0' & - - # Save the bg process's id so we can kill later - last_pid=$! done } From e33eb570c461e4f0c97d7ce1f2c33a15f2a200b7 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:58:06 -0700 Subject: [PATCH 11/45] Send only the prefix to the suggestion server --- src/async.zsh | 11 ++++------- src/strategies/default.zsh | 4 +++- zsh-autosuggestions.zsh | 15 +++++++-------- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 91c3587..2e23fb6 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -4,21 +4,18 @@ #--------------------------------------------------------------------# _zsh_autosuggest_async_fetch_suggestion() { - local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" - local prefix="$(_zsh_autosuggest_escape_command "$1")" - - # Send the suggestion command to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' + # Send the prefix to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME $1 $'\0' } # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { - while read -d $'\0' cmd; do + while read -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null # Run suggestion search in the background - print -n -- "$(eval "$cmd")"$'\0' & + echo -n -E "$(_zsh_autosuggest_strategy_default "$prefix")"$'\0' & done } diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index 29333f5..a242ee0 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -7,5 +7,7 @@ # _zsh_autosuggest_strategy_default() { - fc -lnrm "$1*" 1 2>/dev/null | head -n 1 + setopt localoptions EXTENDED_GLOB + + fc -lnrm "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}*" 1 2>/dev/null | head -n 1 } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index f99953b..f85aac6 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -419,7 +419,9 @@ zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion # _zsh_autosuggest_strategy_default() { - fc -lnrm "$1*" 1 2>/dev/null | head -n 1 + setopt localoptions EXTENDED_GLOB + + fc -lnrm "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}*" 1 2>/dev/null | head -n 1 } #--------------------------------------------------------------------# @@ -479,21 +481,18 @@ _zsh_autosuggest_strategy_match_prev_cmd() { #--------------------------------------------------------------------# _zsh_autosuggest_async_fetch_suggestion() { - local strategy_function="_zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" - local prefix="$(_zsh_autosuggest_escape_command "$1")" - - # Send the suggestion command to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "$strategy_function '$prefix'"$'\0' + # Send the prefix to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME $1 $'\0' } # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { - while read -d $'\0' cmd; do + while read -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null # Run suggestion search in the background - print -n -- "$(eval "$cmd")"$'\0' & + echo -n -E "$(_zsh_autosuggest_strategy_default "$prefix")"$'\0' & done } From 5c891afd48e3c27faaff86f988816b11fe499dde Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:58:25 -0700 Subject: [PATCH 12/45] Reset zsh options inside pty (from zsh-async) --- src/async.zsh | 2 ++ zsh-autosuggestions.zsh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/async.zsh b/src/async.zsh index 2e23fb6..566a81a 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -10,6 +10,8 @@ _zsh_autosuggest_async_fetch_suggestion() { # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { + emulate -R zsh + while read -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index f85aac6..d8f1103 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -487,6 +487,8 @@ _zsh_autosuggest_async_fetch_suggestion() { # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { + emulate -R zsh + while read -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null From b530b0c99679c00a5234c25f0adfa0d70a522900 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 19:58:52 -0700 Subject: [PATCH 13/45] Use `zpty -r` with pattern matching to fetch suggestion --- src/async.zsh | 8 ++++---- zsh-autosuggestions.zsh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 566a81a..1535297 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -25,10 +25,10 @@ _zsh_autosuggest_async_suggestion_server() { # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_suggestion_ready() { - # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do - while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do - zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" - done + local suggestion + + zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zle _autosuggest-show-suggestion "${suggestion%$'\0'}" } # Recreate the pty to get a fresh list of history events diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index d8f1103..34c1f90 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -502,10 +502,10 @@ _zsh_autosuggest_async_suggestion_server() { # First arg will be fd ready for reading # Second arg will be passed in case of error _zsh_autosuggest_async_suggestion_ready() { - # while zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion 2>/dev/null; do - while read -u $_ZSH_AUTOSUGGEST_PTY_FD -d $'\0' suggestion; do - zle _autosuggest-show-suggestion "${suggestion//$'\r'$'\n'/$'\n'}" - done + local suggestion + + zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zle _autosuggest-show-suggestion "${suggestion%$'\0'}" } # Recreate the pty to get a fresh list of history events From 0337005eb05de7f9f99b1fbb42b8da72eddbf5ff Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 21:58:17 -0700 Subject: [PATCH 14/45] Disable word splitting while reading to preserve whitespace --- src/async.zsh | 4 ++-- zsh-autosuggestions.zsh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 1535297..0e29a3f 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -5,14 +5,14 @@ _zsh_autosuggest_async_fetch_suggestion() { # Send the prefix to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME $1 $'\0' + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' } # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { emulate -R zsh - while read -d $'\0' prefix; do + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 34c1f90..ff3f0ac 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -482,14 +482,14 @@ _zsh_autosuggest_strategy_match_prev_cmd() { _zsh_autosuggest_async_fetch_suggestion() { # Send the prefix to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME $1 $'\0' + zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' } # Pty is spawned running this function _zsh_autosuggest_async_suggestion_server() { emulate -R zsh - while read -d $'\0' prefix; do + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null From e5a5b0c1e0cbd2a8eb3f30543e8891b8ca61d392 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 22:27:09 -0700 Subject: [PATCH 15/45] Output only newlines in the pty --- src/async.zsh | 3 +++ zsh-autosuggestions.zsh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/async.zsh b/src/async.zsh index 0e29a3f..18dbb9d 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -12,6 +12,9 @@ _zsh_autosuggest_async_fetch_suggestion() { _zsh_autosuggest_async_suggestion_server() { emulate -R zsh + # Output only newlines (not carriage return + newline) + stty -onlcr + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index ff3f0ac..449e08d 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -489,6 +489,9 @@ _zsh_autosuggest_async_fetch_suggestion() { _zsh_autosuggest_async_suggestion_server() { emulate -R zsh + # Output only newlines (not carriage return + newline) + stty -onlcr + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null From ab2742537f248f6c807535db7fa121157c593990 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 22:27:47 -0700 Subject: [PATCH 16/45] Quote the suggestion to support sh_split_word option --- src/widgets.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets.zsh b/src/widgets.zsh index 0dfb3f0..c6162b3 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -129,7 +129,7 @@ zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute _zsh_autosuggest_show_suggestion() { - local suggestion=$1 + local suggestion="$1" _zsh_autosuggest_highlight_reset diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 449e08d..246e96d 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -394,7 +394,7 @@ zle -N autosuggest-clear _zsh_autosuggest_widget_clear zle -N autosuggest-execute _zsh_autosuggest_widget_execute _zsh_autosuggest_show_suggestion() { - local suggestion=$1 + local suggestion="$1" _zsh_autosuggest_highlight_reset From b3208b08af4e40fbc6a040b0ad9455f33c236f3b Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 22:48:30 -0700 Subject: [PATCH 17/45] Pass the chosen strategy into the suggestion server pty --- src/async.zsh | 6 ++++-- zsh-autosuggestions.zsh | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 18dbb9d..1d2ae87 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -15,12 +15,14 @@ _zsh_autosuggest_async_suggestion_server() { # Output only newlines (not carriage return + newline) stty -onlcr + local strategy=$1 + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null # Run suggestion search in the background - echo -n -E "$(_zsh_autosuggest_strategy_default "$prefix")"$'\0' & + echo -n -E "$($strategy "$prefix")"$'\0' & done } @@ -51,7 +53,7 @@ _zsh_autosuggest_async_recreate_pty() { typeset -h REPLY # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_server + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 246e96d..2dc5141 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -492,12 +492,14 @@ _zsh_autosuggest_async_suggestion_server() { # Output only newlines (not carriage return + newline) stty -onlcr + local strategy=$1 + while IFS='' read -r -d $'\0' prefix; do # Kill last bg process kill -KILL %1 &>/dev/null # Run suggestion search in the background - echo -n -E "$(_zsh_autosuggest_strategy_default "$prefix")"$'\0' & + echo -n -E "$($strategy "$prefix")"$'\0' & done } @@ -528,7 +530,7 @@ _zsh_autosuggest_async_recreate_pty() { typeset -h REPLY # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME _zsh_autosuggest_async_suggestion_server + zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" # Store the fd so we can destroy this pty later _ZSH_AUTOSUGGEST_PTY_FD=$REPLY From 8e06a54b1c6b53442d6fa47e1614c4b144fbd734 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 22:49:21 -0700 Subject: [PATCH 18/45] Add test for string with "\n" in it --- spec/strategies/default_spec.rb | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb index 36a1221..1b6a4a2 100644 --- a/spec/strategies/default_spec.rb +++ b/spec/strategies/default_spec.rb @@ -46,6 +46,20 @@ describe 'default strategy' do end end + context 'with a newline hist entry' do + before do + session.send_string('echo "\n"') + session.send_keys('enter') + + session.clear + end + + it do + session.send_keys('e') + wait_for { session.content }.to eq 'echo "\n"' + end + end + context 'with a hist entry with a backslash' do before do session.run_command('echo "hello\nworld"') From 0305908adf1f9ee703d545cc18ef77c34092d410 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 23:04:07 -0700 Subject: [PATCH 19/45] Revert `fc` usage in calculating suggestion As far as I know, `fc` makes it impossible to tell whether history items used an actual newline character or the string "\n". Pulling from the `$history` array gives a more accurate representation of the actual command that was run. --- spec/strategies/default_spec.rb | 2 +- src/strategies/default.zsh | 9 ++++++++- zsh-autosuggestions.zsh | 9 ++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb index 1b6a4a2..3987c7e 100644 --- a/spec/strategies/default_spec.rb +++ b/spec/strategies/default_spec.rb @@ -30,7 +30,7 @@ describe 'default strategy' do end end - xcontext 'with a multiline hist entry' do + context 'with a multiline hist entry' do before do session.send_string('echo "') session.send_keys('enter') diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index a242ee0..4246b78 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -9,5 +9,12 @@ _zsh_autosuggest_strategy_default() { setopt localoptions EXTENDED_GLOB - fc -lnrm "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}*" 1 2>/dev/null | head -n 1 + local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" + + # Get the keys of the history items that match + local -a histkeys + histkeys=(${(k)history[(r)$prefix*]}) + + # Echo the value of the first key + echo -E "${history[$histkeys[1]]}" } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 2dc5141..a438c65 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -421,7 +421,14 @@ zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion _zsh_autosuggest_strategy_default() { setopt localoptions EXTENDED_GLOB - fc -lnrm "${1//(#m)[\\()\[\]|*?~]/\\$MATCH}*" 1 2>/dev/null | head -n 1 + local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" + + # Get the keys of the history items that match + local -a histkeys + histkeys=(${(k)history[(r)$prefix*]}) + + # Echo the value of the first key + echo -E "${history[$histkeys[1]]}" } #--------------------------------------------------------------------# From 50e6832b8c5f5b3aff9da8f7aaf5a0b836f27368 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 23:05:50 -0700 Subject: [PATCH 20/45] Escape the prefix passed into the match_prev_cmd strategy --- src/strategies/match_prev_cmd.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh index bf8bdd9..632a24b 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* diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index a438c65..b72f3e2 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -453,7 +453,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* From 21d9eda5dd0fdf74afeb9d452735e4a0c14b7a61 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Tue, 24 Jan 2017 23:59:38 -0700 Subject: [PATCH 21/45] Wrap suggestion fetch command in parens to actually run in background --- src/async.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 1d2ae87..ddc07a5 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -22,7 +22,7 @@ _zsh_autosuggest_async_suggestion_server() { kill -KILL %1 &>/dev/null # Run suggestion search in the background - echo -n -E "$($strategy "$prefix")"$'\0' & + (echo -n -E "$($strategy "$prefix")"$'\0') & done } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index b72f3e2..bcee1ee 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -506,7 +506,7 @@ _zsh_autosuggest_async_suggestion_server() { kill -KILL %1 &>/dev/null # Run suggestion search in the background - echo -n -E "$($strategy "$prefix")"$'\0' & + (echo -n -E "$($strategy "$prefix")"$'\0') & done } From 54e1eee924be6b11db297301d6f95d3d3ddba32b Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 25 Jan 2017 00:00:13 -0700 Subject: [PATCH 22/45] Optimize case where manually typing in a suggestion --- src/widgets.zsh | 11 +++++++++++ zsh-autosuggestions.zsh | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/widgets.zsh b/src/widgets.zsh index c6162b3..66eef58 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -26,6 +26,17 @@ _zsh_autosuggest_modify() { _zsh_autosuggest_invoke_original_widget $@ retval=$? + # 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" diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index bcee1ee..266cdff 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -291,6 +291,17 @@ _zsh_autosuggest_modify() { _zsh_autosuggest_invoke_original_widget $@ retval=$? + # 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" From 6c5cd4233189fb67872a58d21c1368fd1d6d1c9b Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Wed, 25 Jan 2017 00:00:53 -0700 Subject: [PATCH 23/45] Go back to tracking last pid because `kill %1` didn't seem to be working --- src/async.zsh | 5 ++++- zsh-autosuggestions.zsh | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index ddc07a5..f0dbc2c 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -16,13 +16,16 @@ _zsh_autosuggest_async_suggestion_server() { stty -onlcr local strategy=$1 + local last_pid while IFS='' read -r -d $'\0' prefix; do # Kill last bg process - kill -KILL %1 &>/dev/null + kill -KILL $last_pid &>/dev/null # Run suggestion search in the background (echo -n -E "$($strategy "$prefix")"$'\0') & + + last_pid=$! done } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 266cdff..a6cbe05 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -511,13 +511,16 @@ _zsh_autosuggest_async_suggestion_server() { stty -onlcr local strategy=$1 + local last_pid while IFS='' read -r -d $'\0' prefix; do # Kill last bg process - kill -KILL %1 &>/dev/null + kill -KILL $last_pid &>/dev/null # Run suggestion search in the background (echo -n -E "$($strategy "$prefix")"$'\0') & + + last_pid=$! done } From 2dbd261989cfaf813e0aa7311cb8a0bed83bfb13 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 16:04:46 -0700 Subject: [PATCH 24/45] Allow configuring of zsh binary to run integration tests against --- spec/terminal_session.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index 63eaf0d..ca06343 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -1,8 +1,10 @@ require 'securerandom' class TerminalSession + ZSH_BIN = ENV['TEST_ZSH_BIN'] || 'zsh' + def initialize(width: 80, height: 24, prompt: '', term: 'xterm-256color') - tmux_command("new-session -d -x #{width} -y #{height} 'PS1=#{prompt} TERM=#{term} zsh -f'") + tmux_command("new-session -d -x #{width} -y #{height} 'PS1=#{prompt} TERM=#{term} #{ZSH_BIN} -f'") end def run_command(command) From 3f57198d0791dc6a176aab887673dbe6c276abaa Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 16:11:01 -0700 Subject: [PATCH 25/45] Only bind widgets once, on initial sourcing --- src/start.zsh | 2 ++ zsh-autosuggestions.zsh | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/start.zsh b/src/start.zsh index 5314e1f..ab60afe 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -5,6 +5,8 @@ # Start the autosuggestion widgets _zsh_autosuggest_start() { + add-zsh-hook -d precmd _zsh_autosuggest_start + _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index a6cbe05..c635630 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -568,6 +568,8 @@ add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty # Start the autosuggestion widgets _zsh_autosuggest_start() { + add-zsh-hook -d precmd _zsh_autosuggest_start + _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets } From 78ba07179a38584ed6756307f329d64da03b0638 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 16:35:33 -0700 Subject: [PATCH 26/45] Add feature detection Checks whether `zpty` gives a file descriptor, which was not the case in older versions of zsh. Based on https://github.com/mafredri/zsh-async/blob/a4b2f81c966a00eeb38876d505d97423cc8addfd/async.zsh#L395-L401 --- Makefile | 1 + src/features.zsh | 19 +++++++++++++++++++ src/start.zsh | 1 + zsh-autosuggestions.zsh | 20 ++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 src/features.zsh diff --git a/Makefile b/Makefile index 2ea2763..63d8020 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ SRC_FILES := \ $(SRC_DIR)/setup.zsh \ $(SRC_DIR)/config.zsh \ $(SRC_DIR)/util.zsh \ + $(SRC_DIR)/features.zsh \ $(SRC_DIR)/deprecated.zsh \ $(SRC_DIR)/bind.zsh \ $(SRC_DIR)/highlight.zsh \ diff --git a/src/features.zsh b/src/features.zsh new file mode 100644 index 0000000..cd960e5 --- /dev/null +++ b/src/features.zsh @@ -0,0 +1,19 @@ + +#--------------------------------------------------------------------# +# Feature Detection # +#--------------------------------------------------------------------# + +_zsh_autosuggest_feature_detect() { + typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD + typeset -h REPLY + + zpty $ZSH_AUTOSUGGEST_PTY_NAME : + + if (( REPLY )); then + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 + else + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 + fi + + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME +} diff --git a/src/start.zsh b/src/start.zsh index ab60afe..0633da3 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -7,6 +7,7 @@ _zsh_autosuggest_start() { add-zsh-hook -d precmd _zsh_autosuggest_start + _zsh_autosuggest_feature_detect _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index c635630..0d17ef7 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -111,6 +111,25 @@ _zsh_autosuggest_escape_command() { echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}" } +#--------------------------------------------------------------------# +# Feature Detection # +#--------------------------------------------------------------------# + +_zsh_autosuggest_feature_detect() { + typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD + typeset -h REPLY + + zpty $ZSH_AUTOSUGGEST_PTY_NAME : + + if (( REPLY )); then + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 + else + _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 + fi + + zpty -d $ZSH_AUTOSUGGEST_PTY_NAME +} + #--------------------------------------------------------------------# # Handle Deprecated Variables/Widgets # #--------------------------------------------------------------------# @@ -570,6 +589,7 @@ add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty _zsh_autosuggest_start() { add-zsh-hook -d precmd _zsh_autosuggest_start + _zsh_autosuggest_feature_detect _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets } From f33b605a63b3d64357fb2f390d07349de9d85013 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 16:37:42 -0700 Subject: [PATCH 27/45] Move async initialization into `start` function to keep in one place --- src/async.zsh | 2 -- src/start.zsh | 3 +++ zsh-autosuggestions.zsh | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index f0dbc2c..81b3268 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -64,5 +64,3 @@ _zsh_autosuggest_async_recreate_pty() { # Set up input handler from the pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } - -add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty diff --git a/src/start.zsh b/src/start.zsh index 0633da3..7a1725e 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -10,6 +10,9 @@ _zsh_autosuggest_start() { _zsh_autosuggest_feature_detect _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets + + _zsh_autosuggest_async_recreate_pty + add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty } add-zsh-hook precmd _zsh_autosuggest_start diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 0d17ef7..efdf6d2 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -579,8 +579,6 @@ _zsh_autosuggest_async_recreate_pty() { zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } -add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty - #--------------------------------------------------------------------# # Start # #--------------------------------------------------------------------# @@ -592,6 +590,9 @@ _zsh_autosuggest_start() { _zsh_autosuggest_feature_detect _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets + + _zsh_autosuggest_async_recreate_pty + add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty } add-zsh-hook precmd _zsh_autosuggest_start From 16666da4884a334ccb4c457c63be3f9d0fa00fec Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 16:50:19 -0700 Subject: [PATCH 28/45] Handle versions of zsh where zpty does not set REPLY to fd of opened pty Based on https://github.com/mafredri/zsh-async/blob/e702ec4697ab6469b8948d599768227304d146b1/async.zsh#L400-L406 --- src/async.zsh | 16 ++++++++++++++-- zsh-autosuggestions.zsh | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 81b3268..eadd9c7 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -52,14 +52,26 @@ _zsh_autosuggest_async_recreate_pty() { zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null fi - # REPLY stores the fd to read from + # 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 + # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" # Store the fd so we can destroy this pty later - _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + if (( REPLY )); then + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + else + _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd + fi + # Set up input handler from the pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index efdf6d2..64bfd34 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -566,14 +566,26 @@ _zsh_autosuggest_async_recreate_pty() { zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null fi - # REPLY stores the fd to read from + # 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 + # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" # Store the fd so we can destroy this pty later - _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + if (( REPLY )); then + _ZSH_AUTOSUGGEST_PTY_FD=$REPLY + else + _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd + fi + # Set up input handler from the pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready From 40bb2e78046f715eac8ff4a0f5a60ad4513cd1b0 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 26 Jan 2017 17:00:56 -0700 Subject: [PATCH 29/45] little cleanup --- src/async.zsh | 3 +-- zsh-autosuggestions.zsh | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index eadd9c7..3e61a0b 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -65,14 +65,13 @@ _zsh_autosuggest_async_recreate_pty() { # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" - # Store the fd so we can destroy this pty later + # 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 pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 64bfd34..d6b6377 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -579,14 +579,13 @@ _zsh_autosuggest_async_recreate_pty() { # Start a new pty running the server function zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" - # Store the fd so we can destroy this pty later + # 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 pty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready } From 89dd69d517161f84c7b5858ae7bfde81026d8836 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 27 Jan 2017 14:06:16 -0700 Subject: [PATCH 30/45] Add pry gem for debugging support --- Gemfile | 1 + Gemfile.lock | 10 +++++++++- spec/spec_helper.rb | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index bb4eb7f..c8090be 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,4 @@ source 'https://rubygems.org' gem 'rspec' gem 'rspec-wait' +gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index 5ad7873..75b649b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,13 @@ GEM remote: https://rubygems.org/ specs: + 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) rspec (3.5.0) rspec-core (~> 3.5.0) rspec-expectations (~> 3.5.0) @@ -17,13 +23,15 @@ GEM rspec-support (3.5.0) rspec-wait (0.0.9) rspec (>= 3, < 4) + slop (3.6.0) PLATFORMS ruby DEPENDENCIES + pry rspec rspec-wait BUNDLED WITH - 1.12.5 + 1.13.6 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b3882d7..2b8eb13 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +require 'pry' require 'rspec/wait' require 'terminal_session' From c3425870f1bb6b0e8109d9808392f55ff6122fab Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 27 Jan 2017 14:07:06 -0700 Subject: [PATCH 31/45] Wait for the terminal.clear to go through before continuing Prevents some flakiness in tests --- spec/strategies/default_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb index 3987c7e..e4bdb2f 100644 --- a/spec/strategies/default_spec.rb +++ b/spec/strategies/default_spec.rb @@ -5,6 +5,8 @@ describe 'default strategy' do session.run_command('source zsh-autosuggestions.zsh') session.run_command('fc -p') session.clear + + wait_for { session.content }.to eq('') end after do From e3eb286ea213dbcac4f9c22f37ba084dd9e22024 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 27 Jan 2017 15:18:26 -0700 Subject: [PATCH 32/45] Lots of little async cleanups --- src/async.zsh | 66 +++++++++------ src/config.zsh | 3 + src/start.zsh | 6 +- src/strategies/default.zsh | 4 +- src/strategies/match_prev_cmd.zsh | 4 +- src/widgets.zsh | 48 ++++++----- zsh-autosuggestions.zsh | 131 ++++++++++++++++++------------ 7 files changed, 160 insertions(+), 102 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index 3e61a0b..fd81bca 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -3,13 +3,8 @@ # Async # #--------------------------------------------------------------------# -_zsh_autosuggest_async_fetch_suggestion() { - # Send the prefix to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' -} - # Pty is spawned running this function -_zsh_autosuggest_async_suggestion_server() { +_zsh_autosuggest_async_server() { emulate -R zsh # Output only newlines (not carriage return + newline) @@ -18,40 +13,37 @@ _zsh_autosuggest_async_suggestion_server() { local strategy=$1 local last_pid - while IFS='' read -r -d $'\0' prefix; do + while IFS='' read -r -d $'\0' query; do # Kill last bg process kill -KILL $last_pid &>/dev/null # Run suggestion search in the background - (echo -n -E "$($strategy "$prefix")"$'\0') & + ( + local suggestion + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" + echo -n -E "$suggestion"$'\0' + ) & last_pid=$! done } +_zsh_autosuggest_async_request() { + # Send the query to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_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_suggestion_ready() { +_zsh_autosuggest_async_response() { local suggestion zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null - zle _autosuggest-show-suggestion "${suggestion%$'\0'}" + zle autosuggest-suggest "${suggestion%$'\0'}" } -# Recreate the pty to get a fresh list of history events -_zsh_autosuggest_async_recreate_pty() { - typeset -g _ZSH_AUTOSUGGEST_PTY_FD - - # Kill the old pty - if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then - # Remove the input handler - zle -F $_ZSH_AUTOSUGGEST_PTY_FD - - # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null - fi - +_zsh_autosuggest_async_pty_create() { # With newer versions of zsh, REPLY stores the fd to read from typeset -h REPLY @@ -63,7 +55,7 @@ _zsh_autosuggest_async_recreate_pty() { fi # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + zpty -b $ZSH_AUTOSUGGEST_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 @@ -73,5 +65,29 @@ _zsh_autosuggest_async_recreate_pty() { fi # Set up input handler from the pty - zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response +} + +_zsh_autosuggest_async_pty_destroy() { + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_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_async_pty_create + + # 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/config.zsh b/src/config.zsh index 68d3b32..29806ea 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -63,3 +63,6 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= + +# Use asynchronous mode by default. Unset this variable to use sync mode. +ZSH_AUTOSUGGEST_USE_ASYNC= diff --git a/src/start.zsh b/src/start.zsh index 7a1725e..49af555 100644 --- a/src/start.zsh +++ b/src/start.zsh @@ -11,8 +11,10 @@ _zsh_autosuggest_start() { _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets - _zsh_autosuggest_async_recreate_pty - add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty + if [ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]; then + _zsh_autosuggest_async_start + fi } +# 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 4246b78..bbd5e59 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -15,6 +15,6 @@ _zsh_autosuggest_strategy_default() { local -a histkeys histkeys=(${(k)history[(r)$prefix*]}) - # Echo the value of the first key - echo -E "${history[$histkeys[1]]}" + # Give back the value of the first key + suggestion="${history[$histkeys[1]]}" } diff --git a/src/strategies/match_prev_cmd.zsh b/src/strategies/match_prev_cmd.zsh index 632a24b..ee26346 100644 --- a/src/strategies/match_prev_cmd.zsh +++ b/src/strategies/match_prev_cmd.zsh @@ -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/widgets.zsh b/src/widgets.zsh index 66eef58..0f0520a 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -46,13 +46,35 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if [ $#BUFFER -gt 0 ]; then if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then - _zsh_autosuggest_async_fetch_suggestion "$BUFFER" + _zsh_autosuggest_fetch fi fi return $retval } +# Fetch a new suggestion based on what's currently in the buffer +_zsh_autosuggest_fetch() { + if zpty -t "$ZSH_AUTOSUGGEST_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" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY + fi +} + # Accept the entire suggestion _zsh_autosuggest_accept() { local -i max_cursor_pos=$#BUFFER @@ -120,7 +142,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; do eval "_zsh_autosuggest_widget_$action() { local -i retval @@ -131,28 +153,14 @@ 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 - -_zsh_autosuggest_show_suggestion() { - local suggestion="$1" - - _zsh_autosuggest_highlight_reset - - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - else - unset POSTDISPLAY - fi - - _zsh_autosuggest_highlight_apply - - zle -R -} - -zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index d6b6377..5d56502 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -100,6 +100,9 @@ ZSH_AUTOSUGGEST_IGNORE_WIDGETS=( # Max size of buffer to trigger autosuggestion. Leave undefined for no upper bound. ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= +# Use asynchronous mode by default. Unset this variable to use sync mode. +ZSH_AUTOSUGGEST_USE_ASYNC= + #--------------------------------------------------------------------# # Utility Functions # #--------------------------------------------------------------------# @@ -330,13 +333,35 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if [ $#BUFFER -gt 0 ]; then if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then - _zsh_autosuggest_async_fetch_suggestion "$BUFFER" + _zsh_autosuggest_fetch fi fi return $retval } +# Fetch a new suggestion based on what's currently in the buffer +_zsh_autosuggest_fetch() { + if zpty -t "$ZSH_AUTOSUGGEST_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" ]; then + POSTDISPLAY="${suggestion#$BUFFER}" + else + unset POSTDISPLAY + fi +} + # Accept the entire suggestion _zsh_autosuggest_accept() { local -i max_cursor_pos=$#BUFFER @@ -404,7 +429,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; do eval "_zsh_autosuggest_widget_$action() { local -i retval @@ -415,32 +440,18 @@ 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 -_zsh_autosuggest_show_suggestion() { - local suggestion="$1" - - _zsh_autosuggest_highlight_reset - - if [ -n "$suggestion" ]; then - POSTDISPLAY="${suggestion#$BUFFER}" - else - unset POSTDISPLAY - fi - - _zsh_autosuggest_highlight_apply - - zle -R -} - -zle -N _autosuggest-show-suggestion _zsh_autosuggest_show_suggestion - #--------------------------------------------------------------------# # Default Suggestion Strategy # #--------------------------------------------------------------------# @@ -457,8 +468,8 @@ _zsh_autosuggest_strategy_default() { local -a histkeys histkeys=(${(k)history[(r)$prefix*]}) - # Echo the value of the first key - echo -E "${history[$histkeys[1]]}" + # Give back the value of the first key + suggestion="${history[$histkeys[1]]}" } #--------------------------------------------------------------------# @@ -509,21 +520,16 @@ _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 # #--------------------------------------------------------------------# -_zsh_autosuggest_async_fetch_suggestion() { - # Send the prefix to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' -} - # Pty is spawned running this function -_zsh_autosuggest_async_suggestion_server() { +_zsh_autosuggest_async_server() { emulate -R zsh # Output only newlines (not carriage return + newline) @@ -532,40 +538,37 @@ _zsh_autosuggest_async_suggestion_server() { local strategy=$1 local last_pid - while IFS='' read -r -d $'\0' prefix; do + while IFS='' read -r -d $'\0' query; do # Kill last bg process kill -KILL $last_pid &>/dev/null # Run suggestion search in the background - (echo -n -E "$($strategy "$prefix")"$'\0') & + ( + local suggestion + _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY "$query" + echo -n -E "$suggestion"$'\0' + ) & last_pid=$! done } +_zsh_autosuggest_async_request() { + # Send the query to the pty to fetch a suggestion + zpty -w -n $ZSH_AUTOSUGGEST_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_suggestion_ready() { +_zsh_autosuggest_async_response() { local suggestion zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null - zle _autosuggest-show-suggestion "${suggestion%$'\0'}" + zle autosuggest-suggest "${suggestion%$'\0'}" } -# Recreate the pty to get a fresh list of history events -_zsh_autosuggest_async_recreate_pty() { - typeset -g _ZSH_AUTOSUGGEST_PTY_FD - - # Kill the old pty - if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then - # Remove the input handler - zle -F $_ZSH_AUTOSUGGEST_PTY_FD - - # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null - fi - +_zsh_autosuggest_async_pty_create() { # With newer versions of zsh, REPLY stores the fd to read from typeset -h REPLY @@ -577,7 +580,7 @@ _zsh_autosuggest_async_recreate_pty() { fi # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_suggestion_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + zpty -b $ZSH_AUTOSUGGEST_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 @@ -587,7 +590,31 @@ _zsh_autosuggest_async_recreate_pty() { fi # Set up input handler from the pty - zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_suggestion_ready + zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response +} + +_zsh_autosuggest_async_pty_destroy() { + if [ -n "$_ZSH_AUTOSUGGEST_PTY_FD" ]; then + # Remove the input handler + zle -F $_ZSH_AUTOSUGGEST_PTY_FD + + # Destroy the pty + zpty -d $ZSH_AUTOSUGGEST_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_async_pty_create + + # We recreate the pty to get a fresh list of history events + add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate } #--------------------------------------------------------------------# @@ -602,8 +629,10 @@ _zsh_autosuggest_start() { _zsh_autosuggest_check_deprecated_config _zsh_autosuggest_bind_widgets - _zsh_autosuggest_async_recreate_pty - add-zsh-hook precmd _zsh_autosuggest_async_recreate_pty + if [ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]; then + _zsh_autosuggest_async_start + fi } +# Start the autosuggestion widgets on the next precmd add-zsh-hook precmd _zsh_autosuggest_start From 2c465a932a3cac2cbdfe9defa7ca8640e48cb33b Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 29 Jan 2017 10:39:07 -0700 Subject: [PATCH 33/45] Rename async pty name config var --- src/async.zsh | 8 ++++---- src/config.zsh | 6 +++--- src/features.zsh | 4 ++-- src/widgets.zsh | 2 +- zsh-autosuggestions.zsh | 20 ++++++++++---------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/async.zsh b/src/async.zsh index fd81bca..40de015 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -30,7 +30,7 @@ _zsh_autosuggest_async_server() { _zsh_autosuggest_async_request() { # Send the query to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' } # Called when new data is ready to be read from the pty @@ -39,7 +39,7 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { local suggestion - zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null zle autosuggest-suggest "${suggestion%$'\0'}" } @@ -55,7 +55,7 @@ _zsh_autosuggest_async_pty_create() { fi # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + 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 @@ -74,7 +74,7 @@ _zsh_autosuggest_async_pty_destroy() { zle -F $_ZSH_AUTOSUGGEST_PTY_FD # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null fi } diff --git a/src/config.zsh b/src/config.zsh index 29806ea..a9f02e6 100644 --- a/src/config.zsh +++ b/src/config.zsh @@ -11,9 +11,6 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- -# Pty name for calculating autosuggestions asynchronously -ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty - ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion @@ -66,3 +63,6 @@ ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Use asynchronous mode by default. Unset this variable to use sync mode. ZSH_AUTOSUGGEST_USE_ASYNC= + +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty diff --git a/src/features.zsh b/src/features.zsh index cd960e5..9198085 100644 --- a/src/features.zsh +++ b/src/features.zsh @@ -7,7 +7,7 @@ _zsh_autosuggest_feature_detect() { typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD typeset -h REPLY - zpty $ZSH_AUTOSUGGEST_PTY_NAME : + zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME : if (( REPLY )); then _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 @@ -15,5 +15,5 @@ _zsh_autosuggest_feature_detect() { _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 fi - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME } diff --git a/src/widgets.zsh b/src/widgets.zsh index 0f0520a..bbf9c3f 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -55,7 +55,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if zpty -t "$ZSH_AUTOSUGGEST_PTY_NAME" &>/dev/null; then + if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 5d56502..d0003e2 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -47,9 +47,6 @@ ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=8' # Prefix to use when saving original versions of bound widgets ZSH_AUTOSUGGEST_ORIGINAL_WIDGET_PREFIX=autosuggest-orig- -# Pty name for calculating autosuggestions asynchronously -ZSH_AUTOSUGGEST_PTY_NAME=zsh_autosuggest_pty - ZSH_AUTOSUGGEST_STRATEGY=default # Widgets that clear the suggestion @@ -103,6 +100,9 @@ ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE= # Use asynchronous mode by default. Unset this variable to use sync mode. ZSH_AUTOSUGGEST_USE_ASYNC= +# Pty name for calculating autosuggestions asynchronously +ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty + #--------------------------------------------------------------------# # Utility Functions # #--------------------------------------------------------------------# @@ -122,7 +122,7 @@ _zsh_autosuggest_feature_detect() { typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD typeset -h REPLY - zpty $ZSH_AUTOSUGGEST_PTY_NAME : + zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME : if (( REPLY )); then _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 @@ -130,7 +130,7 @@ _zsh_autosuggest_feature_detect() { _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0 fi - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME } #--------------------------------------------------------------------# @@ -342,7 +342,7 @@ _zsh_autosuggest_modify() { # Fetch a new suggestion based on what's currently in the buffer _zsh_autosuggest_fetch() { - if zpty -t "$ZSH_AUTOSUGGEST_PTY_NAME" &>/dev/null; then + if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then _zsh_autosuggest_async_request "$BUFFER" else local suggestion @@ -555,7 +555,7 @@ _zsh_autosuggest_async_server() { _zsh_autosuggest_async_request() { # Send the query to the pty to fetch a suggestion - zpty -w -n $ZSH_AUTOSUGGEST_PTY_NAME "${1}"$'\0' + zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' } # Called when new data is ready to be read from the pty @@ -564,7 +564,7 @@ _zsh_autosuggest_async_request() { _zsh_autosuggest_async_response() { local suggestion - zpty -rt $ZSH_AUTOSUGGEST_PTY_NAME suggestion '*'$'\0' 2>/dev/null + zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null zle autosuggest-suggest "${suggestion%$'\0'}" } @@ -580,7 +580,7 @@ _zsh_autosuggest_async_pty_create() { fi # Start a new pty running the server function - zpty -b $ZSH_AUTOSUGGEST_PTY_NAME "_zsh_autosuggest_async_server _zsh_autosuggest_strategy_$ZSH_AUTOSUGGEST_STRATEGY" + 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 @@ -599,7 +599,7 @@ _zsh_autosuggest_async_pty_destroy() { zle -F $_ZSH_AUTOSUGGEST_PTY_FD # Destroy the pty - zpty -d $ZSH_AUTOSUGGEST_PTY_NAME &>/dev/null + zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null fi } From 5151adfe400c8a0ccefe7a4f440ae0fb6f5f67a0 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 29 Jan 2017 10:40:05 -0700 Subject: [PATCH 34/45] Make TerminalSession#clear block until the screen is cleared --- spec/terminal_session.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index ca06343..ee0bca1 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -28,6 +28,7 @@ class TerminalSession def clear send_keys('C-l') + sleep(0.1) until content == '' end def destroy From 51e8755634a5ad3651ab3758d898065d769960eb Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 29 Jan 2017 10:42:28 -0700 Subject: [PATCH 35/45] TerminalSession methods return self to support chaining --- spec/terminal_session.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index ee0bca1..733728b 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -10,14 +10,20 @@ class TerminalSession 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 content(esc_seqs: false) @@ -29,6 +35,8 @@ class TerminalSession def clear send_keys('C-l') sleep(0.1) until content == '' + + self end def destroy From 98f926d53d4982490e04c8f2b8bb9a24e4afb676 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 29 Jan 2017 10:43:00 -0700 Subject: [PATCH 36/45] Clean up TerminalSession constructor a bit --- spec/terminal_session.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index 733728b..bbadb9a 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -3,8 +3,17 @@ require 'securerandom' class TerminalSession ZSH_BIN = ENV['TEST_ZSH_BIN'] || 'zsh' - def initialize(width: 80, height: 24, prompt: '', term: 'xterm-256color') - tmux_command("new-session -d -x #{width} -y #{height} 'PS1=#{prompt} TERM=#{term} #{ZSH_BIN} -f'") + def initialize(opts = {}) + opts = { + width: 80, + height: 24, + prompt: '', + term: 'xterm-256color', + zsh_bin: ZSH_BIN + }.merge(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 run_command(command) From 64e7ec5bf8ce1d0650eb7e7f0764b6de91db1033 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Sun, 29 Jan 2017 10:43:20 -0700 Subject: [PATCH 37/45] Rename internal term session method --- spec/terminal_session.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index bbadb9a..8eeac32 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -61,12 +61,12 @@ class TerminalSession private - def socket_name - @socket_name ||= SecureRandom.hex(6) + def tmux_socket_name + @tmux_socket_name ||= SecureRandom.hex(6) end def tmux_command(cmd) - out = `tmux -u -L #{socket_name} #{cmd}` + out = `tmux -u -L #{tmux_socket_name} #{cmd}` raise('tmux error') unless $?.success? From 38eb7cdafdc7264706e804e78e997f64379152c5 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 16 Feb 2017 19:07:41 -0700 Subject: [PATCH 38/45] Update license date --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ed8056c5e80c847709d7e76fbd634c2b8d61fd0d Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 16 Feb 2017 19:18:03 -0700 Subject: [PATCH 39/45] Lots of async changes --- .gitmodules | 6 - .rspec | 1 + .rubocop.yml | 30 ++++ Makefile | 23 +-- script/test_runner.zsh | 54 ------- spec/integrations/client_zpty_spec.rb | 11 ++ spec/multi_line_spec.rb | 13 ++ spec/options/async_zpty_name_spec.rb | 15 ++ spec/options/buffer_max_size_spec.rb | 30 ++++ spec/options/highlight_style_spec.rb | 7 + spec/options/original_widget_prefix_spec.rb | 7 + spec/options/strategy_spec.rb | 20 +++ spec/options/use_async_spec.rb | 7 + spec/options/widget_lists_spec.rb | 72 +++++++++ spec/spec_helper.rb | 35 +++++ spec/special_characters_spec.rb | 55 +++++++ spec/strategies/default_spec.rb | 146 +----------------- spec/strategies/match_prev_cmd_spec.rb | 65 ++------ spec/terminal_session.rb | 4 +- spec/widgets/accept_spec.rb | 162 -------------------- src/async.zsh | 24 ++- src/strategies/default.zsh | 19 ++- src/widgets.zsh | 2 +- test/bind_test.zsh | 45 ------ test/highlight_test.zsh | 73 --------- test/suggestion_test.zsh | 46 ------ test/test_helper.zsh | 60 -------- test/widgets/accept_test.zsh | 161 ------------------- test/widgets/clear_test.zsh | 77 ---------- test/widgets/execute_test.zsh | 26 ---- test/widgets/modify_test.zsh | 88 ----------- test/widgets/partial_accept_test.zsh | 84 ---------- vendor/shunit2 | 1 - vendor/stub.sh | 1 - zsh-autosuggestions.zsh | 47 ++++-- 35 files changed, 384 insertions(+), 1133 deletions(-) delete mode 100644 .gitmodules create mode 100644 .rubocop.yml delete mode 100755 script/test_runner.zsh create mode 100644 spec/integrations/client_zpty_spec.rb create mode 100644 spec/multi_line_spec.rb create mode 100644 spec/options/async_zpty_name_spec.rb create mode 100644 spec/options/buffer_max_size_spec.rb create mode 100644 spec/options/highlight_style_spec.rb create mode 100644 spec/options/original_widget_prefix_spec.rb create mode 100644 spec/options/strategy_spec.rb create mode 100644 spec/options/use_async_spec.rb create mode 100644 spec/options/widget_lists_spec.rb create mode 100644 spec/special_characters_spec.rb delete mode 100644 spec/widgets/accept_spec.rb delete mode 100644 test/bind_test.zsh delete mode 100644 test/highlight_test.zsh delete mode 100644 test/suggestion_test.zsh delete mode 100644 test/test_helper.zsh delete mode 100644 test/widgets/accept_test.zsh delete mode 100644 test/widgets/clear_test.zsh delete mode 100644 test/widgets/execute_test.zsh delete mode 100644 test/widgets/modify_test.zsh delete mode 100644 test/widgets/partial_accept_test.zsh delete mode 160000 vendor/shunit2 delete mode 160000 vendor/stub.sh 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 index 83e16f8..43ae203 100644 --- a/.rspec +++ b/.rspec @@ -1,2 +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/Makefile b/Makefile index 63d8020..c7aaf1d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ SRC_DIR := ./src -VENDOR_DIR := ./vendor SRC_FILES := \ $(SRC_DIR)/setup.zsh \ @@ -22,34 +21,16 @@ HEADER_FILES := \ PLUGIN_TARGET := zsh-autosuggestions.zsh -SHUNIT2 := $(VENDOR_DIR)/shunit2/2.1.6 -STUB_SH := $(VENDOR_DIR)/stub.sh/stub.sh - -UNIT_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: rspec unit_test - -unit_test: all $(UNIT_TEST_PREREQS) - script/test_runner.zsh $(UNIT_TESTS) - -rspec: all - bundle exec rspec $(RSPEC_TESTS) +test: all + bundle exec rspec $(TESTS) 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/client_zpty_spec.rb b/spec/integrations/client_zpty_spec.rb new file mode 100644 index 0000000..f294a1c --- /dev/null +++ b/spec/integrations/client_zpty_spec.rb @@ -0,0 +1,11 @@ +describe 'a running zpty command' do + it 'is not affected by running zsh-autosuggestions' do + session.run_command('zmodload zsh/zpty') + session.run_command('zpty -b kitty cat') + session.run_command('zpty -w kitty cat') + sleep 1 + session.run_command('zpty -r kitty') + + wait_for(session.content).to end_with("\ncat") + 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..c768f54 --- /dev/null +++ b/spec/options/async_zpty_name_spec.rb @@ -0,0 +1,15 @@ +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) { ['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 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..8b9ebab --- /dev/null +++ b/spec/options/use_async_spec.rb @@ -0,0 +1,7 @@ +describe 'suggestion fetching' do + it 'is performed asynchronously' + + context 'when ZSH_AUTOSUGGEST_USE_ASYNC is unset' do + it 'is performed synchronously' + end +end diff --git a/spec/options/widget_lists_spec.rb b/spec/options/widget_lists_spec.rb new file mode 100644 index 0000000..3e03acf --- /dev/null +++ b/spec/options/widget_lists_spec.rb @@ -0,0 +1,72 @@ +describe 'a zle widget' do + let(:before_sourcing) { -> { session.run_command('my-widget() {}; zle -N my-widget; bindkey ^B my-widget') } } + + context 'when added to ZSH_AUTOSUGGEST_ACCEPT_WIDGETS' do + let(:options) { ['ZSH_AUTOSUGGEST_ACCEPT_WIDGETS=(my-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=(my-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=(my-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 +end + +describe 'a zle widget that moves the cursor forward' do + let(:before_sourcing) { -> { session.run_command('my-widget() { zle forward-char }; zle -N my-widget; bindkey ^B my-widget') } } + + context 'when added to ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS' do + let(:options) { ['ZSH_AUTOSUGGEST_PARTIAL_ACCEPT_WIDGETS=(my-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 + +describe 'a builtin zle widget' do + let(:widget) { 'beep' } + + 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("\nbuiltin") + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2b8eb13..64115d2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,39 @@ 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 @@ -12,4 +45,6 @@ RSpec.configure do |config| 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..21f681b --- /dev/null +++ b/spec/special_characters_spec.rb @@ -0,0 +1,55 @@ +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 + end +end diff --git a/spec/strategies/default_spec.rb b/spec/strategies/default_spec.rb index e4bdb2f..94f3450 100644 --- a/spec/strategies/default_spec.rb +++ b/spec/strategies/default_spec.rb @@ -1,148 +1,8 @@ -describe 'default strategy' do - let(:session) { TerminalSession.new } - - before do - session.run_command('source zsh-autosuggestions.zsh') - session.run_command('fc -p') - session.clear - - wait_for { session.content }.to eq('') - end - - after do - session.destroy - end - - context 'with some simple history entries' do - before do - session.run_command('ls foo') - session.run_command('ls bar') - - session.clear - end - - it 'suggests nothing if there is no match' do - session.send_string('ls q') - wait_for { session.content }.to eq('ls q') - end - - it 'suggests the most recent matching history item' do +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 - - context 'with a multiline hist entry' do - before do - session.send_string('echo "') - session.send_keys('enter') - session.send_string('"') - session.send_keys('enter') - - session.clear - end - - it do - session.send_keys('e') - wait_for { session.content }.to eq "echo \"\n\"" - end - end - - context 'with a newline hist entry' do - before do - session.send_string('echo "\n"') - session.send_keys('enter') - - session.clear - end - - it do - session.send_keys('e') - wait_for { session.content }.to eq 'echo "\n"' - end - end - - context 'with a hist entry with a backslash' do - before do - session.run_command('echo "hello\nworld"') - session.clear - end - - it do - session.send_string('echo "hello\\') - wait_for { session.content }.to eq('echo "hello\nworld"') - end - end - - context 'with a hist entry with double backslashes' do - before do - session.run_command('echo "\\\\"') - session.clear - end - - it do - session.send_string('echo "\\\\') - wait_for { session.content }.to eq('echo "\\\\"') - end - end - - context 'with a hist entry with a tilde' do - before do - session.run_command('ls ~/foo') - session.clear - end - - it do - session.send_string('ls ~') - wait_for { session.content }.to eq('ls ~/foo') - end - - context 'with extended_glob set' do - before do - session.run_command('setopt local_options extended_glob') - session.clear - end - - it do - session.send_string('ls ~') - wait_for { session.content }.to eq('ls ~/foo') - end - end - end - - context 'with a hist entry with parentheses' do - before do - session.run_command('echo "$(ls foo)"') - session.clear - end - - it do - session.send_string('echo "$(') - wait_for { session.content }.to eq('echo "$(ls foo)"') - end - end - - context 'with a hist entry with square brackets' do - before do - session.run_command('echo "$history[123]"') - session.clear - end - - it do - session.send_string('echo "$history[') - wait_for { session.content }.to eq('echo "$history[123]"') - end - end - - context 'with a hist entry with pound sign' do - before do - session.run_command('echo "#yolo"') - session.clear - end - - it do - session.send_string('echo "#') - wait_for { session.content }.to eq('echo "#yolo"') - end - end end diff --git a/spec/strategies/match_prev_cmd_spec.rb b/spec/strategies/match_prev_cmd_spec.rb index e038fc7..21be712 100644 --- a/spec/strategies/match_prev_cmd_spec.rb +++ b/spec/strategies/match_prev_cmd_spec.rb @@ -1,60 +1,17 @@ -describe 'match_prev_cmd strategy' do - let(:session) { TerminalSession.new } - - before do - session.run_command('source zsh-autosuggestions.zsh') - session.run_command('ZSH_AUTOSUGGEST_STRATEGY=match_prev_cmd') - session.run_command('fc -p') - session.clear - end - - after do - session.destroy - end - - context 'with some history entries' do - before do - session.run_command('echo what') - session.run_command('ls foo') - session.run_command('echo what') - session.run_command('ls bar') - session.run_command('ls baz') - - session.clear - end - - it 'suggests nothing if prefix does not match' do - session.send_string('ls q') - wait_for { session.content }.to eq('ls q') - end - - it 'suggests the most recent matching history item' do - session.send_string('ls') - wait_for { session.content }.to eq('ls baz') - end - - it 'suggests the most recent after the previous command' do - session.run_command('echo what') - session.clear +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 - - context 'with a multiline hist entry' do - before do - session.send_string('echo "') - session.send_keys('enter') - session.send_string('"') - session.send_keys('enter') - - session.clear - end - - it do - session.send_keys('e') - wait_for { session.content }.to eq "echo \"\n\"" - end - end end diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index 8eeac32..cf4ee84 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -41,7 +41,7 @@ class TerminalSession tmux_command(cmd).strip end - def clear + def clear_screen send_keys('C-l') sleep(0.1) until content == '' @@ -68,7 +68,7 @@ class TerminalSession def tmux_command(cmd) out = `tmux -u -L #{tmux_socket_name} #{cmd}` - raise('tmux error') unless $?.success? + raise("tmux error running: '#{cmd}'") unless $?.success? out end diff --git a/spec/widgets/accept_spec.rb b/spec/widgets/accept_spec.rb deleted file mode 100644 index 42e1e99..0000000 --- a/spec/widgets/accept_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -describe 'accept widget' do - let(:session) { TerminalSession.new } - - before do - session.run_command('source zsh-autosuggestions.zsh') - session.run_command(select_keymap) - session.run_command('fc -p') - session.run_command('echo hello world') - - session.clear - - session.send_string('echo') - wait_for { session.content }.to start_with('echo') - end - - after do - session.destroy - end - - describe 'emacs keymap' do - let(:select_keymap) { 'bindkey -e' } - - context 'forward-char' do - subject { session.send_keys('right') } - - context 'when the cursor is at the end of the buffer' do - it 'accepts the suggestion' do - expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') - end - - it 'moves the cursor to the end of the buffer' do - expect { subject }.to change { session.cursor }.from([4, 0]).to([16, 0]) - end - end - - context 'when the cursor is not at the end of the buffer' do - before { 2.times { session.send_keys('left') } } - - it 'does not accept the suggestion' do - expect { subject }.not_to change { session.content(esc_seqs: true) } - end - - it 'moves the cursor forward one character' do - expect { subject }.to change { session.cursor }.from([2, 0]).to([3, 0]) - end - end - end - - context 'end-of-line' do - subject { session.send_keys('C-e') } - - context 'when the cursor is at the end of the buffer' do - it 'accepts the suggestion' do - expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') - end - - it 'moves the cursor to the end of the buffer' do - expect { subject }.to change { session.cursor }.from([4, 0]).to([16, 0]) - end - end - - context 'when the cursor is not at the end of the buffer' do - before { 2.times { session.send_keys('left') } } - - it 'does not accept the suggestion' do - expect { subject }.not_to change { session.content(esc_seqs: true) } - end - - it 'moves the cursor to the end of the line' do - expect { subject }.to change { session.cursor }.from([2, 0]).to([4, 0]) - end - end - end - end - - describe 'vi keymap' do - let(:select_keymap) { 'bindkey -v' } - - before { session.send_keys('escape') } - - context 'vi-forward-char' do - subject { session.send_keys('l') } - - context 'when the cursor is at the end of the buffer' do - it 'accepts the suggestion' do - expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') - end - - it 'moves the cursor to the end of the buffer' do - wait_for { session.cursor }.to eq([3, 0]) - expect { subject }.to change { session.cursor }.from([3, 0]).to([15, 0]) - end - end - - context 'when the cursor is not at the end of the buffer' do - before { 2.times { session.send_keys('h') } } - - it 'does not accept the suggestion' do - expect { subject }.not_to change { session.content(esc_seqs: true) } - end - - it 'moves the cursor forward one character' do - expect { subject }.to change { session.cursor }.from([1, 0]).to([2, 0]) - end - end - end - - context 'vi-end-of-line' do - subject { session.send_keys('$') } - - context 'when the cursor is at the end of the buffer' do - it 'accepts the suggestion' do - expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') - end - - it 'moves the cursor to the end of the buffer' do - wait_for { session.cursor }.to eq([3, 0]) - expect { subject }.to change { session.cursor }.from([3, 0]).to([15, 0]) - end - end - - context 'when the cursor is not at the end of the buffer' do - before { 2.times { session.send_keys('h') } } - - it 'does not accept the suggestion' do - expect { subject }.not_to change { session.content(esc_seqs: true) } - end - - it 'moves the cursor to the end of the line' do - expect { subject }.to change { session.cursor }.from([1, 0]).to([3, 0]) - end - end - end - - context 'vi-add-eol' do - subject { session.send_keys('A') } - - context 'when the cursor is at the end of the buffer' do - it 'accepts the suggestion' do - expect { subject }.to change { session.content(esc_seqs: true) }.to('echo hello world') - end - - it 'moves the cursor to the end of the buffer' do - wait_for { session.cursor }.to eq([3, 0]) - expect { subject }.to change { session.cursor }.from([3, 0]).to([16, 0]) - end - end - - context 'when the cursor is not at the end of the buffer' do - before { 2.times { session.send_keys('h') } } - - it 'does not accept the suggestion' do - expect { subject }.not_to change { session.content(esc_seqs: true) } - end - - it 'moves the cursor to the end of the line' do - expect { subject }.to change { session.cursor }.from([1, 0]).to([4, 0]) - end - end - end - end -end diff --git a/src/async.zsh b/src/async.zsh index 40de015..da70b80 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -3,10 +3,20 @@ # Async # #--------------------------------------------------------------------# -# Pty is spawned running this function +# 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 @@ -29,7 +39,7 @@ _zsh_autosuggest_async_server() { } _zsh_autosuggest_async_request() { - # Send the query to the pty to fetch a suggestion + # Write the query to the zpty process to fetch a suggestion zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' } @@ -37,10 +47,12 @@ _zsh_autosuggest_async_request() { # 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'}" + zle autosuggest-suggest "${suggestion%%$'\0'##}" } _zsh_autosuggest_async_pty_create() { @@ -54,7 +66,7 @@ _zsh_autosuggest_async_pty_create() { exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi - # Start a new pty running the server function + # 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 @@ -64,7 +76,7 @@ _zsh_autosuggest_async_pty_create() { _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd fi - # Set up input handler from the pty + # Set up input handler from the zpty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response } @@ -73,7 +85,7 @@ _zsh_autosuggest_async_pty_destroy() { # Remove the input handler zle -F $_ZSH_AUTOSUGGEST_PTY_FD - # Destroy the pty + # Destroy the zpty zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null fi } diff --git a/src/strategies/default.zsh b/src/strategies/default.zsh index bbd5e59..60c0494 100644 --- a/src/strategies/default.zsh +++ b/src/strategies/default.zsh @@ -7,14 +7,19 @@ # _zsh_autosuggest_strategy_default() { - setopt localoptions EXTENDED_GLOB + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh - local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB - # Get the keys of the history items that match - local -a histkeys - histkeys=(${(k)history[(r)$prefix*]}) + # 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*]}" - # Give back the value of the first key - suggestion="${history[$histkeys[1]]}" } diff --git a/src/widgets.zsh b/src/widgets.zsh index bbf9c3f..3031066 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -45,7 +45,7 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if [ $#BUFFER -gt 0 ]; then - if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then + if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then _zsh_autosuggest_fetch fi fi 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/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 d0003e2..d21bfd5 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -2,7 +2,7 @@ # https://github.com/zsh-users/zsh-autosuggestions # v0.3.3 # 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 @@ -332,7 +332,7 @@ _zsh_autosuggest_modify() { # Get a new suggestion if the buffer is not empty after modification if [ $#BUFFER -gt 0 ]; then - if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -lt "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then + if [ -z "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" -o $#BUFFER -le "$ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE" ]; then _zsh_autosuggest_fetch fi fi @@ -460,16 +460,21 @@ zle -N autosuggest-execute _zsh_autosuggest_widget_execute # _zsh_autosuggest_strategy_default() { - setopt localoptions EXTENDED_GLOB + # Reset options to defaults and enable LOCAL_OPTIONS + emulate -L zsh - local prefix="${1//(#m)[\\()\[\]|*?~]/\\$MATCH}" + # Enable globbing flags so that we can use (#m) + setopt EXTENDED_GLOB - # Get the keys of the history items that match - local -a histkeys - histkeys=(${(k)history[(r)$prefix*]}) + # 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*]}" - # Give back the value of the first key - suggestion="${history[$histkeys[1]]}" } #--------------------------------------------------------------------# @@ -528,10 +533,20 @@ _zsh_autosuggest_strategy_match_prev_cmd() { # Async # #--------------------------------------------------------------------# -# Pty is spawned running this function +# 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 @@ -554,7 +569,7 @@ _zsh_autosuggest_async_server() { } _zsh_autosuggest_async_request() { - # Send the query to the pty to fetch a suggestion + # Write the query to the zpty process to fetch a suggestion zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0' } @@ -562,10 +577,12 @@ _zsh_autosuggest_async_request() { # 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'}" + zle autosuggest-suggest "${suggestion%%$'\0'##}" } _zsh_autosuggest_async_pty_create() { @@ -579,7 +596,7 @@ _zsh_autosuggest_async_pty_create() { exec {zptyfd}>&- # Close it so it's free to be used by zpty. fi - # Start a new pty running the server function + # 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 @@ -589,7 +606,7 @@ _zsh_autosuggest_async_pty_create() { _ZSH_AUTOSUGGEST_PTY_FD=$zptyfd fi - # Set up input handler from the pty + # Set up input handler from the zpty zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response } @@ -598,7 +615,7 @@ _zsh_autosuggest_async_pty_destroy() { # Remove the input handler zle -F $_ZSH_AUTOSUGGEST_PTY_FD - # Destroy the pty + # Destroy the zpty zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null fi } From 9feac573c932a2598bd406b54aed633d199282fe Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 16 Feb 2017 19:27:32 -0700 Subject: [PATCH 40/45] Do not show any error output from async zpty server process --- src/async.zsh | 4 ++++ zsh-autosuggestions.zsh | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/async.zsh b/src/async.zsh index da70b80..60055b9 100644 --- a/src/async.zsh +++ b/src/async.zsh @@ -20,6 +20,10 @@ _zsh_autosuggest_async_server() { # Output only newlines (not carriage return + newline) stty -onlcr + + # Silence any error messages + exec 2>/dev/null + local strategy=$1 local last_pid diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index d21bfd5..1492f19 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -550,6 +550,10 @@ _zsh_autosuggest_async_server() { # Output only newlines (not carriage return + newline) stty -onlcr + + # Silence any error messages + exec 2>/dev/null + local strategy=$1 local last_pid From 06fca77ffb7d85c8b83d5daa591e3f30e646f1d5 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Thu, 16 Feb 2017 20:12:04 -0700 Subject: [PATCH 41/45] Readme updates for v0.4.0 --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a5c3f3..85cac70 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,10 @@ 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. +### Disable Asynchronous Mode + +As of `v0.4.0`, suggestions are fetched asynchronously using the `zsh/zpty` module. To disable this behavior and fall back to fetching suggestions synchronously, unset the `ZSH_AUTOSUGGEST_USE_ASYNC` variable. + ### Key Bindings @@ -154,9 +158,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 From c959408305ff34b0d40f254ace5adb7e7e64ef3e Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 17 Feb 2017 15:33:09 -0700 Subject: [PATCH 42/45] Only wait a max of 2 seconds for content to match after clearing screen --- spec/terminal_session.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index cf4ee84..275a90b 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -43,7 +43,12 @@ class TerminalSession def clear_screen send_keys('C-l') - sleep(0.1) until content == '' + + i = 0 + until content == opts[:prompt] || i > 20 do + sleep(0.1) + i = i + 1 + end self end From c4bfd8e2c6cf20a5b074b2fcc708d4a7bec8b9f2 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 17 Feb 2017 15:51:50 -0700 Subject: [PATCH 43/45] Need to prevent zpty feature detection from HUPing existing zptys --- spec/integrations/client_zpty_spec.rb | 13 ++++++------- src/features.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/integrations/client_zpty_spec.rb b/spec/integrations/client_zpty_spec.rb index f294a1c..fb7bbeb 100644 --- a/spec/integrations/client_zpty_spec.rb +++ b/spec/integrations/client_zpty_spec.rb @@ -1,11 +1,10 @@ describe 'a running zpty command' do - it 'is not affected by running zsh-autosuggestions' do - session.run_command('zmodload zsh/zpty') - session.run_command('zpty -b kitty cat') - session.run_command('zpty -w kitty cat') - sleep 1 - session.run_command('zpty -r kitty') + let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } } - wait_for(session.content).to end_with("\ncat") + 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/src/features.zsh b/src/features.zsh index 9198085..35dfcc3 100644 --- a/src/features.zsh +++ b/src/features.zsh @@ -7,7 +7,7 @@ _zsh_autosuggest_feature_detect() { typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD typeset -h REPLY - zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME : + zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME '{ zshexit() { kill -KILL $$; sleep 1 } }' if (( REPLY )); then _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index 1492f19..f33c3d2 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -122,7 +122,7 @@ _zsh_autosuggest_feature_detect() { typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD typeset -h REPLY - zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME : + zpty $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME '{ zshexit() { kill -KILL $$; sleep 1 } }' if (( REPLY )); then _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1 From 938144530cd9cafa9a334d1e0b42458f3a3250e2 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 17 Feb 2017 16:01:07 -0700 Subject: [PATCH 44/45] Fix tests --- spec/terminal_session.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/terminal_session.rb b/spec/terminal_session.rb index 275a90b..a089d27 100644 --- a/spec/terminal_session.rb +++ b/spec/terminal_session.rb @@ -12,7 +12,9 @@ class TerminalSession zsh_bin: ZSH_BIN }.merge(opts) - cmd="PS1=#{opts[:prompt]} TERM=#{opts[:term]} #{ZSH_BIN} -f" + @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 @@ -66,6 +68,8 @@ class TerminalSession private + attr_reader :opts + def tmux_socket_name @tmux_socket_name ||= SecureRandom.hex(6) end From 23ef16c297fd17eec9688ff2c3d11bcdab416671 Mon Sep 17 00:00:00 2001 From: Eric Freese Date: Fri, 17 Feb 2017 18:26:34 -0700 Subject: [PATCH 45/45] Do not show suggestions if the buffer is empty --- src/widgets.zsh | 2 +- zsh-autosuggestions.zsh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets.zsh b/src/widgets.zsh index 3031066..a0a59d5 100644 --- a/src/widgets.zsh +++ b/src/widgets.zsh @@ -68,7 +68,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { local suggestion="$1" - if [ -n "$suggestion" ]; then + if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then POSTDISPLAY="${suggestion#$BUFFER}" else unset POSTDISPLAY diff --git a/zsh-autosuggestions.zsh b/zsh-autosuggestions.zsh index f33c3d2..ed631c3 100644 --- a/zsh-autosuggestions.zsh +++ b/zsh-autosuggestions.zsh @@ -355,7 +355,7 @@ _zsh_autosuggest_fetch() { _zsh_autosuggest_suggest() { local suggestion="$1" - if [ -n "$suggestion" ]; then + if [ -n "$suggestion" ] && [ $#BUFFER -gt 0 ]; then POSTDISPLAY="${suggestion#$BUFFER}" else unset POSTDISPLAY