Skip to content

Commit cb97bde

Browse files
committed
Perf: speed up _zsh_highlight_highlighter_main_paint
1 parent 9ba6860 commit cb97bde

File tree

2 files changed

+155
-123
lines changed

2 files changed

+155
-123
lines changed

highlighters/main/main-highlighter.zsh

Lines changed: 150 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -104,45 +104,30 @@ _zsh_highlight_main_add_many_region_highlights() {
104104
done
105105
}
106106

107+
_zsh_highlight_main_calculate_styles() {
108+
local config="${(pj:\0:)${(@kv)ZSH_HIGHLIGHT_STYLES}}".
109+
[[ $config == $_zsh_highlight_main__config ]] && return
110+
_zsh_highlight_main__config=$config
111+
typeset -gA _zsh_highlight_main__styles=("${(@kv)ZSH_HIGHLIGHT_STYLES}")
112+
113+
integer finished
114+
local key val
115+
while (( !finished )); do
116+
finished=1
117+
for key val in ${(@kv)_zsh_highlight_main__fallback_of}; do
118+
[[ -n $_zsh_highlight_main__styles[$key] ]] && continue
119+
if [[ -z $_zsh_highlight_main__styles[$key] &&
120+
-n ${_zsh_highlight_main__styles[$key]::=${_zsh_highlight_main__styles[$val]}} ]]; then
121+
finished=0
122+
fi
123+
done
124+
done
125+
}
126+
107127
_zsh_highlight_main_calculate_fallback() {
108-
local -A fallback_of; fallback_of=(
109-
alias arg0
110-
suffix-alias arg0
111-
global-alias dollar-double-quoted-argument
112-
builtin arg0
113-
function arg0
114-
command arg0
115-
precommand arg0
116-
hashed-command arg0
117-
autodirectory arg0
118-
arg0_\* arg0
119-
120-
# TODO: Maybe these? —
121-
# named-fd file-descriptor
122-
# numeric-fd file-descriptor
123-
124-
path_prefix path
125-
# The path separator fallback won't ever be used, due to the optimisation
126-
# in _zsh_highlight_main_highlighter_highlight_path_separators().
127-
path_pathseparator path
128-
path_prefix_pathseparator path_prefix
129-
130-
single-quoted-argument{-unclosed,}
131-
double-quoted-argument{-unclosed,}
132-
dollar-quoted-argument{-unclosed,}
133-
back-quoted-argument{-unclosed,}
134-
135-
command-substitution{-quoted,,-unquoted,}
136-
command-substitution-delimiter{-quoted,,-unquoted,}
137-
138-
command-substitution{-delimiter,}
139-
process-substitution{-delimiter,}
140-
back-quoted-argument{-delimiter,}
141-
)
142128
local needle=$1 value
143129
reply=($1)
144-
while [[ -n ${value::=$fallback_of[(k)$needle]} ]]; do
145-
unset "fallback_of[$needle]" # paranoia against infinite loops
130+
while [[ -n ${value::=$_zsh_highlight_main__fallback_of[(k)$needle]} ]]; do
146131
reply+=($value)
147132
needle=$value
148133
done
@@ -283,8 +268,8 @@ _zsh_highlight_main__resolve_alias() {
283268
# Return true iff $1 is a global alias
284269
_zsh_highlight_main__is_global_alias() {
285270
if zmodload -e zsh/parameter; then
286-
(( ${+galiases[$arg]} ))
287-
elif [[ $arg == '='* ]]; then
271+
(( ${+galiases[$1]} ))
272+
elif [[ $1 == '='* ]]; then
288273
# avoid running into «alias -L '=foo'» erroring out with 'bad assignment'
289274
return 1
290275
else
@@ -325,8 +310,6 @@ _zsh_highlight_highlighter_main_paint()
325310
return
326311
fi
327312

328-
typeset -a ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR
329-
typeset -a ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW
330313
local -a options_to_set reply # used in callees
331314
local REPLY
332315

@@ -340,88 +323,23 @@ _zsh_highlight_highlighter_main_paint()
340323
# present, mean the precommand will not be acting as a precommand, i.e., will
341324
# not be followed by a :start: word.
342325
local flags_solo
343-
# $precommand_options maps precommand name to values of $flags_with_argument,
344-
# $flags_sans_argument, and flags_solo for that precommand, joined by a
345-
# colon. (The value is NOT a getopt(3) spec, although it resembles one.)
326+
# $_zsh_highlight_main__precommand_options maps precommand name to values of
327+
# $flags_with_argument, $flags_sans_argument, and flags_solo for that precommand,
328+
# joined by a colon. (The value is NOT a getopt(3) spec, although it resembles one.)
346329
#
347330
# Currently, setting $flags_sans_argument is only important for commands that
348331
# have a non-empty $flags_with_argument; see test-data/precommand4.zsh.
349-
local -A precommand_options
350-
precommand_options=(
351-
# Precommand modifiers as of zsh 5.6.2 cf. zshmisc(1).
352-
'-' ''
353-
'builtin' ''
354-
'command' :pvV
355-
'exec' a:cl
356-
'noglob' ''
357-
# 'time' and 'nocorrect' shouldn't be added here; they're reserved words, not precommands.
358-
359-
'doas' aCu:Lns # as of OpenBSD's doas(1) dated September 4, 2016
360-
'nice' n: # as of current POSIX spec
361-
'pkexec' '' # doesn't take short options; immune to #121 because it's usually not passed --option flags
362-
# Not listed: -h, which has two different meanings.
363-
'sudo' Cgprtu:AEHPSbilns:eKkVv # as of sudo 1.8.21p2
364-
'stdbuf' ioe:
365-
'eatmydata' ''
366-
'catchsegv' ''
367-
'nohup' ''
368-
'setsid' :wc
369-
'env' u:i
370-
'ionice' cn:t:pPu # util-linux 2.33.1-0.1
371-
'strace' IbeaosXPpEuOS:ACdfhikqrtTvVxyDc # strace 4.26-0.2
372-
373-
# As of OpenSSH 8.1p1
374-
'ssh-agent' aEPt:csDd:k
375-
# suckless-tools v44
376-
# Argumentless flags that can't be followed by a command: -v
377-
'tabbed' gnprtTuU:cdfhs
378-
379-
# moreutils 0.62-1
380-
'chronic' :ev
381-
'ifne' :n
382-
383-
)
384-
# Commands that would need to skip one positional argument:
385-
# flock
386-
# ssh
387332

388-
if [[ $zsyh_user_options[ignorebraces] == on || ${zsyh_user_options[ignoreclosebraces]:-off} == on ]]; then
389-
local right_brace_is_recognised_everywhere=false
333+
if [[ $zsyh_user_options[ignorebraces] == on || $zsyh_user_options[ignoreclosebraces] == on ]]; then
334+
local -i right_brace_is_recognised_everywhere=0
390335
else
391-
local right_brace_is_recognised_everywhere=true
336+
local -i right_brace_is_recognised_everywhere=1
392337
fi
393338

394339
if [[ $zsyh_user_options[pathdirs] == on ]]; then
395340
options_to_set+=( PATH_DIRS )
396341
fi
397342

398-
ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR=(
399-
'|' '||' ';' '&' '&&'
400-
$'\n' # ${(z)} returns ';' but we convert it to $'\n'
401-
'|&'
402-
'&!' '&|'
403-
# ### 'case' syntax, but followed by a pattern, not by a command
404-
# ';;' ';&' ';|'
405-
)
406-
407-
# Tokens that, at (naively-determined) "command position", are followed by
408-
# a de jure command position. All of these are reserved words.
409-
ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW=(
410-
$'\x7b' # block
411-
$'\x28' # subshell
412-
'()' # anonymous function
413-
'while'
414-
'until'
415-
'if'
416-
'then'
417-
'elif'
418-
'else'
419-
'do'
420-
'time'
421-
'coproc'
422-
'!' # reserved word; unrelated to $histchars[1]
423-
)
424-
425343
if (( $+X_ZSH_HIGHLIGHT_DIRS_BLACKLIST )); then
426344
print >&2 'zsh-syntax-highlighting: X_ZSH_HIGHLIGHT_DIRS_BLACKLIST is deprecated. Please use ZSH_HIGHLIGHT_DIRS_BLACKLIST.'
427345
ZSH_HIGHLIGHT_DIRS_BLACKLIST=($X_ZSH_HIGHLIGHT_DIRS_BLACKLIST)
@@ -431,13 +349,19 @@ _zsh_highlight_highlighter_main_paint()
431349
_zsh_highlight_main_highlighter_highlight_list -$#PREBUFFER '' 1 "$PREBUFFER$BUFFER"
432350

433351
# end is a reserved word
434-
local start end_ style
352+
integer start end_
353+
local style
435354
for start end_ style in $reply; do
436355
(( start >= end_ )) && { print -r -- >&2 "zsh-syntax-highlighting: BUG: _zsh_highlight_highlighter_main_paint: start($start) >= end($end_)"; return }
437356
(( end_ <= 0 )) && continue
438357
(( start < 0 )) && start=0 # having start<0 is normal with e.g. multiline strings
439-
_zsh_highlight_main_calculate_fallback $style
440-
_zsh_highlight_add_highlight $start $end_ $reply
358+
if (( $+_zsh_highlight_main__styles )); then
359+
style=$_zsh_highlight_main__styles[$style]
360+
[[ -n $style ]] && region_highlight+=("$start $end_ $style, memo=zsh-syntax-highlighting")
361+
else
362+
_zsh_highlight_main_calculate_fallback $style
363+
_zsh_highlight_add_highlight $start $end_ $reply
364+
fi
441365
done
442366
}
443367

@@ -545,8 +469,8 @@ _zsh_highlight_main_highlighter_highlight_list()
545469
# when given as a separate word; i.e., "foo" in "-u foo" (two
546470
# words) but not in "-ufoo" (one word).
547471
# Note: :sudo_opt: and :sudo_arg: are used for any precommand
548-
# declared in ${precommand_options}, not just for sudo(8).
549-
# The naming is historical.
472+
# declared in ${_zsh_highlight_main__precommand_options}, not just
473+
# for sudo(8). The naming is historical.
550474
# - :regular: "Not a command word", and command delimiters are permitted.
551475
# Mainly used to detect premature termination of commands.
552476
# - :always: The word 'always' in the «{ foo } always { bar }» syntax.
@@ -840,7 +764,7 @@ _zsh_highlight_main_highlighter_highlight_list()
840764
fi
841765

842766
# The Great Fork: is this a command word? Is this a non-command word?
843-
if [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_COMMANDSEPARATOR:#"$arg"} ]] &&
767+
if [[ -n ${(M)_zsh_highlight_main__tokens_commandseparator:#"$arg"} ]] &&
844768
[[ $braces_stack != *T* || $arg != ('||'|'&&') ]]; then
845769

846770
# First, determine the style of the command separator itself.
@@ -899,10 +823,10 @@ _zsh_highlight_main_highlighter_highlight_list()
899823
saw_assignment=false
900824
next_word=':start::start_of_pipeline:' # only left brace is allowed, apparently
901825
elif ! (( in_redirection)) && [[ $this_word == *':start:'* ]]; then # $arg is the command word
902-
if (( ${+precommand_options[$arg]} )) && _zsh_highlight_main__is_runnable $arg; then
826+
if (( ${+_zsh_highlight_main__precommand_options[$arg]} )) && _zsh_highlight_main__is_runnable $arg; then
903827
style=precommand
904828
() {
905-
set -- "${(@s.:.)precommand_options[$arg]}"
829+
set -- "${(@s.:.)_zsh_highlight_main__precommand_options[$arg]}"
906830
flags_with_argument=$1
907831
flags_sans_argument=$2
908832
flags_solo=$3
@@ -928,7 +852,7 @@ _zsh_highlight_main_highlighter_highlight_list()
928852
braces_stack='Y'"$braces_stack"
929853
;;
930854
($'\x7d')
931-
# We're at command word, so no need to check $right_brace_is_recognised_everywhere
855+
# We're at command word, so no need to check right_brace_is_recognised_everywhere
932856
_zsh_highlight_main__stack_pop 'Y' reserved-word
933857
if [[ $style == reserved-word ]]; then
934858
next_word+=':always:'
@@ -1087,7 +1011,7 @@ _zsh_highlight_main_highlighter_highlight_list()
10871011
;;
10881012
esac
10891013
fi
1090-
if [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_CONTROL_FLOW:#"$arg"} ]]; then
1014+
if [[ -n ${(M)_zsh_highlight_main__tokens_control_flow:#"$arg"} ]]; then
10911015
next_word=':start::start_of_pipeline:'
10921016
fi
10931017
elif _zsh_highlight_main__is_global_alias "$arg"; then # $arg is a global alias that isn't in command position
@@ -1126,7 +1050,7 @@ _zsh_highlight_main_highlighter_highlight_list()
11261050
fi
11271051
;;
11281052
(*) if false; then
1129-
elif [[ $arg = $'\x7d' ]] && $right_brace_is_recognised_everywhere; then
1053+
elif [[ $arg = $'\x7d' ]] && (( right_brace_is_recognised_everywhere )); then
11301054
# Parsing rule: {
11311055
#
11321056
# Additionally, `tt(})' is recognized in any position if neither the
@@ -1313,7 +1237,7 @@ _zsh_highlight_main_highlighter_highlight_argument()
13131237
local base_style=default i=$1 option_eligible=${2:-1} path_eligible=1 ret start style
13141238
local -a highlights
13151239

1316-
local -a match mbegin mend
1240+
local -a match mbegin mend reply
13171241
local MATCH; integer MBEGIN MEND
13181242

13191243
case "$arg[i]" in
@@ -1837,6 +1761,10 @@ _zsh_highlight_main__precmd_hook() {
18371761

18381762
_zsh_highlight_main__command_type_cache=()
18391763
_zsh_highlight_main__path_cache=()
1764+
1765+
if [[ $ZSH_VERSION != (5.<9->*|<6->.*) ]]; then
1766+
_zsh_highlight_main_calculate_styles
1767+
fi
18401768
}
18411769

18421770
autoload -Uz add-zsh-hook
@@ -1849,3 +1777,102 @@ else
18491777
unset _zsh_highlight_main__command_type_cache _zsh_highlight_main__path_cache
18501778
fi
18511779
typeset -ga ZSH_HIGHLIGHT_DIRS_BLACKLIST
1780+
1781+
typeset -gA _zsh_highlight_main__precommand_options=(
1782+
# Precommand modifiers as of zsh 5.6.2 cf. zshmisc(1).
1783+
'-' ''
1784+
'builtin' ''
1785+
'command' :pvV
1786+
'exec' a:cl
1787+
'noglob' ''
1788+
# 'time' and 'nocorrect' shouldn't be added here; they're reserved words, not precommands.
1789+
1790+
'doas' aCu:Lns # as of OpenBSD's doas(1) dated September 4, 2016
1791+
'nice' n: # as of current POSIX spec
1792+
'pkexec' '' # doesn't take short options; immune to #121 because it's usually not passed --option flags
1793+
# Not listed: -h, which has two different meanings.
1794+
'sudo' Cgprtu:AEHPSbilns:eKkVv # as of sudo 1.8.21p2
1795+
'stdbuf' ioe:
1796+
'eatmydata' ''
1797+
'catchsegv' ''
1798+
'nohup' ''
1799+
'setsid' :wc
1800+
'env' u:i
1801+
'ionice' cn:t:pPu # util-linux 2.33.1-0.1
1802+
'strace' IbeaosXPpEuOS:ACdfhikqrtTvVxyDc # strace 4.26-0.2
1803+
1804+
# As of OpenSSH 8.1p1
1805+
'ssh-agent' aEPt:csDd:k
1806+
# suckless-tools v44
1807+
# Argumentless flags that can't be followed by a command: -v
1808+
'tabbed' gnprtTuU:cdfhs
1809+
1810+
# moreutils 0.62-1
1811+
'chronic' :ev
1812+
'ifne' :n
1813+
)
1814+
# Commands that would need to skip one positional argument:
1815+
# flock
1816+
# ssh
1817+
1818+
typeset -ga _zsh_highlight_main__tokens_commandseparator=(
1819+
'|' '||' ';' '&' '&&'
1820+
$'\n' # ${(z)} returns ';' but we convert it to $'\n'
1821+
'|&'
1822+
'&!' '&|'
1823+
# ### 'case' syntax, but followed by a pattern, not by a command
1824+
# ';;' ';&' ';|'
1825+
)
1826+
1827+
# Tokens that, at (naively-determined) "command position", are followed by
1828+
# a de jure command position. All of these are reserved words.
1829+
typeset -ga _zsh_highlight_main__tokens_control_flow=(
1830+
$'\x7b' # block
1831+
$'\x28' # subshell
1832+
'()' # anonymous function
1833+
'while'
1834+
'until'
1835+
'if'
1836+
'then'
1837+
'elif'
1838+
'else'
1839+
'do'
1840+
'time'
1841+
'coproc'
1842+
'!' # reserved word; unrelated to $histchars[1]
1843+
)
1844+
1845+
typeset -gA _zsh_highlight_main__fallback_of=(
1846+
alias arg0
1847+
suffix-alias arg0
1848+
global-alias dollar-double-quoted-argument
1849+
builtin arg0
1850+
function arg0
1851+
command arg0
1852+
precommand arg0
1853+
hashed-command arg0
1854+
autodirectory arg0
1855+
arg0_\* arg0
1856+
1857+
# TODO: Maybe these? —
1858+
# named-fd file-descriptor
1859+
# numeric-fd file-descriptor
1860+
1861+
path_prefix path
1862+
# The path separator fallback won't ever be used, due to the optimisation
1863+
# in _zsh_highlight_main_highlighter_highlight_path_separators().
1864+
path_pathseparator path
1865+
path_prefix_pathseparator path_prefix
1866+
1867+
single-quoted-argument{-unclosed,}
1868+
double-quoted-argument{-unclosed,}
1869+
dollar-quoted-argument{-unclosed,}
1870+
back-quoted-argument{-unclosed,}
1871+
1872+
command-substitution{-quoted,,-unquoted,}
1873+
command-substitution-delimiter{-quoted,,-unquoted,}
1874+
1875+
command-substitution{-delimiter,}
1876+
process-substitution{-delimiter,}
1877+
back-quoted-argument{-delimiter,}
1878+
)

tests/test-highlighting.zsh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ _zsh_highlight_add_highlight()
9191
region_highlight+=("$1 $2 $3")
9292
}
9393

94+
_zsh_highlight_main_calculate_styles()
95+
{
96+
# Do nothing
97+
}
98+
9499
# Activate the highlighter.
95100
ZSH_HIGHLIGHT_HIGHLIGHTERS=($1)
96101

0 commit comments

Comments
 (0)