Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use zle-line-pre-redraw (the feature/redrawhook branch) #749

Merged
merged 29 commits into from
Aug 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
aed99f6
wrappers: Reimplement using Mikachu's zle-line-pre-redraw hook (worke…
danielshahaf Sep 30, 2015
85e62a8
driver: Reimplement using 'add-zle-hook-widget zle-line-pre-redraw'
danielshahaf Jun 15, 2016
74a27de
driver: Hook zle-line-finish.
danielshahaf Jul 29, 2016
30c6e70
driver: Pass zle-line-finish arguments on to _zsh_highlight.
danielshahaf Aug 24, 2016
04fd6bb
changelog: Note the effect of fixing #245/#90 and an alternative.
danielshahaf Sep 5, 2016
d98622d
changelog: Use a more specific link.
danielshahaf Sep 5, 2016
38477f2
driver: Use a different way of checking whether add-zle-hook-widget i…
danielshahaf Sep 5, 2016
d4ab7e5
redo _zsh_highlight__function_callable_p
danielshahaf Sep 16, 2016
1651137
docs: Update FAQ answer per changes on this branch.
danielshahaf Oct 7, 2016
66ae59e
docs: Rewrap.
danielshahaf Oct 7, 2016
d2594c1
noop: Make a whitespace-only change to reduce noise in the next commit.
danielshahaf Oct 17, 2016
b5249f1
driver: Rewrite without a state variable
danielshahaf Oct 17, 2016
a868b69
test harness: Actually test the new code.
danielshahaf Oct 17, 2016
f665eec
driver: Avoid a fork in the common case.
danielshahaf Jan 8, 2018
d0fb0df
driver: Make the shadowing $WIDGET read only.
danielshahaf Jan 8, 2018
f265ef0
driver: Use idiomatic module check
phy1729 Jan 8, 2018
2cbb3fb
driver: Allow for -U in autoloaded function definition
phy1729 Jan 8, 2018
56ba7f0
driver: Clarify comment. No functional change.
danielshahaf Jan 9, 2018
8d4c635
driver: Do not pass widget arguments to _zsh_highlight
phy1729 Oct 14, 2018
b08d508
driver: Fix a bug that prevented subsequent, third-party zle-line-pre…
danielshahaf May 4, 2020
c28312b
Merge remote-tracking branch 'origin/master' into feature/redrawhook
danielshahaf Jul 14, 2020
daf0d94
On the feature/redrawhook branch, move the changelog entry to the cur…
danielshahaf Jul 14, 2020
7b863fb
Merge remote-tracking branch 'origin/master' into feature/redrawhook
danielshahaf Jul 14, 2020
59cb9a5
driver: Make the redrawhook codepath conditional upon the memo= feature.
danielshahaf Jul 14, 2020
9ce3540
Merge remote-tracking branch 'origin/master' into feature/redrawhook
danielshahaf Jul 14, 2020
cb33cc0
On the feature/redrawhook branch, change the detection of the 'memo='…
danielshahaf Jul 14, 2020
637e1c7
Merge remote-tracking branch 'origin/master' into feature/redrawhook
danielshahaf Jul 14, 2020
cba4a1b
On the feature/redrawhook branch, changelog: Add entries for issues f…
danielshahaf Jul 14, 2020
7cc6226
docs: Track making the new codepath conditional upon the 'memo=' feat…
danielshahaf Aug 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 <kbd>⮀ Tab</kbd> 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`.
Expand Down
3 changes: 3 additions & 0 deletions tests/generate.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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]"
Expand Down
3 changes: 3 additions & 0 deletions tests/test-highlighting.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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=("$@")

Expand Down
3 changes: 3 additions & 0 deletions tests/test-perfs.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
220 changes: 155 additions & 65 deletions zsh-syntax-highlighting.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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 <keys> ${(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 <keys> ${(q-)cur_widget}\` without creating the ${(qq)cur_widget} widget with \`zle -N\` or \`zle -C\`.)"
fi
esac
done
}
fi

# Load highlighters from directory.
#
Expand Down