diff --git a/README.md b/README.md index 4558463..625e063 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,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 5.3 and newer, +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 diff --git a/changelog.md b/changelog.md index 94cb929..520cbab 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,28 @@ up to 952a97dbc99a54bd86141b7a57625e748941a937 + 0.4.1 +# Changes in version 0.5.0 + + +## 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, XXXXXXXXXXXX) + +[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 + + + # Changes in version 0.4.1 ## Fixes: diff --git a/tests/generate.zsh b/tests/generate.zsh index 64a1ede..ef89f94 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 (( $# != 3 )) || [[ $1 == -* ]]; then print -r -- >&2 "$0: usage: $0 BUFFER HIGHLIGHTER BASENAME" diff --git a/tests/test-highlighting.zsh b/tests/test-highlighting.zsh index 1d169d6..243f846 100755 --- a/tests/test-highlighting.zsh +++ b/tests/test-highlighting.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/tests/test-perfs.zsh b/tests/test-perfs.zsh index 3411754..49d95c7 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 ec01306..d77ab65 100644 --- a/zsh-syntax-highlighting.zsh +++ b/zsh-syntax-highlighting.zsh @@ -42,6 +42,50 @@ 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 (( ${+functions} )); then + ## zsh/parameter is available + #(( ${+functions[$1]} )) && + [[ "$functions[$1]" == *"builtin autoload -X" ]] + else + ## zsh/parameter isn't available + #[[ $(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() { + (( ${+functions[$1]} )) || [[ $(type -wa -- "$1") == *'function'* ]] +} + +# 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 # ------------------------------------------------------------------------------------------------- @@ -249,71 +293,89 @@ _zsh_highlight_call_widget() _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 +if _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. + # + # A nested function is required to hide zle parameters; see + # "User-defined widgets" in zshall. + () { + local -h +r WIDGET=zle-line-finish + _zsh_highlight "$@" + } "$@" } + _zsh_highlight_bind_widgets(){} + add-zle-hook-widget zle-line-pre-redraw _zsh_highlight + add-zle-hook-widget zle-line-finish _zsh_highlight__zle-line-finish +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)}) - - # 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-* ]] && [[ -z $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}" - 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)}) + + # 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-* ]] && [[ -z $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}" + fi + esac + done + } +fi # Load highlighters from directory. #