mirror of
https://github.com/zsh-users/zsh-autosuggestions.git
synced 2024-11-18 09:51:06 +01:00
Revert async process substitution & completion strategy
They're not quite ready. Keep them on a feature branch for now.
This commit is contained in:
parent
4838222dde
commit
41657e3565
15 changed files with 333 additions and 340 deletions
2
Makefile
2
Makefile
|
@ -1,8 +1,10 @@
|
|||
SRC_DIR := ./src
|
||||
|
||||
SRC_FILES := \
|
||||
$(SRC_DIR)/setup.zsh \
|
||||
$(SRC_DIR)/config.zsh \
|
||||
$(SRC_DIR)/util.zsh \
|
||||
$(SRC_DIR)/features.zsh \
|
||||
$(SRC_DIR)/bind.zsh \
|
||||
$(SRC_DIR)/highlight.zsh \
|
||||
$(SRC_DIR)/widgets.zsh \
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
_[Fish](http://fishshell.com/)-like fast/unobtrusive autosuggestions for zsh._
|
||||
|
||||
It suggests commands as you type.
|
||||
It suggests commands as you type, based on command history.
|
||||
|
||||
Requirements: Zsh v4.3.11 or later
|
||||
|
||||
|
@ -39,13 +39,10 @@ Set `ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE` to configure the style that the suggestion
|
|||
|
||||
### Suggestion Strategy
|
||||
|
||||
`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently three built-in strategies to choose from:
|
||||
`ZSH_AUTOSUGGEST_STRATEGY` is an array that specifies how suggestions should be generated. The strategies in the array are tried successively until a suggestion is found. There are currently two built-in strategies to choose from:
|
||||
|
||||
- `history`: Chooses the most recent match from history.
|
||||
- `match_prev_cmd`: Like `history`, but chooses the most recent match whose preceding history item matches the most recently executed command ([more info](src/strategies/match_prev_cmd.zsh)). Note that this strategy won't work as expected with ZSH options that don't preserve the history order such as `HIST_IGNORE_ALL_DUPS` or `HIST_EXPIRE_DUPS_FIRST`.
|
||||
- `completion`: (experimental) Chooses a suggestion based on what tab-completion would suggest. (requires `zpty` module)
|
||||
|
||||
For example, setting `ZSH_AUTOSUGGEST_STRATEGY=(history completion)` will first try to find a suggestion from your history, but, if it can't find a match, will find a suggestion from the completion engine.
|
||||
|
||||
|
||||
### Widget Mapping
|
||||
|
@ -70,7 +67,7 @@ This can be useful when pasting large amount of text in the terminal, to avoid t
|
|||
|
||||
### Enable Asynchronous Mode
|
||||
|
||||
As of `v0.4.0`, suggestions can be fetched asynchronously. To enable this behavior, set the `ZSH_AUTOSUGGEST_USE_ASYNC` variable (it can be set to anything).
|
||||
As of `v0.4.0`, suggestions can be fetched asynchronously using the `zsh/zpty` module. To enable this behavior, set the `ZSH_AUTOSUGGEST_USE_ASYNC` variable (it can be set to anything).
|
||||
|
||||
|
||||
### Key Bindings
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
context 'with asynchronous suggestions enabled' do
|
||||
before do
|
||||
skip 'Async mode not supported below v5.0.8' if session.zsh_version < Gem::Version.new('5.0.8')
|
||||
end
|
||||
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_USE_ASYNC="] }
|
||||
|
||||
describe '`up-line-or-beginning-search`' do
|
||||
|
@ -26,6 +30,55 @@ context 'with asynchronous suggestions enabled' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'should not add extra carriage returns before newlines' do
|
||||
session.
|
||||
send_string('echo "').
|
||||
send_keys('escape').
|
||||
send_keys('enter').
|
||||
send_string('"').
|
||||
send_keys('enter')
|
||||
|
||||
session.clear_screen
|
||||
|
||||
session.send_string('echo')
|
||||
wait_for { session.content }.to eq("echo \"\n\"")
|
||||
end
|
||||
|
||||
it 'should treat carriage returns and newlines as separate characters' do
|
||||
session.
|
||||
send_string('echo "').
|
||||
send_keys('C-v').
|
||||
send_keys('enter').
|
||||
send_string('foo"').
|
||||
send_keys('enter')
|
||||
|
||||
session.
|
||||
send_string('echo "').
|
||||
send_keys('control').
|
||||
send_keys('enter').
|
||||
send_string('bar"').
|
||||
send_keys('enter')
|
||||
|
||||
session.clear_screen
|
||||
|
||||
session.
|
||||
send_string('echo "').
|
||||
send_keys('C-v').
|
||||
send_keys('enter')
|
||||
|
||||
wait_for { session.content }.to eq('echo "^Mfoo"')
|
||||
end
|
||||
|
||||
describe 'exiting a subshell' do
|
||||
it 'should not cause error messages to be printed' do
|
||||
session.run_command('$(exit)')
|
||||
|
||||
sleep 1
|
||||
|
||||
expect(session.content).to eq('$(exit)')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
describe 'a running zpty command' do
|
||||
let(:before_sourcing) { -> { session.run_command('zmodload zsh/zpty && zpty -b kitty cat') } }
|
||||
|
||||
context 'when using `completion` strategy' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_STRATEGY=completion"] }
|
||||
|
||||
it 'is not affected' do
|
||||
session.send_keys('a').send_keys('C-h')
|
||||
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
|
||||
end
|
||||
|
|
19
spec/options/async_zpty_name_spec.rb
Normal file
19
spec/options/async_zpty_name_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
context 'when async suggestions are enabled' do
|
||||
let(:options) { ["ZSH_AUTOSUGGEST_USE_ASYNC="] }
|
||||
|
||||
describe 'the zpty for async suggestions' do
|
||||
it 'is created with the default name' do
|
||||
session.run_command('zpty -t zsh_autosuggest_pty &>/dev/null; echo $?')
|
||||
wait_for { session.content }.to end_with("\n0")
|
||||
end
|
||||
|
||||
context 'when ZSH_AUTOSUGGEST_ASYNC_PTY_NAME is set' do
|
||||
let(:options) { super() + ['ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=foo_pty'] }
|
||||
|
||||
it 'is created with the specified name' do
|
||||
session.run_command('zpty -t foo_pty &>/dev/null; echo $?')
|
||||
wait_for { session.content }.to end_with("\n0")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,26 +0,0 @@
|
|||
describe 'the `completion` suggestion strategy' do
|
||||
let(:options) { ['ZSH_AUTOSUGGEST_STRATEGY=completion'] }
|
||||
let(:before_sourcing) do
|
||||
-> do
|
||||
session.
|
||||
run_command('autoload compinit && compinit').
|
||||
run_command('_foo() { compadd bar }').
|
||||
run_command('compdef _foo baz')
|
||||
end
|
||||
end
|
||||
|
||||
it 'suggests the first completion result' do
|
||||
session.send_string('baz ')
|
||||
wait_for { session.content }.to eq('baz bar')
|
||||
end
|
||||
|
||||
context 'when async mode is enabled' do
|
||||
let(:options) { ['ZSH_AUTOSUGGEST_USE_ASYNC=true', 'ZSH_AUTOSUGGEST_STRATEGY=completion'] }
|
||||
|
||||
it 'suggests the first completion result' do
|
||||
session.send_string('baz ')
|
||||
wait_for { session.content }.to eq('baz bar')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -18,6 +18,10 @@ class TerminalSession
|
|||
tmux_command("new-session -d -x #{opts[:width]} -y #{opts[:height]} '#{cmd}'")
|
||||
end
|
||||
|
||||
def zsh_version
|
||||
@zsh_version ||= Gem::Version.new(`#{ZSH_BIN} -c 'echo -n $ZSH_VERSION'`)
|
||||
end
|
||||
|
||||
def tmux_socket_name
|
||||
@tmux_socket_name ||= SecureRandom.hex(6)
|
||||
end
|
||||
|
|
130
src/async.zsh
130
src/async.zsh
|
@ -3,61 +3,107 @@
|
|||
# Async #
|
||||
#--------------------------------------------------------------------#
|
||||
|
||||
zmodload zsh/system
|
||||
# Zpty process is spawned running this function
|
||||
_zsh_autosuggest_async_server() {
|
||||
emulate -R zsh
|
||||
|
||||
_zsh_autosuggest_async_request() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD _ZSH_AUTOSUGGEST_CHILD_PID
|
||||
# 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
|
||||
}
|
||||
|
||||
# If we've got a pending request, cancel it
|
||||
if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then
|
||||
# Close the file descriptor and remove the handler
|
||||
exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&-
|
||||
zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD
|
||||
# Don't add any extra carriage returns
|
||||
stty -onlcr
|
||||
|
||||
# Zsh will make a new process group for the child process only if job
|
||||
# control is enabled (MONITOR option)
|
||||
if [[ -o MONITOR ]]; then
|
||||
# Send the signal to the process group to kill any processes that may
|
||||
# have been forked by the suggestion strategy
|
||||
kill -TERM -$_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null
|
||||
else
|
||||
# Kill just the child process since it wasn't placed in a new process
|
||||
# group. If the suggestion strategy forked any child processes they may
|
||||
# be orphaned and left behind.
|
||||
kill -TERM $_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
# Don't translate carriage returns to newlines
|
||||
stty -icrnl
|
||||
|
||||
# Fork a process to fetch a suggestion and open a pipe to read from it
|
||||
exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <(
|
||||
# Tell parent process our pid
|
||||
echo $sysparams[pid]
|
||||
# Silence any error messages
|
||||
exec 2>/dev/null
|
||||
|
||||
# Fetch and print the suggestion
|
||||
local last_pid
|
||||
|
||||
while IFS='' read -r -d $'\0' query; do
|
||||
# Kill last bg process
|
||||
kill -KILL $last_pid &>/dev/null
|
||||
|
||||
# Run suggestion search in the background
|
||||
(
|
||||
local suggestion
|
||||
_zsh_autosuggest_fetch_suggestion "$1"
|
||||
echo -nE "$suggestion"
|
||||
)
|
||||
_zsh_autosuggest_fetch_suggestion "$query"
|
||||
echo -n -E "$suggestion"$'\0'
|
||||
) &
|
||||
|
||||
# Read the pid from the child process
|
||||
read _ZSH_AUTOSUGGEST_CHILD_PID <&$_ZSH_AUTOSUGGEST_ASYNC_FD
|
||||
|
||||
# When the fd is readable, call the response handler
|
||||
zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response
|
||||
last_pid=$!
|
||||
done
|
||||
}
|
||||
|
||||
# Called when new data is ready to be read from the pipe
|
||||
_zsh_autosuggest_async_request() {
|
||||
# Write the query to the zpty process to fetch a suggestion
|
||||
zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0'
|
||||
}
|
||||
|
||||
# Called when new data is ready to be read from the pty
|
||||
# First arg will be fd ready for reading
|
||||
# Second arg will be passed in case of error
|
||||
_zsh_autosuggest_async_response() {
|
||||
if [[ -z "$2" || "$2" == "hup" ]]; then
|
||||
# Read everything from the fd and give it as a suggestion
|
||||
zle autosuggest-suggest -- "$(cat <&$1)"
|
||||
setopt LOCAL_OPTIONS EXTENDED_GLOB
|
||||
|
||||
# Close the fd
|
||||
exec {1}<&-
|
||||
local suggestion
|
||||
|
||||
zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null
|
||||
zle autosuggest-suggest -- "${suggestion%%$'\0'##}"
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_create() {
|
||||
# With newer versions of zsh, REPLY stores the fd to read from
|
||||
typeset -h REPLY
|
||||
|
||||
# If we won't get a fd back from zpty, try to guess it
|
||||
if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); 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
|
||||
|
||||
# Always remove the handler
|
||||
zle -F "$1"
|
||||
# Fork a zpty process running the server function
|
||||
zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server
|
||||
|
||||
# Store the fd so we can remove the handler later
|
||||
if (( REPLY )); then
|
||||
_ZSH_AUTOSUGGEST_PTY_FD=$REPLY
|
||||
else
|
||||
_ZSH_AUTOSUGGEST_PTY_FD=$zptyfd
|
||||
fi
|
||||
|
||||
# Set up input handler from the zpty
|
||||
zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_destroy() {
|
||||
# Remove the input handler
|
||||
zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null
|
||||
|
||||
# Destroy the zpty
|
||||
zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_recreate() {
|
||||
_zsh_autosuggest_async_pty_destroy
|
||||
_zsh_autosuggest_async_pty_create
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_start() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_PTY_FD
|
||||
|
||||
_zsh_autosuggest_feature_detect_zpty_returns_fd
|
||||
_zsh_autosuggest_async_pty_recreate
|
||||
|
||||
# We recreate the pty to get a fresh list of history events
|
||||
add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate
|
||||
}
|
||||
|
|
|
@ -69,5 +69,5 @@
|
|||
# Max size of buffer to trigger autosuggestion. Leave null for no upper bound.
|
||||
: ${ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=}
|
||||
|
||||
# Pty name for capturing completions for completion suggestion strategy
|
||||
: ${ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty}
|
||||
# Pty name for calculating autosuggestions asynchronously
|
||||
: ${ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty}
|
||||
|
|
19
src/features.zsh
Normal file
19
src/features.zsh
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
#--------------------------------------------------------------------#
|
||||
# Feature Detection #
|
||||
#--------------------------------------------------------------------#
|
||||
|
||||
_zsh_autosuggest_feature_detect_zpty_returns_fd() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD
|
||||
typeset -h REPLY
|
||||
|
||||
zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }'
|
||||
|
||||
if (( REPLY )); then
|
||||
_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1
|
||||
else
|
||||
_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0
|
||||
fi
|
||||
|
||||
zpty -d zsh_autosuggest_feature_detect
|
||||
}
|
10
src/setup.zsh
Normal file
10
src/setup.zsh
Normal file
|
@ -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
|
|
@ -14,8 +14,11 @@ _zsh_autosuggest_start() {
|
|||
# zsh-syntax-highlighting widgets. This also allows modifications
|
||||
# to the widget list variables to take effect on the next precmd.
|
||||
add-zsh-hook precmd _zsh_autosuggest_bind_widgets
|
||||
|
||||
if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then
|
||||
_zsh_autosuggest_async_start
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the autosuggestion widgets on the next precmd
|
||||
autoload -Uz add-zsh-hook
|
||||
add-zsh-hook precmd _zsh_autosuggest_start
|
||||
|
|
|
@ -1,104 +0,0 @@
|
|||
|
||||
#--------------------------------------------------------------------#
|
||||
# Completion Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
# Fetches a suggestion from the completion engine
|
||||
#
|
||||
|
||||
_zsh_autosuggest_capture_postcompletion() {
|
||||
# Always insert the first completion into the buffer
|
||||
compstate[insert]=1
|
||||
|
||||
# Don't list completions
|
||||
unset compstate[list]
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_widget() {
|
||||
local -a +h comppostfuncs
|
||||
comppostfuncs=(_zsh_autosuggest_capture_postcompletion)
|
||||
|
||||
# Only capture completions at the end of the buffer
|
||||
CURSOR=$#BUFFER
|
||||
|
||||
# Run the original widget wrapping `.complete-word` so we don't
|
||||
# recursively try to fetch suggestions, since our pty is forked
|
||||
# after autosuggestions is initialized.
|
||||
zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]}
|
||||
|
||||
# The completion has been added, print the buffer as the suggestion
|
||||
echo -nE - $'\0'$BUFFER$'\0'
|
||||
}
|
||||
|
||||
zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget
|
||||
|
||||
_zsh_autosuggest_capture_setup() {
|
||||
# There is a bug in zpty module in older zsh versions by which a
|
||||
# zpty that exits will kill all zpty processes that were forked
|
||||
# before it. Here we set up a zsh exit hook to SIGKILL the zpty
|
||||
# process immediately, before it has a chance to kill any other
|
||||
# zpty processes.
|
||||
if ! is-at-least 5.4; then
|
||||
zshexit() {
|
||||
kill -KILL $$
|
||||
sleep 1 # Block for long enough for the signal to come through
|
||||
}
|
||||
fi
|
||||
|
||||
bindkey '^I' autosuggest-capture-completion
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_sync() {
|
||||
_zsh_autosuggest_capture_setup
|
||||
|
||||
zle autosuggest-capture-completion
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_async() {
|
||||
_zsh_autosuggest_capture_setup
|
||||
|
||||
zmodload zsh/parameter 2>/dev/null || return # For `$functions`
|
||||
|
||||
# Make vared completion work as if for a normal command line
|
||||
# https://stackoverflow.com/a/7057118/154703
|
||||
autoload +X _complete
|
||||
functions[_original_complete]=$functions[_complete]
|
||||
_complete () {
|
||||
unset 'compstate[vared]'
|
||||
_original_complete "$@"
|
||||
}
|
||||
|
||||
# Open zle with buffer set so we can capture completions for it
|
||||
vared 1
|
||||
}
|
||||
|
||||
_zsh_autosuggest_strategy_completion() {
|
||||
typeset -g suggestion
|
||||
local line REPLY
|
||||
|
||||
# Exit if we don't have completions
|
||||
whence compdef >/dev/null || return
|
||||
|
||||
# Exit if we don't have zpty
|
||||
zmodload zsh/zpty 2>/dev/null || return
|
||||
|
||||
# Zle will be inactive if we are in async mode
|
||||
if zle; then
|
||||
zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync
|
||||
else
|
||||
zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$1"
|
||||
zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t'
|
||||
fi
|
||||
|
||||
{
|
||||
# The completion result is surrounded by null bytes, so read the
|
||||
# content between the first two null bytes.
|
||||
zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0'
|
||||
|
||||
# On older versions of zsh, we sometimes get extra bytes after the
|
||||
# second null byte, so trim those off the end
|
||||
suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}"
|
||||
} always {
|
||||
# Destroy the pty
|
||||
zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME
|
||||
}
|
||||
}
|
|
@ -95,7 +95,7 @@ _zsh_autosuggest_modify() {
|
|||
|
||||
# Fetch a new suggestion based on what's currently in the buffer
|
||||
_zsh_autosuggest_fetch() {
|
||||
if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then
|
||||
if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then
|
||||
_zsh_autosuggest_async_request "$BUFFER"
|
||||
else
|
||||
local suggestion
|
||||
|
|
|
@ -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 #
|
||||
#--------------------------------------------------------------------#
|
||||
|
@ -95,8 +105,8 @@
|
|||
# Max size of buffer to trigger autosuggestion. Leave null for no upper bound.
|
||||
: ${ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=}
|
||||
|
||||
# Pty name for capturing completions for completion suggestion strategy
|
||||
: ${ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME=zsh_autosuggest_completion_pty}
|
||||
# Pty name for calculating autosuggestions asynchronously
|
||||
: ${ZSH_AUTOSUGGEST_ASYNC_PTY_NAME=zsh_autosuggest_pty}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Utility Functions #
|
||||
|
@ -109,6 +119,25 @@ _zsh_autosuggest_escape_command() {
|
|||
echo -E "${1//(#m)[\"\'\\()\[\]|*?~]/\\$MATCH}"
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Feature Detection #
|
||||
#--------------------------------------------------------------------#
|
||||
|
||||
_zsh_autosuggest_feature_detect_zpty_returns_fd() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD
|
||||
typeset -h REPLY
|
||||
|
||||
zpty zsh_autosuggest_feature_detect '{ zshexit() { kill -KILL $$; sleep 1 } }'
|
||||
|
||||
if (( REPLY )); then
|
||||
_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=1
|
||||
else
|
||||
_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD=0
|
||||
fi
|
||||
|
||||
zpty -d zsh_autosuggest_feature_detect
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Widget Helpers #
|
||||
#--------------------------------------------------------------------#
|
||||
|
@ -351,7 +380,7 @@ _zsh_autosuggest_modify() {
|
|||
|
||||
# Fetch a new suggestion based on what's currently in the buffer
|
||||
_zsh_autosuggest_fetch() {
|
||||
if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then
|
||||
if zpty -t "$ZSH_AUTOSUGGEST_ASYNC_PTY_NAME" &>/dev/null; then
|
||||
_zsh_autosuggest_async_request "$BUFFER"
|
||||
else
|
||||
local suggestion
|
||||
|
@ -472,110 +501,6 @@ zle -N autosuggest-enable _zsh_autosuggest_widget_enable
|
|||
zle -N autosuggest-disable _zsh_autosuggest_widget_disable
|
||||
zle -N autosuggest-toggle _zsh_autosuggest_widget_toggle
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# Completion Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
# Fetches a suggestion from the completion engine
|
||||
#
|
||||
|
||||
_zsh_autosuggest_capture_postcompletion() {
|
||||
# Always insert the first completion into the buffer
|
||||
compstate[insert]=1
|
||||
|
||||
# Don't list completions
|
||||
unset compstate[list]
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_widget() {
|
||||
local -a +h comppostfuncs
|
||||
comppostfuncs=(_zsh_autosuggest_capture_postcompletion)
|
||||
|
||||
# Only capture completions at the end of the buffer
|
||||
CURSOR=$#BUFFER
|
||||
|
||||
# Run the original widget wrapping `.complete-word` so we don't
|
||||
# recursively try to fetch suggestions, since our pty is forked
|
||||
# after autosuggestions is initialized.
|
||||
zle -- ${(k)widgets[(r)completion:.complete-word:_main_complete]}
|
||||
|
||||
# The completion has been added, print the buffer as the suggestion
|
||||
echo -nE - $'\0'$BUFFER$'\0'
|
||||
}
|
||||
|
||||
zle -N autosuggest-capture-completion _zsh_autosuggest_capture_completion_widget
|
||||
|
||||
_zsh_autosuggest_capture_setup() {
|
||||
# There is a bug in zpty module in older zsh versions by which a
|
||||
# zpty that exits will kill all zpty processes that were forked
|
||||
# before it. Here we set up a zsh exit hook to SIGKILL the zpty
|
||||
# process immediately, before it has a chance to kill any other
|
||||
# zpty processes.
|
||||
if ! is-at-least 5.4; then
|
||||
zshexit() {
|
||||
kill -KILL $$
|
||||
sleep 1 # Block for long enough for the signal to come through
|
||||
}
|
||||
fi
|
||||
|
||||
bindkey '^I' autosuggest-capture-completion
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_sync() {
|
||||
_zsh_autosuggest_capture_setup
|
||||
|
||||
zle autosuggest-capture-completion
|
||||
}
|
||||
|
||||
_zsh_autosuggest_capture_completion_async() {
|
||||
_zsh_autosuggest_capture_setup
|
||||
|
||||
zmodload zsh/parameter 2>/dev/null || return # For `$functions`
|
||||
|
||||
# Make vared completion work as if for a normal command line
|
||||
# https://stackoverflow.com/a/7057118/154703
|
||||
autoload +X _complete
|
||||
functions[_original_complete]=$functions[_complete]
|
||||
_complete () {
|
||||
unset 'compstate[vared]'
|
||||
_original_complete "$@"
|
||||
}
|
||||
|
||||
# Open zle with buffer set so we can capture completions for it
|
||||
vared 1
|
||||
}
|
||||
|
||||
_zsh_autosuggest_strategy_completion() {
|
||||
typeset -g suggestion
|
||||
local line REPLY
|
||||
|
||||
# Exit if we don't have completions
|
||||
whence compdef >/dev/null || return
|
||||
|
||||
# Exit if we don't have zpty
|
||||
zmodload zsh/zpty 2>/dev/null || return
|
||||
|
||||
# Zle will be inactive if we are in async mode
|
||||
if zle; then
|
||||
zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_sync
|
||||
else
|
||||
zpty $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME _zsh_autosuggest_capture_completion_async "\$1"
|
||||
zpty -w $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME $'\t'
|
||||
fi
|
||||
|
||||
{
|
||||
# The completion result is surrounded by null bytes, so read the
|
||||
# content between the first two null bytes.
|
||||
zpty -r $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME line '*'$'\0''*'$'\0'
|
||||
|
||||
# On older versions of zsh, we sometimes get extra bytes after the
|
||||
# second null byte, so trim those off the end
|
||||
suggestion="${${${(M)line:#*$'\0'*$'\0'*}#*$'\0'}%%$'\0'*}"
|
||||
} always {
|
||||
# Destroy the pty
|
||||
zpty -d $ZSH_AUTOSUGGEST_COMPLETIONS_PTY_NAME
|
||||
}
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
# History Suggestion Strategy #
|
||||
#--------------------------------------------------------------------#
|
||||
|
@ -688,63 +613,109 @@ _zsh_autosuggest_fetch_suggestion() {
|
|||
# Async #
|
||||
#--------------------------------------------------------------------#
|
||||
|
||||
zmodload zsh/system
|
||||
# Zpty process is spawned running this function
|
||||
_zsh_autosuggest_async_server() {
|
||||
emulate -R zsh
|
||||
|
||||
_zsh_autosuggest_async_request() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_ASYNC_FD _ZSH_AUTOSUGGEST_CHILD_PID
|
||||
# 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
|
||||
}
|
||||
|
||||
# If we've got a pending request, cancel it
|
||||
if [[ -n "$_ZSH_AUTOSUGGEST_ASYNC_FD" ]] && { true <&$_ZSH_AUTOSUGGEST_ASYNC_FD } 2>/dev/null; then
|
||||
# Close the file descriptor and remove the handler
|
||||
exec {_ZSH_AUTOSUGGEST_ASYNC_FD}<&-
|
||||
zle -F $_ZSH_AUTOSUGGEST_ASYNC_FD
|
||||
# Don't add any extra carriage returns
|
||||
stty -onlcr
|
||||
|
||||
# Zsh will make a new process group for the child process only if job
|
||||
# control is enabled (MONITOR option)
|
||||
if [[ -o MONITOR ]]; then
|
||||
# Send the signal to the process group to kill any processes that may
|
||||
# have been forked by the suggestion strategy
|
||||
kill -TERM -$_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null
|
||||
else
|
||||
# Kill just the child process since it wasn't placed in a new process
|
||||
# group. If the suggestion strategy forked any child processes they may
|
||||
# be orphaned and left behind.
|
||||
kill -TERM $_ZSH_AUTOSUGGEST_CHILD_PID 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
# Don't translate carriage returns to newlines
|
||||
stty -icrnl
|
||||
|
||||
# Fork a process to fetch a suggestion and open a pipe to read from it
|
||||
exec {_ZSH_AUTOSUGGEST_ASYNC_FD}< <(
|
||||
# Tell parent process our pid
|
||||
echo $sysparams[pid]
|
||||
# Silence any error messages
|
||||
exec 2>/dev/null
|
||||
|
||||
# Fetch and print the suggestion
|
||||
local last_pid
|
||||
|
||||
while IFS='' read -r -d $'\0' query; do
|
||||
# Kill last bg process
|
||||
kill -KILL $last_pid &>/dev/null
|
||||
|
||||
# Run suggestion search in the background
|
||||
(
|
||||
local suggestion
|
||||
_zsh_autosuggest_fetch_suggestion "$1"
|
||||
echo -nE "$suggestion"
|
||||
)
|
||||
_zsh_autosuggest_fetch_suggestion "$query"
|
||||
echo -n -E "$suggestion"$'\0'
|
||||
) &
|
||||
|
||||
# Read the pid from the child process
|
||||
read _ZSH_AUTOSUGGEST_CHILD_PID <&$_ZSH_AUTOSUGGEST_ASYNC_FD
|
||||
|
||||
# When the fd is readable, call the response handler
|
||||
zle -F "$_ZSH_AUTOSUGGEST_ASYNC_FD" _zsh_autosuggest_async_response
|
||||
last_pid=$!
|
||||
done
|
||||
}
|
||||
|
||||
# Called when new data is ready to be read from the pipe
|
||||
_zsh_autosuggest_async_request() {
|
||||
# Write the query to the zpty process to fetch a suggestion
|
||||
zpty -w -n $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME "${1}"$'\0'
|
||||
}
|
||||
|
||||
# Called when new data is ready to be read from the pty
|
||||
# First arg will be fd ready for reading
|
||||
# Second arg will be passed in case of error
|
||||
_zsh_autosuggest_async_response() {
|
||||
if [[ -z "$2" || "$2" == "hup" ]]; then
|
||||
# Read everything from the fd and give it as a suggestion
|
||||
zle autosuggest-suggest -- "$(cat <&$1)"
|
||||
setopt LOCAL_OPTIONS EXTENDED_GLOB
|
||||
|
||||
# Close the fd
|
||||
exec {1}<&-
|
||||
local suggestion
|
||||
|
||||
zpty -rt $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME suggestion '*'$'\0' 2>/dev/null
|
||||
zle autosuggest-suggest -- "${suggestion%%$'\0'##}"
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_create() {
|
||||
# With newer versions of zsh, REPLY stores the fd to read from
|
||||
typeset -h REPLY
|
||||
|
||||
# If we won't get a fd back from zpty, try to guess it
|
||||
if (( ! $_ZSH_AUTOSUGGEST_ZPTY_RETURNS_FD )); 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
|
||||
|
||||
# Always remove the handler
|
||||
zle -F "$1"
|
||||
# Fork a zpty process running the server function
|
||||
zpty -b $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME _zsh_autosuggest_async_server
|
||||
|
||||
# Store the fd so we can remove the handler later
|
||||
if (( REPLY )); then
|
||||
_ZSH_AUTOSUGGEST_PTY_FD=$REPLY
|
||||
else
|
||||
_ZSH_AUTOSUGGEST_PTY_FD=$zptyfd
|
||||
fi
|
||||
|
||||
# Set up input handler from the zpty
|
||||
zle -F $_ZSH_AUTOSUGGEST_PTY_FD _zsh_autosuggest_async_response
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_destroy() {
|
||||
# Remove the input handler
|
||||
zle -F $_ZSH_AUTOSUGGEST_PTY_FD &>/dev/null
|
||||
|
||||
# Destroy the zpty
|
||||
zpty -d $ZSH_AUTOSUGGEST_ASYNC_PTY_NAME &>/dev/null
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_pty_recreate() {
|
||||
_zsh_autosuggest_async_pty_destroy
|
||||
_zsh_autosuggest_async_pty_create
|
||||
}
|
||||
|
||||
_zsh_autosuggest_async_start() {
|
||||
typeset -g _ZSH_AUTOSUGGEST_PTY_FD
|
||||
|
||||
_zsh_autosuggest_feature_detect_zpty_returns_fd
|
||||
_zsh_autosuggest_async_pty_recreate
|
||||
|
||||
# We recreate the pty to get a fresh list of history events
|
||||
add-zsh-hook precmd _zsh_autosuggest_async_pty_recreate
|
||||
}
|
||||
|
||||
#--------------------------------------------------------------------#
|
||||
|
@ -762,8 +733,11 @@ _zsh_autosuggest_start() {
|
|||
# zsh-syntax-highlighting widgets. This also allows modifications
|
||||
# to the widget list variables to take effect on the next precmd.
|
||||
add-zsh-hook precmd _zsh_autosuggest_bind_widgets
|
||||
|
||||
if [[ -n "${ZSH_AUTOSUGGEST_USE_ASYNC+x}" ]]; then
|
||||
_zsh_autosuggest_async_start
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the autosuggestion widgets on the next precmd
|
||||
autoload -Uz add-zsh-hook
|
||||
add-zsh-hook precmd _zsh_autosuggest_start
|
||||
|
|
Loading…
Reference in a new issue