Skip to content

Commit 239c720

Browse files
committed
Merge branch 'feature/redrawhook'
* feature/redrawhook: docs: Track making the new codepath conditional upon the 'memo=' feature. On the feature/redrawhook branch, changelog: Add entries for issues fixed by this branch. On the feature/redrawhook branch, change the detection of the 'memo=' feature to avoid a catch-22. driver: Make the redrawhook codepath conditional upon the memo= feature. On the feature/redrawhook branch, move the changelog entry to the current release's section. driver: Fix a bug that prevented subsequent, third-party zle-line-pre-redraw hooks from running. driver: Do not pass widget arguments to _zsh_highlight driver: Clarify comment. No functional change. driver: Allow for -U in autoloaded function definition driver: Use idiomatic module check driver: Make the shadowing $WIDGET read only. driver: Avoid a fork in the common case. test harness: Actually test the new code. driver: Rewrite without a state variable noop: Make a whitespace-only change to reduce noise in the next commit. docs: Rewrap. docs: Update FAQ answer per changes on this branch. redo _zsh_highlight__function_callable_p driver: Use a different way of checking whether add-zle-hook-widget is present. changelog: Use a more specific link. changelog: Note the effect of fixing #245/#90 and an alternative. driver: Pass zle-line-finish arguments on to _zsh_highlight. driver: Hook zle-line-finish. driver: Reimplement using 'add-zle-hook-widget zle-line-pre-redraw' wrappers: Reimplement using Mikachu's zle-line-pre-redraw hook (workers/36650).
2 parents 2d60a47 + 7cc6226 commit 239c720

File tree

6 files changed

+249
-68
lines changed

6 files changed

+249
-68
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,23 @@ FAQ
4444

4545
### Why must `zsh-syntax-highlighting.zsh` be sourced at the end of the `.zshrc` file?
4646

47-
`zsh-syntax-highlighting.zsh` wraps ZLE widgets. It must be sourced after all
48-
custom widgets have been created (i.e., after all `zle -N` calls and after
49-
running `compinit`). Widgets created later will work, but will not update the
47+
zsh-syntax-highlighting works by hooking into the Zsh Line Editor (ZLE) and
48+
computing syntax highlighting for the command-line buffer as it stands at the
49+
time z-sy-h's hook is invoked.
50+
51+
In zsh 5.2 and older,
52+
`zsh-syntax-highlighting.zsh` hooks into ZLE by wrapping ZLE widgets. It must
53+
be sourced after all custom widgets have been created (i.e., after all `zle -N`
54+
calls and after running `compinit`) in order to be able to wrap all of them.
55+
Widgets created after z-sy-h is sourced will work, but will not update the
5056
syntax highlighting.
5157

58+
In zsh newer than 5.8 (not including 5.8 itself),
59+
zsh-syntax-highlighting uses the `add-zle-hook-widget` facility to install
60+
a `zle-line-pre-redraw` hook. Hooks are run in order of registration,
61+
therefore, z-sy-h must be sourced (and register its hook) after anything else
62+
that adds hooks that modify the command-line buffer.
63+
5264
### Does syntax highlighting work during incremental history search?
5365

