diff --git a/README.md b/README.md
index 4951dba78..303ee1e23 100644
--- a/README.md
+++ b/README.md
@@ -44,11 +44,23 @@ FAQ
### Why must `zsh-syntax-highlighting.zsh` be sourced at the end of the `.zshrc` file?
-`zsh-syntax-highlighting.zsh` wraps ZLE widgets. It must be sourced after all
-custom widgets have been created (i.e., after all `zle -N` calls and after
-running `compinit`). Widgets created later will work, but will not update the
+zsh-syntax-highlighting works by hooking into the Zsh Line Editor (ZLE) and
+computing syntax highlighting for the command-line buffer as it stands at the
+time z-sy-h's hook is invoked.
+
+In zsh 5.2 and older,
+`zsh-syntax-highlighting.zsh` hooks into ZLE by wrapping ZLE widgets. It must
+be sourced after all custom widgets have been created (i.e., after all `zle -N`
+calls and after running `compinit`) in order to be able to wrap all of them.
+Widgets created after z-sy-h is sourced will work, but will not update the
syntax highlighting.
+In zsh newer than 5.8 (not including 5.8 itself),
+zsh-syntax-highlighting uses the `add-zle-hook-widget` facility to install
+a `zle-line-pre-redraw` hook. Hooks are run in order of registration,
+therefore, z-sy-h must be sourced (and register its hook) after anything else
+that adds hooks that modify the command-line buffer.
+
### Does syntax highlighting work during incremental history search?
Highlighting the command line during an incremental history search (by default bound to
diff --git a/changelog.md b/changelog.md
index 518d6a8f4..1bc1ac0fc 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,57 @@
# Changes in HEAD
+## Changes fixed as part of the switch to zle-line-pre-redraw
+
+The changes in this section were fixed by switching to a `zle-line-pre-redraw`-based
+implementation.
+
+Note: The new implementation will only be used on future zsh releases,
+numbered 5.8.0.3 and newer, due to interoperability issues with other plugins
+(issues #418 and #579). The underlying zsh feature has been available since
+zsh 5.2.
+
+Whilst under development, the new implementation was known as the
+"feature/redrawhook" topic branch.
+
+- Fixed: Highlighting not triggered after popping a buffer from the buffer stack
+ (using the `push-line` widget, default binding: `M-q`)
+ [#40]
+
+- Fixed: Invoking completion when there were no matches removed highlighting
+ [#90, #470]
+
+- Fixed: Two successive deletes followed by a yank only yanked the latest
+ delete, rather than both of them
+ [#150, #151, #160; cf. #183]
+
+- Presumed fixed: Completing `$(xsel)` results in an error message from `xsel`,
+ with pre-2017 versions of `xsel`. (For 2017 vintage and newer, see the issue
+ for details.)
+ [#154]
+
+- Fixed: When the standard `bracketed-paste-magic` widget is in use, pastes were slow
+ [#295]
+
+- Fixed: No way to prevent a widget from being wrapped
+ [#324]
+
+- Fixed: No highlighting while cycling menu completion
+ [#375]
+
+- Fixed: Does not coexist with the `IGNORE_EOF` option
+ [#377]
+
+- Fixed: The `undefined-key` widget was wrapped
+ [#421]
+
+- Fixed: Does not coexist with the standard `surround` family of widgets
+ [#520]
+
+- Fixed: First completed filename doesn't get `path` highlighting
+ [#632]
+
+
# Changes in 0.8.0-alpha1-pre-redrawhook
## Notice about an improbable-but-not-impossible forward incompatibility
@@ -19,6 +70,25 @@ added to zsh at z-sy-h's initiative. The new feature is used in the fix
to issue #418.
+## Incompatible changes:
+
+- An unsuccessful completion (a ⮀ Tab press that doesn't change the
+ command line) no longer causes highlighting to be lost. Visual feedback can
+ alternatively be achieved by setting the `format` zstyle under the `warnings`
+ tag, for example,
+
+ zstyle ':completion:*:warnings' format '%F{red}No matches%f'
+
+ Refer to the [description of the `format` style in `zshcompsys(1)`]
+ [zshcompsys-Standard-Styles-format].
+
+ (#90, part of #245 (feature/redrawhook))
+
+[zshcompsys-Standard-Styles]: http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Standard-Styles
+[zshcompsys-Standard-Styles-format]: http://zsh.sourceforge.net/Doc/Release/Completion-System.html#index-format_002c-completion-style
+
+
+
## Other changes:
- Document `$ZSH_HIGHLIGHT_MAXLENGTH`.
diff --git a/tests/generate.zsh b/tests/generate.zsh
index 8b1607392..56960202b 100755
--- a/tests/generate.zsh
+++ b/tests/generate.zsh
@@ -31,6 +31,9 @@
emulate -LR zsh
setopt localoptions extendedglob
+# Required for add-zle-hook-widget.
+zmodload zsh/zle
+
# Argument parsing.
if (( $# * $# - 7 * $# + 12 )) || [[ $1 == -* ]]; then
print -r -- >&2 "$0: usage: $0 BUFFER HIGHLIGHTER BASENAME [PREAMBLE]"
diff --git a/tests/test-highlighting.zsh b/tests/test-highlighting.zsh
index 30e93b13f..8b564a8b9 100755
--- a/tests/test-highlighting.zsh
+++ b/tests/test-highlighting.zsh
@@ -31,6 +31,9 @@
setopt NO_UNSET WARN_CREATE_GLOBAL
+# Required for add-zle-hook-widget.
+zmodload zsh/zle
+
local -r root=${0:h:h}
local -a anon_argv; anon_argv=("$@")
diff --git a/tests/test-perfs.zsh b/tests/test-perfs.zsh
index ff083de2a..aa139aad5 100755
--- a/tests/test-perfs.zsh
+++ b/tests/test-perfs.zsh
@@ -29,6 +29,9 @@
# -------------------------------------------------------------------------------------------------
+# Required for add-zle-hook-widget.
+zmodload zsh/zle
+
# Check an highlighter was given as argument.
[[ -n "$1" ]] || {
echo >&2 "Bail out! You must provide the name of a valid highlighter as argument."
diff --git a/zsh-syntax-highlighting.zsh b/zsh-syntax-highlighting.zsh
index f98dc4b06..3608203f5 100644
--- a/zsh-syntax-highlighting.zsh
+++ b/zsh-syntax-highlighting.zsh
@@ -49,6 +49,52 @@ if true; then
fi
fi
+# This function takes a single argument F and returns True iff F is an autoload stub.
+_zsh_highlight__function_is_autoload_stub_p() {
+ if zmodload -e zsh/parameter; then
+ #(( ${+functions[$1]} )) &&
+ [[ "$functions[$1]" == *"builtin autoload -X"* ]]
+ else
+ #[[ $(type -wa -- "$1") == *'function'* ]] &&
+ [[ "${${(@f)"$(which -- "$1")"}[2]}" == $'\t'$histchars[3]' undefined' ]]
+ fi
+ # Do nothing here: return the exit code of the if.
+}
+
+# Return True iff the argument denotes a function name.
+_zsh_highlight__is_function_p() {
+ if zmodload -e zsh/parameter; then
+ (( ${+functions[$1]} ))
+ else
+ [[ $(type -wa -- "$1") == *'function'* ]]
+ fi
+}
+
+# This function takes a single argument F and returns True iff F denotes the
+# name of a callable function. A function is callable if it is fully defined
+# or if it is marked for autoloading and autoloading it at the first call to it
+# will succeed. In particular, if a function has been marked for autoloading
+# but is not available in $fpath, then this function will return False therefor.
+#
+# See users/21671 http://www.zsh.org/cgi-bin/mla/redirect?USERNUMBER=21671
+_zsh_highlight__function_callable_p() {
+ if _zsh_highlight__is_function_p "$1" &&
+ ! _zsh_highlight__function_is_autoload_stub_p "$1"
+ then
+ # Already fully loaded.
+ return 0 # true
+ else
+ # "$1" is either an autoload stub, or not a function at all.
+ #
+ # Use a subshell to avoid affecting the calling shell.
+ #
+ # We expect 'autoload +X' to return non-zero if it fails to fully load
+ # the function.
+ ( autoload -U +X -- "$1" 2>/dev/null )
+ return $?
+ fi
+}
+
# -------------------------------------------------------------------------------------------------
# Core highlighting update system
# -------------------------------------------------------------------------------------------------
@@ -347,76 +393,120 @@ _zsh_highlight_add_highlight()
# $1 is name of widget to call
_zsh_highlight_call_widget()
{
- builtin zle "$@" &&
+ builtin zle "$@" &&
_zsh_highlight
}
-# Rebind all ZLE widgets to make them invoke _zsh_highlights.
-_zsh_highlight_bind_widgets()
-{
- setopt localoptions noksharrays
- typeset -F SECONDS
- local prefix=orig-s$SECONDS-r$RANDOM # unique each time, in case we're sourced more than once
-
- # Load ZSH module zsh/zleparameter, needed to override user defined widgets.
- zmodload zsh/zleparameter 2>/dev/null || {
- print -r -- >&2 'zsh-syntax-highlighting: failed loading zsh/zleparameter.'
- return 1
+# Decide whether to use the zle-line-pre-redraw codepath (colloquially known as
+# "feature/redrawhook", after the topic branch's name) or the legacy "bind all
+# widgets" codepath.
+#
+# We use the new codepath under two conditions:
+#
+# 1. If it's available, which we check by testing for add-zle-hook-widget's availability.
+#
+# 2. If zsh has the memo= feature, which is required for interoperability reasons.
+# See issues #579 and #735, and the issues referenced from them.
+#
+# We check this with a plain version number check, since a functional check,
+# as done by _zsh_highlight, can only be done from inside a widget
+# function — a catch-22.
+#
+# See _zsh_highlight for the magic version number. (The use of 5.8.0.2
+# rather than 5.8.0.3 as in the _zsh_highlight is deliberate.)
+if is-at-least 5.8.0.2 && _zsh_highlight__function_callable_p add-zle-hook-widget
+then
+ autoload -U add-zle-hook-widget
+ _zsh_highlight__zle-line-finish() {
+ # Reset $WIDGET since the 'main' highlighter depends on it.
+ #
+ # Since $WIDGET is declared by zle as read-only in this function's scope,
+ # a nested function is required in order to shadow its built-in value;
+ # see "User-defined widgets" in zshall.
+ () {
+ local -h -r WIDGET=zle-line-finish
+ _zsh_highlight
+ }
}
+ _zsh_highlight__zle-line-pre-redraw() {
+ # Set $? to 0 for _zsh_highlight. Without this, subsequent
+ # zle-line-pre-redraw hooks won't run, since add-zle-hook-widget happens to
+ # call us with $? == 1 in the common case.
+ true && _zsh_highlight "$@"
+ }
+ _zsh_highlight_bind_widgets(){}
+ if [[ -o zle ]]; then
+ add-zle-hook-widget zle-line-pre-redraw _zsh_highlight__zle-line-pre-redraw
+ add-zle-hook-widget zle-line-finish _zsh_highlight__zle-line-finish
+ fi
+else
+ # Rebind all ZLE widgets to make them invoke _zsh_highlights.
+ _zsh_highlight_bind_widgets()
+ {
+ setopt localoptions noksharrays
+ typeset -F SECONDS
+ local prefix=orig-s$SECONDS-r$RANDOM # unique each time, in case we're sourced more than once
+
+ # Load ZSH module zsh/zleparameter, needed to override user defined widgets.
+ zmodload zsh/zleparameter 2>/dev/null || {
+ print -r -- >&2 'zsh-syntax-highlighting: failed loading zsh/zleparameter.'
+ return 1
+ }
- # Override ZLE widgets to make them invoke _zsh_highlight.
- local -U widgets_to_bind
- widgets_to_bind=(${${(k)widgets}:#(.*|run-help|which-command|beep|set-local-history|yank|yank-pop)})
-
- # Always wrap special zle-line-finish widget. This is needed to decide if the
- # current line ends and special highlighting logic needs to be applied.
- # E.g. remove cursor imprint, don't highlight partial paths, ...
- widgets_to_bind+=(zle-line-finish)
-
- # Always wrap special zle-isearch-update widget to be notified of updates in isearch.
- # This is needed because we need to disable highlighting in that case.
- widgets_to_bind+=(zle-isearch-update)
-
- local cur_widget
- for cur_widget in $widgets_to_bind; do
- case ${widgets[$cur_widget]:-""} in
-
- # Already rebound event: do nothing.
- user:_zsh_highlight_widget_*);;
-
- # The "eval"'s are required to make $cur_widget a closure: the value of the parameter at function
- # definition time is used.
- #
- # We can't use ${0/_zsh_highlight_widget_} because these widgets are always invoked with
- # NO_function_argzero, regardless of the option's setting here.
-
- # User defined widget: override and rebind old one with prefix "orig-".
- user:*) zle -N $prefix-$cur_widget ${widgets[$cur_widget]#*:}
- eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
- zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
-
- # Completion widget: override and rebind old one with prefix "orig-".
- completion:*) zle -C $prefix-$cur_widget ${${(s.:.)widgets[$cur_widget]}[2,3]}
- eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
- zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
-
- # Builtin widget: override and make it call the builtin ".widget".
- builtin) eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget .${(q)cur_widget} -- \"\$@\" }"
- zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
-
- # Incomplete or nonexistent widget: Bind to z-sy-h directly.
- *)
- if [[ $cur_widget == zle-* ]] && (( ! ${+widgets[$cur_widget]} )); then
- _zsh_highlight_widget_${cur_widget}() { :; _zsh_highlight }
- zle -N $cur_widget _zsh_highlight_widget_$cur_widget
- else
- # Default: unhandled case.
- print -r -- >&2 "zsh-syntax-highlighting: unhandled ZLE widget ${(qq)cur_widget}"
- print -r -- >&2 "zsh-syntax-highlighting: (This is sometimes caused by doing \`bindkey ${(q-)cur_widget}\` without creating the ${(qq)cur_widget} widget with \`zle -N\` or \`zle -C\`.)"
- fi
- esac
- done
-}
+ # Override ZLE widgets to make them invoke _zsh_highlight.
+ local -U widgets_to_bind
+ widgets_to_bind=(${${(k)widgets}:#(.*|run-help|which-command|beep|set-local-history|yank|yank-pop)})
+
+ # Always wrap special zle-line-finish widget. This is needed to decide if the
+ # current line ends and special highlighting logic needs to be applied.
+ # E.g. remove cursor imprint, don't highlight partial paths, ...
+ widgets_to_bind+=(zle-line-finish)
+
+ # Always wrap special zle-isearch-update widget to be notified of updates in isearch.
+ # This is needed because we need to disable highlighting in that case.
+ widgets_to_bind+=(zle-isearch-update)
+
+ local cur_widget
+ for cur_widget in $widgets_to_bind; do
+ case ${widgets[$cur_widget]:-""} in
+
+ # Already rebound event: do nothing.
+ user:_zsh_highlight_widget_*);;
+
+ # The "eval"'s are required to make $cur_widget a closure: the value of the parameter at function
+ # definition time is used.
+ #
+ # We can't use ${0/_zsh_highlight_widget_} because these widgets are always invoked with
+ # NO_function_argzero, regardless of the option's setting here.
+
+ # User defined widget: override and rebind old one with prefix "orig-".
+ user:*) zle -N $prefix-$cur_widget ${widgets[$cur_widget]#*:}
+ eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
+ zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
+
+ # Completion widget: override and rebind old one with prefix "orig-".
+ completion:*) zle -C $prefix-$cur_widget ${${(s.:.)widgets[$cur_widget]}[2,3]}
+ eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
+ zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
+
+ # Builtin widget: override and make it call the builtin ".widget".
+ builtin) eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget .${(q)cur_widget} -- \"\$@\" }"
+ zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
+
+ # Incomplete or nonexistent widget: Bind to z-sy-h directly.
+ *)
+ if [[ $cur_widget == zle-* ]] && (( ! ${+widgets[$cur_widget]} )); then
+ _zsh_highlight_widget_${cur_widget}() { :; _zsh_highlight }
+ zle -N $cur_widget _zsh_highlight_widget_$cur_widget
+ else
+ # Default: unhandled case.
+ print -r -- >&2 "zsh-syntax-highlighting: unhandled ZLE widget ${(qq)cur_widget}"
+ print -r -- >&2 "zsh-syntax-highlighting: (This is sometimes caused by doing \`bindkey ${(q-)cur_widget}\` without creating the ${(qq)cur_widget} widget with \`zle -N\` or \`zle -C\`.)"
+ fi
+ esac
+ done
+ }
+fi
# Load highlighters from directory.
#