5466
Highlighting the command line during an incremental history search (by default bound to

changelog.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,57 @@
11
# Changes in HEAD
22

33

4+
## Changes fixed as part of the switch to zle-line-pre-redraw
5+
6+
The changes in this section were fixed by switching to a `zle-line-pre-redraw`-based
7+
implementation.
8+
9+
Note: The new implementation will only be used on future zsh releases,
10+
numbered 5.8.0.3 and newer, due to interoperability issues with other plugins
11+
(issues #418 and #579). The underlying zsh feature has been available since
12+
zsh 5.2.
13+
14+
Whilst under development, the new implementation was known as the
15+
"feature/redrawhook" topic branch.
16+
17+
- Fixed: Highlighting not triggered after popping a buffer from the buffer stack
18+
(using the `push-line` widget, default binding: `M-q`)
19+
[#40]
20+
21+
- Fixed: Invoking completion when there were no matches removed highlighting
22+
[#90, #470]
23+
24+
- Fixed: Two successive deletes followed by a yank only yanked the latest
25+
delete, rather than both of them
26+
[#150, #151, #160; cf. #183]
27+
28+
- Presumed fixed: Completing `$(xsel)` results in an error message from `xsel`,
29+
with pre-2017 versions of `xsel`. (For 2017 vintage and newer, see the issue
30+
for details.)
31+
[#154]
32+
33+
- Fixed: When the standard `bracketed-paste-magic` widget is in use, pastes were slow
34+
[#295]
35+
36+
- Fixed: No way to prevent a widget from being wrapped
37+
[#324]
38+
39+
- Fixed: No highlighting while cycling menu completion
40+
[#375]
41+
42+
- Fixed: Does not coexist with the `IGNORE_EOF` option
43+
[#377]
44+
45+
- Fixed: The `undefined-key` widget was wrapped
46+
[#421]
47+
48+
- Fixed: Does not coexist with the standard `surround` family of widgets
49+
[#520]
50+
51+
- Fixed: First completed filename doesn't get `path` highlighting
52+
[#632]
53+
54+
455
# Changes in 0.8.0-alpha1-pre-redrawhook
556

657
## 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
1970
to issue #418.
2071

2172

73+
## Incompatible changes:
74+
75+
- An unsuccessful completion (a <kbd>⮀ Tab</kbd> press that doesn't change the
76+
command line) no longer causes highlighting to be lost. Visual feedback can
77+
alternatively be achieved by setting the `format` zstyle under the `warnings`
78+
tag, for example,
79+
80+
zstyle ':completion:*:warnings' format '%F{red}No matches%f'
81+
82+
Refer to the [description of the `format` style in `zshcompsys(1)`]
83+
[zshcompsys-Standard-Styles-format].
84+
85+
(#90, part of #245 (feature/redrawhook))
86+
87+
[zshcompsys-Standard-Styles]: http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Standard-Styles
88+
[zshcompsys-Standard-Styles-format]: http://zsh.sourceforge.net/Doc/Release/Completion-System.html#index-format_002c-completion-style
89+
90+
91+
2292
## Other changes:
2393

2494
- Document `$ZSH_HIGHLIGHT_MAXLENGTH`.

tests/generate.zsh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
emulate -LR zsh
3232
setopt localoptions extendedglob
3333

34+
# Required for add-zle-hook-widget.
35+
zmodload zsh/zle
36+
3437
# Argument parsing.
3538
if (( $# * $# - 7 * $# + 12 )) || [[ $1 == -* ]]; then
3639
print -r -- >&2 "$0: usage: $0 BUFFER HIGHLIGHTER BASENAME [PREAMBLE]"

tests/test-highlighting.zsh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
setopt NO_UNSET WARN_CREATE_GLOBAL
3333

34+
# Required for add-zle-hook-widget.
35+
zmodload zsh/zle
36+
3437
local -r root=${0:h:h}
3538
local -a anon_argv; anon_argv=("$@")
3639

tests/test-perfs.zsh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
# -------------------------------------------------------------------------------------------------
3030

3131

32+
# Required for add-zle-hook-widget.
33+
zmodload zsh/zle
34+
3235
# Check an highlighter was given as argument.
3336
[[ -n "$1" ]] || {
3437
echo >&2 "Bail out! You must provide the name of a valid highlighter as argument."

zsh-syntax-highlighting.zsh

Lines changed: 155 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,52 @@ if true; then
4949
fi
5050
fi
5151

52+
# This function takes a single argument F and returns True iff F is an autoload stub.
53+
_zsh_highlight__function_is_autoload_stub_p() {
54+
if zmodload -e zsh/parameter; then
55+
#(( ${+functions[$1]} )) &&
56+
[[ "$functions[$1]" == *"builtin autoload -X"* ]]
57+
else
58+
#[[ $(type -wa -- "$1") == *'function'* ]] &&
59+
[[ "${${(@f)"$(which -- "$1")"}[2]}" == $'\t'$histchars[3]' undefined' ]]
60+
fi
61+
# Do nothing here: return the exit code of the if.
62+
}
63+
64+
# Return True iff the argument denotes a function name.
65+
_zsh_highlight__is_function_p() {
66+
if zmodload -e zsh/parameter; then
67+
(( ${+functions[$1]} ))
68+
else
69+
[[ $(type -wa -- "$1") == *'function'* ]]
70+
fi
71+
}
72+
73+
# This function takes a single argument F and returns True iff F denotes the
74+
# name of a callable function. A function is callable if it is fully defined
75+
# or if it is marked for autoloading and autoloading it at the first call to it
76+
# will succeed. In particular, if a function has been marked for autoloading
77+
# but is not available in $fpath, then this function will return False therefor.
78+
#
79+
# See users/21671 http://www.zsh.org/cgi-bin/mla/redirect?USERNUMBER=21671
80+
_zsh_highlight__function_callable_p() {
81+
if _zsh_highlight__is_function_p "$1" &&
82+
! _zsh_highlight__function_is_autoload_stub_p "$1"
83+
then
84+
# Already fully loaded.
85+
return 0 # true
86+
else
87+
# "$1" is either an autoload stub, or not a function at all.
88+
#
89+
# Use a subshell to avoid affecting the calling shell.
90+
#
91+
# We expect 'autoload +X' to return non-zero if it fails to fully load
92+
# the function.
93+
( autoload -U +X -- "$1" 2>/dev/null )
94+
return $?
95+
fi
96+
}
97+
5298
# -------------------------------------------------------------------------------------------------
5399
# Core highlighting update system
54100
# -------------------------------------------------------------------------------------------------
@@ -347,76 +393,120 @@ _zsh_highlight_add_highlight()
347393
# $1 is name of widget to call
348394
_zsh_highlight_call_widget()
349395
{
350-
builtin zle "$@" &&
396+
builtin zle "$@" &&
351397
_zsh_highlight
352398
}
353399

354-
# Rebind all ZLE widgets to make them invoke _zsh_highlights.
355-
_zsh_highlight_bind_widgets()
356-
{
357-
setopt localoptions noksharrays
358-
typeset -F SECONDS
359-
local prefix=orig-s$SECONDS-r$RANDOM # unique each time, in case we're sourced more than once
360-
361-
# Load ZSH module zsh/zleparameter, needed to override user defined widgets.
362-
zmodload zsh/zleparameter 2>/dev/null || {
363-
print -r -- >&2 'zsh-syntax-highlighting: failed loading zsh/zleparameter.'
364-
return 1
400+
# Decide whether to use the zle-line-pre-redraw codepath (colloquially known as
401+
# "feature/redrawhook", after the topic branch's name) or the legacy "bind all
402+
# widgets" codepath.
403+
#
404+
# We use the new codepath under two conditions:
405+
#
406+
# 1. If it's available, which we check by testing for add-zle-hook-widget's availability.
407+
#
408+
# 2. If zsh has the memo= feature, which is required for interoperability reasons.
409+
# See issues #579 and #735, and the issues referenced from them.
410+
#
411+
# We check this with a plain version number check, since a functional check,
412+
# as done by _zsh_highlight, can only be done from inside a widget
413+
# function — a catch-22.
414+
#
415+
# See _zsh_highlight for the magic version number. (The use of 5.8.0.2
416+
# rather than 5.8.0.3 as in the _zsh_highlight is deliberate.)
417+
if is-at-least 5.8.0.2 && _zsh_highlight__function_callable_p add-zle-hook-widget
418+
then
419+
autoload -U add-zle-hook-widget
420+
_zsh_highlight__zle-line-finish() {
421+
# Reset $WIDGET since the 'main' highlighter depends on it.
422+
#
423+
# Since $WIDGET is declared by zle as read-only in this function's scope,
424+
# a nested function is required in order to shadow its built-in value;
425+
# see "User-defined widgets" in zshall.
426+
() {
427+
local -h -r WIDGET=zle-line-finish
428+
_zsh_highlight
429+
}
365430
}
431+
_zsh_highlight__zle-line-pre-redraw() {
432+
# Set $? to 0 for _zsh_highlight. Without this, subsequent
433+
# zle-line-pre-redraw hooks won't run, since add-zle-hook-widget happens to
434+
# call us with $? == 1 in the common case.
435+
true && _zsh_highlight "$@"
436+
}
437+
_zsh_highlight_bind_widgets(){}
438+
if [[ -o zle ]]; then
439+
add-zle-hook-widget zle-line-pre-redraw _zsh_highlight__zle-line-pre-redraw
440+
add-zle-hook-widget zle-line-finish _zsh_highlight__zle-line-finish
441+
fi
442+
else
443+
# Rebind all ZLE widgets to make them invoke _zsh_highlights.
444+
_zsh_highlight_bind_widgets()
445+
{
446+
setopt localoptions noksharrays
447+
typeset -F SECONDS
448+
local prefix=orig-s$SECONDS-r$RANDOM # unique each time, in case we're sourced more than once
449+
450+
# Load ZSH module zsh/zleparameter, needed to override user defined widgets.
451+
zmodload zsh/zleparameter 2>/dev/null || {
452+
print -r -- >&2 'zsh-syntax-highlighting: failed loading zsh/zleparameter.'
453+
return 1
454+
}
366455

367-
# Override ZLE widgets to make them invoke _zsh_highlight.
368-
local -U widgets_to_bind
369-
widgets_to_bind=(${${(k)widgets}:#(.*|run-help|which-command|beep|set-local-history|yank|yank-pop)})
370-
371-
# Always wrap special zle-line-finish widget. This is needed to decide if the
372-
# current line ends and special highlighting logic needs to be applied.
373-
# E.g. remove cursor imprint, don't highlight partial paths, ...
374-
widgets_to_bind+=(zle-line-finish)
375-
376-
# Always wrap special zle-isearch-update widget to be notified of updates in isearch.
377-
# This is needed because we need to disable highlighting in that case.
378-
widgets_to_bind+=(zle-isearch-update)
379-
380-
local cur_widget
381-
for cur_widget in $widgets_to_bind; do
382-
case ${widgets[$cur_widget]:-""} in
383-
384-
# Already rebound event: do nothing.
385-
user:_zsh_highlight_widget_*);;
386-
387-
# The "eval"'s are required to make $cur_widget a closure: the value of the parameter at function
388-
# definition time is used.
389-
#
390-
# We can't use ${0/_zsh_highlight_widget_} because these widgets are always invoked with
391-
# NO_function_argzero, regardless of the option's setting here.
392-
393-
# User defined widget: override and rebind old one with prefix "orig-".
394-
user:*) zle -N $prefix-$cur_widget ${widgets[$cur_widget]#*:}
395-
eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
396-
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
397-
398-
# Completion widget: override and rebind old one with prefix "orig-".
399-
completion:*) zle -C $prefix-$cur_widget ${${(s.:.)widgets[$cur_widget]}[2,3]}
400-
eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
401-
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
402-
403-
# Builtin widget: override and make it call the builtin ".widget".
404-
builtin) eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget .${(q)cur_widget} -- \"\$@\" }"
405-
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
406-
407-
# Incomplete or nonexistent widget: Bind to z-sy-h directly.
408-
*)
409-
if [[ $cur_widget == zle-* ]] && (( ! ${+widgets[$cur_widget]} )); then
410-
_zsh_highlight_widget_${cur_widget}() { :; _zsh_highlight }
411-
zle -N $cur_widget _zsh_highlight_widget_$cur_widget
412-
else
413-
# Default: unhandled case.
414-
print -r -- >&2 "zsh-syntax-highlighting: unhandled ZLE widget ${(qq)cur_widget}"
415-
print -r -- >&2 "zsh-syntax-highlighting: (This is sometimes caused by doing \`bindkey <keys> ${(q-)cur_widget}\` without creating the ${(qq)cur_widget} widget with \`zle -N\` or \`zle -C\`.)"
416-
fi
417-
esac
418-
done
419-
}
456+
# Override ZLE widgets to make them invoke _zsh_highlight.
457+
local -U widgets_to_bind
458+
widgets_to_bind=(${${(k)widgets}:#(.*|run-help|which-command|beep|set-local-history|yank|yank-pop)})
459+
460+
# Always wrap special zle-line-finish widget. This is needed to decide if the
461+
# current line ends and special highlighting logic needs to be applied.
462+
# E.g. remove cursor imprint, don't highlight partial paths, ...
463+
widgets_to_bind+=(zle-line-finish)
464+
465+
# Always wrap special zle-isearch-update widget to be notified of updates in isearch.
466+
# This is needed because we need to disable highlighting in that case.
467+
widgets_to_bind+=(zle-isearch-update)
468+
469+
local cur_widget
470+
for cur_widget in $widgets_to_bind; do
471+
case ${widgets[$cur_widget]:-""} in
472+
473+
# Already rebound event: do nothing.
474+
user:_zsh_highlight_widget_*);;
475+
476+
# The "eval"'s are required to make $cur_widget a closure: the value of the parameter at function
477+
# definition time is used.
478+
#
479+
# We can't use ${0/_zsh_highlight_widget_} because these widgets are always invoked with
480+
# NO_function_argzero, regardless of the option's setting here.
481+
482+
# User defined widget: override and rebind old one with prefix "orig-".
483+
user:*) zle -N $prefix-$cur_widget ${widgets[$cur_widget]#*:}
484+
eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
485+
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
486+
487+
# Completion widget: override and rebind old one with prefix "orig-".
488+
completion:*) zle -C $prefix-$cur_widget ${${(s.:.)widgets[$cur_widget]}[2,3]}
489+
eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget ${(q)prefix}-${(q)cur_widget} -- \"\$@\" }"
490+
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
491+
492+
# Builtin widget: override and make it call the builtin ".widget".
493+
builtin) eval "_zsh_highlight_widget_${(q)prefix}-${(q)cur_widget}() { _zsh_highlight_call_widget .${(q)cur_widget} -- \"\$@\" }"
494+
zle -N $cur_widget _zsh_highlight_widget_$prefix-$cur_widget;;
495+
496+
# Incomplete or nonexistent widget: Bind to z-sy-h directly.
497+
*)
498+
if [[ $cur_widget == zle-* ]] && (( ! ${+widgets[$cur_widget]} )); then
499+
_zsh_highlight_widget_${cur_widget}() { :; _zsh_highlight }
500+
zle -N $cur_widget _zsh_highlight_widget_$cur_widget
501+
else
502+
# Default: unhandled case.
503+
print -r -- >&2 "zsh-syntax-highlighting: unhandled ZLE widget ${(qq)cur_widget}"
504+
print -r -- >&2 "zsh-syntax-highlighting: (This is sometimes caused by doing \`bindkey <keys> ${(q-)cur_widget}\` without creating the ${(qq)cur_widget} widget with \`zle -N\` or \`zle -C\`.)"
505+
fi
506+
esac
507+
done
508+
}
509+
fi
420510

421511
# Load highlighters from directory.
422512
#

0 commit comments

Comments
 (0)