From 0772ddd3468f43db9a086ea1eeb7e064cb9717c9 Mon Sep 17 00:00:00 2001 From: Julien Nicoulaud Date: Sun, 12 Jun 2011 22:57:14 +0200 Subject: [PATCH] Big refactoring. * Don't override user defined styles * Better modularisation of highlighters * Allow to define which highlighters are activated * Allow to define the order in which they are defined * Minor performance optimizations * Fixed some variables leak * Improve documentation * Brackets highlighter: use ZSH_HIGHLIGHT_STYLES instead of a specific array --- README.md | 58 +-- highlighters/README.md | 47 +++ highlighters/brackets/README.md | 28 ++ .../brackets-highlighter.zsh} | 39 +- highlighters/main/README.md | 55 +++ highlighters/main/main-highlighter.zsh | 164 +++++++++ .../main/test-data}/multiple-redirections.zsh | 0 .../main/test-data}/nested-parentheses.zsh | 0 .../main/test-data}/simple-command.zsh | 0 .../main/test-data}/simple-redirection.zsh | 0 .../main/test-data}/unknown-command.zsh | 0 highlighters/pattern/README.md | 21 ++ .../pattern-highlighter.zsh} | 34 +- tests/README.md | 20 ++ tests/test-highlighting.zsh | 29 +- tests/test-perfs.zsh | 33 +- zsh-syntax-highlighting.zsh | 340 ++++++------------ 17 files changed, 540 insertions(+), 328 deletions(-) create mode 100644 highlighters/README.md create mode 100644 highlighters/brackets/README.md rename highlighters/{brackets.zsh => brackets/brackets-highlighter.zsh} (78%) create mode 100644 highlighters/main/README.md create mode 100644 highlighters/main/main-highlighter.zsh rename {tests/data => highlighters/main/test-data}/multiple-redirections.zsh (100%) rename {tests/data => highlighters/main/test-data}/nested-parentheses.zsh (100%) rename {tests/data => highlighters/main/test-data}/simple-command.zsh (100%) rename {tests/data => highlighters/main/test-data}/simple-redirection.zsh (100%) rename {tests/data => highlighters/main/test-data}/unknown-command.zsh (100%) create mode 100644 highlighters/pattern/README.md rename highlighters/{keyword.zsh => pattern/pattern-highlighter.zsh} (75%) create mode 100644 tests/README.md diff --git a/README.md b/README.md index d574418..76cb3a2 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,47 @@ -zsh-syntax-highlighting ![Project status](http://stillmaintained.com/nicoulaj/zsh-syntax-highlighting.png) -========================================================================================================== +zsh-syntax-highlighting +======================= **[Fish shell](http://www.fishshell.com) like syntax highlighting for [Zsh](http://www.zsh.org).** -*Requirements: zsh 4.3.9 or superior.* +*Requirements: zsh 4.3.9+.* -## Try it - -Here is a one-liner to try it without installing or modifying anything: - - wget --no-check-certificate --output-document=/tmp/zsh-syntax-highlighting.zsh https://github.com/nicoulaj/zsh-syntax-highlighting/raw/master/zsh-syntax-highlighting.zsh && . /tmp/zsh-syntax-highlighting.zsh - - -## Install it - +How to install +-------------- ### In your ~/.zshrc * Download the script or clone this repository: - git clone git://github.com/nicoulaj/zsh-syntax-highlighting.git + git clone git://github.com/nicoulaj/zsh-syntax-highlighting.git * Source the script **at the end** of `~/.zshrc`: - source /path/to/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh + source /path/to/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh * Source `~/.zshrc` to take changes into account: - source ~/.zshrc + source ~/.zshrc ### With oh-my-zsh * Download the script or clone this repository in [oh-my-zsh](http://github.com/robbyrussell/oh-my-zsh) plugins directory: - cd ~/.oh-my-zsh/plugins/ - git clone git://github.com/nicoulaj/zsh-syntax-highlighting.git + cd ~/.oh-my-zsh/plugins/ + git clone git://github.com/nicoulaj/zsh-syntax-highlighting.git * Activate the plugin in `~/.zshrc` (in **last** position): - plugins=( [plugins...] zsh-syntax-highlighting) + plugins=( [plugins...] zsh-syntax-highlighting) * Source `~/.zshrc` to take changes into account: - source ~/.zshrc + source ~/.zshrc -## Tweak it +How to tweak +------------ -Optionally, you can override the default styles used for highlighting. The styles are declared in the `ZSH_HIGHLIGHT_STYLES` array. You can override styles this way: - - # To differentiate aliases from other command types - ZSH_HIGHLIGHT_STYLES[alias]='fg=magenta,bold' - - # To have paths colored instead of underlined - ZSH_HIGHLIGHT_STYLES[path]='fg=cyan' - - # To disable highlighting of globbing expressions - ZSH_HIGHLIGHT_STYLES[globbing]='none' - -You can tweak the styles used to colorize matching brackets by overriding the `ZSH_HIGHLIGHT_MATCHING_BRACKETS_STYLES`. - - ZSH_HIGHLIGHT_MATCHING_BRACKETS_STYLES=( - 'fg=blue,bold' # Style for first level of imbrication - 'fg=green,bold' # Style for second level of imbrication - 'fg=magenta,bold' # etc... Put as many styles as you wish, or leave - 'fg=yellow,bold' # empty to disable brackets matching. - 'fg=cyan,bold' - ) - -This must be done **after** the script is sourced, otherwise your styles will be overwritten. The syntax for declaring styles is [documented here](http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#SEC135). +Syntax highlighting is done by pluggable highlighter scripts, see the [highlighters directory](highlighters) +for documentation and configuration settings. diff --git a/highlighters/README.md b/highlighters/README.md new file mode 100644 index 0000000..bd02395 --- /dev/null +++ b/highlighters/README.md @@ -0,0 +1,47 @@ +zsh-syntax-highlighting / highlighters +====================================== + +Syntax highlighting is done by pluggable highlighters: + +* [***main***](main) - the base highlighter, and the only one active by default. +* [***brackets***](brackets) - matches brackets and parenthesis. +* [***pattern***](pattern) - matches user-defined patterns. + + +How to activate highlighters +---------------------------- + +To activate an highlighter, add it to the `ZSH_HIGHLIGHT_HIGHLIGHTERS` array in `~/.zshrc`, for example: + + ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern) + + +How to tweak highlighters +------------------------- + +Highlighters look up styles from the `ZSH_HIGHLIGHT_STYLES` array. Navigate into each highlighter directory to see what styles it defines and how to configure it. + + +How to implement a new highlighter +---------------------------------- + +To create your own ***myhighlighter*** highlighter: + +* Create your script at **highlighters/*myhighlighter*/*myhighlighter*-highlighter.zsh**. +* Implement the `_zsh_highlight_myhighlighter_highlighter_predicate` function. This function must return 0 when the highlighter needs to be called, for example: + + _zsh_highlight_myhighlighter_highlighter_predicate() { + # Call this highlighter in SVN repositories + [[ -d .svn ]] + } + +* Implement the `_zsh_highlight_myhighlighter_highlighter` function. This function does the actual syntax highlighting, by modifying `region_highlight`, for example: + + _zsh_highlight_myhighlighter_highlighter() { + # Colorize the whole buffer with blue background + region_highlight+=(0 $#BUFFER bg=blue) + } + +* Activate your highlighter in `~/.zshrc`: + + ZSH_HIGHLIGHT_HIGHLIGHTERS+=(myhighlighter) diff --git a/highlighters/brackets/README.md b/highlighters/brackets/README.md new file mode 100644 index 0000000..813c9b9 --- /dev/null +++ b/highlighters/brackets/README.md @@ -0,0 +1,28 @@ +zsh-syntax-highlighting / highlighters / brackets +================================================= + +This is the ***brackets*** highlighter, that highlights brackets, parenthesis and matches them. + + +How to activate it +------------------ +To activate it, add it to `ZSH_HIGHLIGHT_HIGHLIGHTERS`: + + ZSH_HIGHLIGHT_HIGHLIGHTERS=( [...] brackets) + + +How to tweak it +--------------- +This highlighter defines the following styles: +* `bracket-error` - unmatched brackets +* `bracket-level-N` - brackets with nest level N + +To override one of those styles, change its entry in `ZSH_HIGHLIGHT_STYLES`, for example in `~/.zshrc`: + + # To define styles for nested brackets up to level 4 + ZSH_HIGHLIGHT_STYLES[bracket-level-1]='fg=blue,bold' + ZSH_HIGHLIGHT_STYLES[bracket-level-2]='fg=red,bold' + ZSH_HIGHLIGHT_STYLES[bracket-level-3]='fg=yellow,bold' + ZSH_HIGHLIGHT_STYLES[bracket-level-4]='fg=magenta,bold' + +The syntax for declaring styles is [documented here](http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#SEC135). diff --git a/highlighters/brackets.zsh b/highlighters/brackets/brackets-highlighter.zsh similarity index 78% rename from highlighters/brackets.zsh rename to highlighters/brackets/brackets-highlighter.zsh index 6af9ba2..63aae99 100644 --- a/highlighters/brackets.zsh +++ b/highlighters/brackets/brackets-highlighter.zsh @@ -29,29 +29,24 @@ # ------------------------------------------------------------------------------------------------- -ZSH_HIGHLIGHT_STYLES+=( - bracket-error 'fg=red,bold' -) +# Define default styles. +: ${ZSH_HIGHLIGHT_STYLES[bracket-error]:=fg=red,bold} +: ${ZSH_HIGHLIGHT_STYLES[bracket-level-1]:=fg=blue,bold} +: ${ZSH_HIGHLIGHT_STYLES[bracket-level-2]:=fg=green,bold} +: ${ZSH_HIGHLIGHT_STYLES[bracket-level-3]:=fg=magenta,bold} +: ${ZSH_HIGHLIGHT_STYLES[bracket-level-4]:=fg=yellow,bold} +: ${ZSH_HIGHLIGHT_STYLES[bracket-level-5]:=fg=cyan,bold} -# Colors for bracket levels. -# Put as many color as you wish. -# Leave it as an empty array to disable. -ZSH_HIGHLIGHT_MATCHING_BRACKETS_STYLES=( - 'fg=blue,bold' - 'fg=green,bold' - 'fg=magenta,bold' - 'fg=yellow,bold' - 'fg=cyan,bold' -) - -# Whether the bracket match highlighting shound be called or not. -_zsh_highlight_bracket-match-p() { - _zsh_highlight_cursor-moved-p || _zsh_highlight_buffer-modified-p +# Whether the brackets highlighter should be called or not. +_zsh_highlight_brackets_highlighter_predicate() +{ + _zsh_highlight_cursor_moved || _zsh_highlight_buffer_modified } -# Bracket match highlighting. -_zsh_highlight_bracket-match() { - bracket_color_size=${#ZSH_HIGHLIGHT_MATCHING_BRACKETS_STYLES} +# Brackets highlighting function. +_zsh_highlight_brackets_highlighter() +{ + bracket_color_size=${#ZSH_HIGHLIGHT_STYLES[(I)bracket-level-*]} if ((bracket_color_size > 0)); then typeset -A levelpos lastoflevel matching revmatching ((level = 0)) @@ -73,7 +68,7 @@ _zsh_highlight_bracket-match() { if ((level < 1)); then region_highlight+=("$((pos - 1)) $pos "$ZSH_HIGHLIGHT_STYLES[bracket-error]) else - region_highlight+=("$((pos - 1)) $pos "$ZSH_HIGHLIGHT_MATCHING_BRACKETS_STYLES[(( (level - 1) % bracket_color_size + 1 ))]) + region_highlight+=("$((pos - 1)) $pos "$ZSH_HIGHLIGHT_STYLES[bracket-level-$(( (level - 1) % bracket_color_size + 1 ))]) fi done ((c = CURSOR + 1)) @@ -85,5 +80,3 @@ _zsh_highlight_bracket-match() { fi fi } - -_zsh_highlight_add-highlighter _zsh_highlight_bracket-match diff --git a/highlighters/main/README.md b/highlighters/main/README.md new file mode 100644 index 0000000..a834572 --- /dev/null +++ b/highlighters/main/README.md @@ -0,0 +1,55 @@ +zsh-syntax-highlighting / highlighters / main +============================================= + +This is the ***main*** highlighter, that highlights: + +* Commands +* Options +* Arguments +* Paths +* Strings + +How to activate it +------------------ +To activate it, add it to `ZSH_HIGHLIGHT_HIGHLIGHTERS`: + + ZSH_HIGHLIGHT_HIGHLIGHTERS=( [...] main) + +This highlighter is active by default. + + +How to tweak it +--------------- +This highlighter defines the following styles: +* `unknown-token` - unknown tokens / errors +* `reserved-word` - shell reserved words +* `alias` - aliases +* `builtin` - shell builtin commands +* `function` - functions +* `command` - commands +* `hashed-command` - hashed commands +* `path` - paths +* `globbing` - globbing expressions +* `history-expansion` - history expansion expressions +* `single-hyphen-option` - single hyphen options +* `double-hyphen-option` - double hyphen options +* `back-quoted-argument` - backquoted expressions +* `single-quoted-argument` - single quoted arguments +* `double-quoted-argument` - double quoted arguments +* `dollar-double-quoted-argument` - dollar double quoted arguments +* `back-double-quoted-argument` - back double quoted arguments +* `assign` - variable assignments +* `default` - parts of the buffer that do not match anything + +To override one of those styles, change its entry in `ZSH_HIGHLIGHT_STYLES`, for example in `~/.zshrc`: + + # To differentiate aliases from other command types + ZSH_HIGHLIGHT_STYLES[alias]='fg=magenta,bold' + + # To have paths colored instead of underlined + ZSH_HIGHLIGHT_STYLES[path]='fg=cyan' + + # To disable highlighting of globbing expressions + ZSH_HIGHLIGHT_STYLES[globbing]='none' + +The syntax for declaring styles is [documented here](http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#SEC135). diff --git a/highlighters/main/main-highlighter.zsh b/highlighters/main/main-highlighter.zsh new file mode 100644 index 0000000..3996f87 --- /dev/null +++ b/highlighters/main/main-highlighter.zsh @@ -0,0 +1,164 @@ +#!/usr/bin/env zsh +# ------------------------------------------------------------------------------------------------- +# Copyright (c) 2010-2011 zsh-syntax-highlighting contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this list of conditions +# and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials provided +# with the distribution. +# * Neither the name of the zsh-syntax-highlighting contributors nor the names of its contributors +# may be used to endorse or promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ------------------------------------------------------------------------------------------------- +# -*- mode: zsh; sh-indentation: 2; indent-tabs-mode: nil; sh-basic-offset: 2; -*- +# vim: ft=zsh sw=2 ts=2 et +# ------------------------------------------------------------------------------------------------- + + +# Define default styles. +: ${ZSH_HIGHLIGHT_STYLES[default]:=none} +: ${ZSH_HIGHLIGHT_STYLES[unknown-token]:=fg=red,bold} +: ${ZSH_HIGHLIGHT_STYLES[reserved-word]:=fg=yellow} +: ${ZSH_HIGHLIGHT_STYLES[alias]:=fg=green} +: ${ZSH_HIGHLIGHT_STYLES[builtin]:=fg=green} +: ${ZSH_HIGHLIGHT_STYLES[function]:=fg=green} +: ${ZSH_HIGHLIGHT_STYLES[command]:=fg=green} +: ${ZSH_HIGHLIGHT_STYLES[hashed-command]:=fg=green} +: ${ZSH_HIGHLIGHT_STYLES[path]:=underline} +: ${ZSH_HIGHLIGHT_STYLES[globbing]:=fg=blue} +: ${ZSH_HIGHLIGHT_STYLES[history-expansion]:=fg=blue} +: ${ZSH_HIGHLIGHT_STYLES[single-hyphen-option]:=none} +: ${ZSH_HIGHLIGHT_STYLES[double-hyphen-option]:=none} +: ${ZSH_HIGHLIGHT_STYLES[back-quoted-argument]:=none} +: ${ZSH_HIGHLIGHT_STYLES[single-quoted-argument]:=fg=yellow} +: ${ZSH_HIGHLIGHT_STYLES[double-quoted-argument]:=fg=yellow} +: ${ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument]:=fg=cyan} +: ${ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument]:=fg=cyan} +: ${ZSH_HIGHLIGHT_STYLES[assign]:=none} + +# Tokens that are always immediately followed by a command. +ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS=( + '|' '||' ';' '&' '&&' 'noglob' 'nocorrect' 'builtin' +) + +# Whether the highlighter should be called or not. +_zsh_highlight_main_highlighter_predicate() +{ + _zsh_highlight_buffer_modified +} + +# Main syntax highlighting function. +_zsh_highlight_main_highlighter() +{ + setopt localoptions extendedglob bareglobqual + local start_pos=0 end_pos highlight_glob=true new_expression=true arg style + region_highlight=() + for arg in ${(z)BUFFER}; do + local substr_color=0 + [[ $start_pos -eq 0 && $arg = 'noglob' ]] && highlight_glob=false + ((start_pos+=${#BUFFER[$start_pos+1,-1]}-${#${BUFFER[$start_pos+1,-1]##[[:space:]]#}})) + ((end_pos=$start_pos+${#arg})) + if $new_expression; then + new_expression=false + res=$(LC_ALL=C builtin type -w $arg 2>/dev/null) + case $res in + *': reserved') style=$ZSH_HIGHLIGHT_STYLES[reserved-word];; + *': alias') style=$ZSH_HIGHLIGHT_STYLES[alias] + local aliased_command="${"$(alias $arg)"#*=}" + [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$aliased_command"} && -z ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$arg"} ]] && ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS+=($arg) + ;; + *': builtin') style=$ZSH_HIGHLIGHT_STYLES[builtin];; + *': function') style=$ZSH_HIGHLIGHT_STYLES[function];; + *': command') style=$ZSH_HIGHLIGHT_STYLES[command];; + *': hashed') style=$ZSH_HIGHLIGHT_STYLES[hashed-command];; + *) if _zsh_highlight_main_highlighter_check_assign; then + style=$ZSH_HIGHLIGHT_STYLES[assign] + new_expression=true + elif _zsh_highlight_main_highlighter_check_path; then + style=$ZSH_HIGHLIGHT_STYLES[path] + elif [[ $arg[0,1] = $histchars[0,1] ]]; then + style=$ZSH_HIGHLIGHT_STYLES[history-expansion] + else + style=$ZSH_HIGHLIGHT_STYLES[unknown-token] + fi + ;; + esac + else + case $arg in + '--'*) style=$ZSH_HIGHLIGHT_STYLES[double-hyphen-option];; + '-'*) style=$ZSH_HIGHLIGHT_STYLES[single-hyphen-option];; + "'"*"'") style=$ZSH_HIGHLIGHT_STYLES[single-quoted-argument];; + '"'*'"') style=$ZSH_HIGHLIGHT_STYLES[double-quoted-argument] + region_highlight+=("$start_pos $end_pos $style") + _zsh_highlight_main_highlighter_highlight_string + substr_color=1 + ;; + '`'*'`') style=$ZSH_HIGHLIGHT_STYLES[back-quoted-argument];; + *"*"*) $highlight_glob && style=$ZSH_HIGHLIGHT_STYLES[globbing] || style=$ZSH_HIGHLIGHT_STYLES[default];; + *) if _zsh_highlight_main_highlighter_check_path; then + style=$ZSH_HIGHLIGHT_STYLES[path] + elif [[ $arg[0,1] = $histchars[0,1] ]]; then + style=$ZSH_HIGHLIGHT_STYLES[history-expansion] + else + style=$ZSH_HIGHLIGHT_STYLES[default] + fi + ;; + esac + fi + [[ $substr_color = 0 ]] && region_highlight+=("$start_pos $end_pos $style") + [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$arg"} ]] && new_expression=true + start_pos=$end_pos + done +} + +# Check if the argument is variable assignment +_zsh_highlight_main_highlighter_check_assign() +{ + setopt localoptions extended_glob + [[ ${(Q)arg} == [[:alpha:]_]([[:alnum:]_])#=* ]] +} + +# Check if the argument is a path. +_zsh_highlight_main_highlighter_check_path() +{ + [[ -z ${(Q)arg} ]] && return 1 + [[ -e ${(Q)arg} ]] && return 0 + [[ ! -e ${(Q)arg:h} ]] && return 1 + [[ ${#BUFFER} == $end_pos && -n $(print ${(Q)arg}*(N)) ]] && return 0 + return 1 +} + +# Highlight special chars inside double-quoted strings +_zsh_highlight_main_highlighter_highlight_string() +{ + setopt localoptions noksharrays + local i j k style + # Starting quote is at 1, so start parsing at offset 2 in the string. + for (( i = 2 ; i < end_pos - start_pos ; i += 1 )) ; do + (( j = i + start_pos - 1 )) + (( k = j + 1 )) + case "$arg[$i]" in + '$') style=$ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument];; + "\\") style=$ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument] + (( k += 1 )) # Color following char too. + (( i += 1 )) # Skip parsing the escaped char. + ;; + *) continue;; + esac + region_highlight+=("$j $k $style") + done +} diff --git a/tests/data/multiple-redirections.zsh b/highlighters/main/test-data/multiple-redirections.zsh similarity index 100% rename from tests/data/multiple-redirections.zsh rename to highlighters/main/test-data/multiple-redirections.zsh diff --git a/tests/data/nested-parentheses.zsh b/highlighters/main/test-data/nested-parentheses.zsh similarity index 100% rename from tests/data/nested-parentheses.zsh rename to highlighters/main/test-data/nested-parentheses.zsh diff --git a/tests/data/simple-command.zsh b/highlighters/main/test-data/simple-command.zsh similarity index 100% rename from tests/data/simple-command.zsh rename to highlighters/main/test-data/simple-command.zsh diff --git a/tests/data/simple-redirection.zsh b/highlighters/main/test-data/simple-redirection.zsh similarity index 100% rename from tests/data/simple-redirection.zsh rename to highlighters/main/test-data/simple-redirection.zsh diff --git a/tests/data/unknown-command.zsh b/highlighters/main/test-data/unknown-command.zsh similarity index 100% rename from tests/data/unknown-command.zsh rename to highlighters/main/test-data/unknown-command.zsh diff --git a/highlighters/pattern/README.md b/highlighters/pattern/README.md new file mode 100644 index 0000000..82a7c65 --- /dev/null +++ b/highlighters/pattern/README.md @@ -0,0 +1,21 @@ +zsh-syntax-highlighting / highlighters / pattern +================================================ + +This is the ***pattern*** highlighter, that highlights user defined patterns. + + +How to activate it +------------------ +To activate it, add it to `ZSH_HIGHLIGHT_HIGHLIGHTERS`: + + ZSH_HIGHLIGHT_HIGHLIGHTERS=( [...] pattern) + + +How to tweak it +--------------- +To use this highlighter, associate patterns with styles in the `ZSH_HIGHLIGHT_PATTERNS` array, for example in `~/.zshrc`: + + # To have commands starting with `rm -rf` in red: + ZSH_HIGHLIGHT_PATTERNS+=('rm -rf *' 'fg=white,bold,bg=red') + +The syntax for declaring styles is [documented here](http://zsh.sourceforge.net/Doc/Release/Zsh-Line-Editor.html#SEC135). diff --git a/highlighters/keyword.zsh b/highlighters/pattern/pattern-highlighter.zsh similarity index 75% rename from highlighters/keyword.zsh rename to highlighters/pattern/pattern-highlighter.zsh index 7302d15..93041a3 100644 --- a/highlighters/keyword.zsh +++ b/highlighters/pattern/pattern-highlighter.zsh @@ -29,35 +29,31 @@ # ------------------------------------------------------------------------------------------------- -# A simple keyword highlighting extension for zsh-syntax-highlighting. - -# To use this, please do the following steps. -# 1) Source this file after zsh-syntax-highlighting.zsh -# % source zsh-syntax-highlighting.zsh -# % source contrib/keyword.zsh -# 2) Add keyword and color pairs to `ZSH_HIGHLIGHT_KEYWORD_KEYWORDS`. -# % ZSH_HIGHLIGHT_KEYWORD_KEYWORDS+=('rm -rf *' 'fg=white,bold,bg=red') -# 3) Please see the effect. -# % ;# rm -rf /tmp/doesnotexist - # List of keyword and color pairs. -typeset -gA ZSH_HIGHLIGHT_KEYWORD_KEYWORDS +typeset -gA ZSH_HIGHLIGHT_PATTERNS -_zsh_highlight-keyword() { +# Whether the pattern highlighter should be called or not. +_zsh_highlight_pattern_highlighter_predicate() +{ + _zsh_highlight_buffer_modified +} + +# Pattern syntax highlighting function. +_zsh_highlight_pattern_highlighter() +{ setopt localoptions extendedglob - for pattern in ${(k)ZSH_HIGHLIGHT_KEYWORD_KEYWORDS}; do - _zsh_highlight-keyword-loop "$BUFFER" "$pattern" + for pattern in ${(k)ZSH_HIGHLIGHT_PATTERNS}; do + _zsh_highlight_pattern_highlighter_loop "$BUFFER" "$pattern" done } -_zsh_highlight-keyword-loop() { +_zsh_highlight_pattern_highlighter_loop() +{ # This does *not* do its job syntactically, sorry. local buf="$1" pat="$2" local -a match mbegin mend if [[ "$buf" == (#b)(*)(${~pat})* ]]; then - region_highlight+=("$((mbegin[2] - 1)) $mend[2] $ZSH_HIGHLIGHT_KEYWORD_KEYWORDS[$pat]") + region_highlight+=("$((mbegin[2] - 1)) $mend[2] $ZSH_HIGHLIGHT_PATTERNS[$pat]") "$0" "$match[1]" "$pat"; return $? fi } - -_zsh_highlight_add-highlighter _zsh_highlight-keyword _zsh_highlight_buffer-modified-p diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..ed3cd85 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,20 @@ +zsh-syntax-highlighting / tests +=============================== + +Utility scripts for testing zsh-syntax-highlighting highlighters. + +The tests expect the highlighter directory to contain a `test-data` directory with test data files. See the [main highlighter](../highlighters/main/test-data) for examples. + + +highlighting test +----------------- +[`test-highlighting.zsh`](test-highlighting.zsh) tests the correctness of the highlighting. Usage: + + zsh test-highlighting.zsh + + +performance test +---------------- +[`test-perfs.zsh`](test-highlighting.zsh) measures the time spent doing the highlighting. Usage: + + zsh test-perfs.zsh diff --git a/tests/test-highlighting.zsh b/tests/test-highlighting.zsh index 7166eae..2e72b2b 100755 --- a/tests/test-highlighting.zsh +++ b/tests/test-highlighting.zsh @@ -29,14 +29,35 @@ # ------------------------------------------------------------------------------------------------- +# Check an highlighter was given as argument. +[[ -n "$1" ]] || { + echo "You must provide the name of a valid highlighter as argument." >&2 + exit 1 +} + +# Check the highlighter is valid. +[[ -f ${0:h:h}/highlighters/$1/$1-highlighter.zsh ]] || { + echo "Could not find highlighter '$1'." >&2 + exit 1 +} + +# Check the highlighter has test data. +[[ -d ${0:h:h}/highlighters/$1/test-data ]] || { + echo "Highlighter '$1' has no test data." >&2 + exit 1 +} + local -a errors highlight_zone local -A observed_result # Load the main script. -. $(dirname $0)/../zsh-syntax-highlighting.zsh +. ${0:h:h}/zsh-syntax-highlighting.zsh -# Process each test data file in data/. -for data_file in $(dirname $0)/data/*.zsh; do +# Activate the highlighter. +ZSH_HIGHLIGHT_HIGHLIGHTERS=($1) + +# Process each test data file in test data directory. +for data_file in ${0:h:h}/highlighters/$1/test-data/*; do # Load the data and prepare checking it. BUFFER= ; expected_region_highlight=(); errors=() @@ -55,7 +76,7 @@ for data_file in $(dirname $0)/data/*.zsh; do # Process the data. region_highlight=() - _zsh_highlight-zle-buffer + _zsh_highlight # Overlapping regions can be declared in region_highlight, so we first build an array of the # observed highlighting. diff --git a/tests/test-perfs.zsh b/tests/test-perfs.zsh index 570bce6..0d4adc3 100755 --- a/tests/test-perfs.zsh +++ b/tests/test-perfs.zsh @@ -29,11 +29,32 @@ # ------------------------------------------------------------------------------------------------- -# Load the main script. -. $(dirname $0)/../zsh-syntax-highlighting.zsh +# Check an highlighter was given as argument. +[[ -n "$1" ]] || { + echo "You must provide the name of a valid highlighter as argument." >&2 + exit 1 +} -# Process each test data file in data/. -for data_file in $(dirname $0)/data/*.zsh; do +# Check the highlighter is valid. +[[ -f ${0:h:h}/highlighters/$1/$1-highlighter.zsh ]] || { + echo "Could not find highlighter '$1'." >&2 + exit 1 +} + +# Check the highlighter has test data. +[[ -d ${0:h:h}/highlighters/$1/test-data ]] || { + echo "Highlighter '$1' has no test data." >&2 + exit 1 +} + +# Load the main script. +. ${0:h:h}/zsh-syntax-highlighting.zsh + +# Activate the highlighter. +ZSH_HIGHLIGHT_HIGHLIGHTERS=($1) + +# Process each test data file in test data directory. +for data_file in ${0:h:h}/highlighters/$1/test-data/*; do # Load the data and prepare checking it. BUFFER= @@ -45,9 +66,9 @@ for data_file in $(dirname $0)/data/*.zsh; do echo "KO\n - 'BUFFER' is not declared or blank." else - # Measure the time taken by _zsh_highlight-zle-buffer. + # Measure the time taken by _zsh_highlight. TIMEFMT="%*Es" - time ( BUFFER="$BUFFER" && _zsh_highlight-zle-buffer) + time ( BUFFER="$BUFFER" && _zsh_highlight) fi diff --git a/zsh-syntax-highlighting.zsh b/zsh-syntax-highlighting.zsh index 87ae5d8..a180e44 100644 --- a/zsh-syntax-highlighting.zsh +++ b/zsh-syntax-highlighting.zsh @@ -33,255 +33,81 @@ # Core highlighting update system # ------------------------------------------------------------------------------------------------- -# Array used by highlighters to declare overridable styles. -typeset -gA ZSH_HIGHLIGHT_STYLES +# Array declaring active highlighters names. +typeset -ga ZSH_HIGHLIGHT_HIGHLIGHTERS -# An `object' implemented by below 3 arrays' elements could be called a -# `highlighter', registered by `_zsh_highlight_add-highlighter`. In other words, these -# arrays are indexed and tied by their own functionality. If they have been -# arranged inconsistently, things goes wrong. -# Please see `_zsh_highlight-zle-buffer` and `_zsh_highlight_add-highlighter`. - -# Actual recolorize functions to be called. -typeset -a zsh_highlight_functions; zsh_highlight_functions=() - -# Predicate functions whether its recolorize function should be called or not. -typeset -a zsh_highlight_predicates; zsh_highlight_predicates=() - -# Highlight storages for each recolorize functions. -typeset -a zsh_highlight_caches; zsh_highlight_caches=() - -_zsh_highlight-zle-buffer() { +# Update ZLE buffer syntax highlighting. +# +# Invokes each highlighter that needs updating. +# This function is supposed to be called whenever the ZLE state changes. +_zsh_highlight() +{ + # Store the previous command return code to restore it whatever happens. local ret=$? + + # Do not highlight if there are pending inputs (copy/paste). + [[ $PENDING -gt 0 ]] && return $ret + { - local -a funinds - local -i rh_size=$#region_highlight - for i in {1..${#zsh_highlight_functions}}; do - local pred=${zsh_highlight_predicates[i]} cache_place=${zsh_highlight_caches[i]} - if _zsh_highlight-zle-buffer-p "$rh_size" "$pred"; then - if ((${#${(P)cache_place}} > 0)); then - region_highlight=(${region_highlight:#(${(P~j.|.)cache_place})}) - local -a empty; empty=(); : ${(PA)cache_place::=$empty} - fi - funinds+=$i + local -a selected_highlighters + local cache_place + + # Select which highlighters in ZSH_HIGHLIGHT_HIGHLIGHTERS need to be invoked. + local highlighter; for highlighter in $ZSH_HIGHLIGHT_HIGHLIGHTERS; do + + # If highlighter needs to be invoked + if "_zsh_highlight_${highlighter}_highlighter_predicate"; then + + # Mark the highlighter as selected for update. + selected_highlighters+=($highlighter) + + # Remove what was stored in its cache from region_highlight. + cache_place="_zsh_highlight_${highlighter}_highlighter_cache" + [[ ${#${(P)cache_place}} -gt 0 ]] && region_highlight=(${region_highlight:#(${(P~j.|.)cache_place})}) fi done - for i in $funinds; do - local func=${zsh_highlight_functions[i]} cache_place=${zsh_highlight_caches[i]} - local -a rh; rh=($region_highlight) + + # Invoke each selected highlighter and store the result in its cache. + local -a region_highlight_copy + for highlighter in $selected_highlighters; do + cache_place="_zsh_highlight_${highlighter}_highlighter_cache" + region_highlight_copy=($region_highlight) { - "$func" + "_zsh_highlight_${highlighter}_highlighter" } always { - : ${(PA)cache_place::=${region_highlight:#(${(~j.|.)rh})}} + : ${(PA)cache_place::=${region_highlight:#(${(~j.|.)region_highlight_copy})}} } done + } always { - ZSH_PRIOR_CURSOR=$CURSOR - ZSH_PRIOR_HIGHLIGHTED_BUFFER=$BUFFER + _ZSH_HIGHLIGHT_PRIOR_BUFFER=$BUFFER + _ZSH_HIGHLIGHT_PRIOR_CURSOR=$CURSOR return $ret } } -# Whether supplied highlight_predicate satisfies or not. -_zsh_highlight-zle-buffer-p() { - local region_highlight_size="$1" highlight_predicate="$2" - # If any highlightings are not taken into account, asume it is needed. - # This holds for some up/down-history commands, for example. - ((region_highlight_size == 0)) || "$highlight_predicate" -} - -# Whether the command line buffer is modified or not. -_zsh_highlight_buffer-modified-p() { - [[ ${ZSH_PRIOR_HIGHLIGHTED_BUFFER:-} != $BUFFER ]] -} - -# Whether the cursor is moved or not. -_zsh_highlight_cursor-moved-p() { - ((ZSH_PRIOR_CURSOR != $CURSOR)) -} - -# Register an highlighter. -_zsh_highlight_add-highlighter() { - zsh_highlight_functions+="$1" - zsh_highlight_predicates+="${2-${1}-p}" - zsh_highlight_caches+="${3-${1//-/_}}" -} - # ------------------------------------------------------------------------------------------------- -# Main highlighter +# API/utility functions for highlighters # ------------------------------------------------------------------------------------------------- -ZSH_HIGHLIGHT_STYLES+=( - default 'none' - unknown-token 'fg=red,bold' - reserved-word 'fg=yellow' - alias 'fg=green' - builtin 'fg=green' - function 'fg=green' - command 'fg=green' - hashed-command 'fg=green' - path 'underline' - globbing 'fg=blue' - history-expansion 'fg=blue' - single-hyphen-option 'none' - double-hyphen-option 'none' - back-quoted-argument 'none' - single-quoted-argument 'fg=yellow' - double-quoted-argument 'fg=yellow' - dollar-double-quoted-argument 'fg=cyan' - back-double-quoted-argument 'fg=cyan' - assign 'none' -) +# Array used by highlighters to declare user overridable styles. +typeset -gA ZSH_HIGHLIGHT_STYLES -# Tokens that are always immediately followed by a command. -ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS=( - '|' '||' ';' '&' '&&' 'noglob' 'nocorrect' 'builtin' -) - -# Check if the argument is variable assignment -_zsh_highlight_check-assign() { - setopt localoptions extended_glob - [[ ${(Q)arg} == [[:alpha:]_]([[:alnum:]_])#=* ]] +# Whether the command line buffer has been modified or not. +# +# Returns 0 if the buffer has changed since _zsh_highlight was last called. +_zsh_highlight_buffer_modified() +{ + [[ ${_ZSH_HIGHLIGHT_PRIOR_BUFFER:-} != $BUFFER ]] } -# Check if the argument is a path. -_zsh_highlight_check-path() { - [[ -z ${(Q)arg} ]] && return 1 - [[ -e ${(Q)arg} ]] && return 0 - [[ ! -e ${(Q)arg:h} ]] && return 1 - [[ ${#BUFFER} == $end_pos && -n $(print ${(Q)arg}*(N)) ]] && return 0 - return 1 -} - -# Highlight special chars inside double-quoted strings -_zsh_highlight_highlight_string() { - setopt localoptions noksharrays - local i j k style - # Starting quote is at 1, so start parsing at offset 2 in the string. - for (( i = 2 ; i < end_pos - start_pos ; i += 1 )) ; do - (( j = i + start_pos - 1 )) - (( k = j + 1 )) - case "$arg[$i]" in - '$') style=$ZSH_HIGHLIGHT_STYLES[dollar-double-quoted-argument];; - "\\") style=$ZSH_HIGHLIGHT_STYLES[back-double-quoted-argument] - (( k += 1 )) # Color following char too. - (( i += 1 )) # Skip parsing the escaped char. - ;; - *) continue;; - esac - region_highlight+=("$j $k $style") - done -} - -# Core syntax highlighting. -_zsh_main-highlight() { - setopt localoptions extendedglob bareglobqual - local start_pos=0 end_pos highlight_glob=true new_expression=true arg style - region_highlight=() - for arg in ${(z)BUFFER}; do - local substr_color=0 - [[ $start_pos -eq 0 && $arg = 'noglob' ]] && highlight_glob=false - ((start_pos+=${#BUFFER[$start_pos+1,-1]}-${#${BUFFER[$start_pos+1,-1]##[[:space:]]#}})) - ((end_pos=$start_pos+${#arg})) - if $new_expression; then - new_expression=false - res=$(LC_ALL=C builtin type -w $arg 2>/dev/null) - case $res in - *': reserved') style=$ZSH_HIGHLIGHT_STYLES[reserved-word];; - *': alias') style=$ZSH_HIGHLIGHT_STYLES[alias] - local aliased_command="${"$(alias $arg)"#*=}" - [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$aliased_command"} && -z ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$arg"} ]] && ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS+=($arg) - ;; - *': builtin') style=$ZSH_HIGHLIGHT_STYLES[builtin];; - *': function') style=$ZSH_HIGHLIGHT_STYLES[function];; - *': command') style=$ZSH_HIGHLIGHT_STYLES[command];; - *': hashed') style=$ZSH_HIGHLIGHT_STYLES[hashed-command];; - *) if _zsh_highlight_check-assign; then - style=$ZSH_HIGHLIGHT_STYLES[assign] - new_expression=true - elif _zsh_highlight_check-path; then - style=$ZSH_HIGHLIGHT_STYLES[path] - elif [[ $arg[0,1] = $histchars[0,1] ]]; then - style=$ZSH_HIGHLIGHT_STYLES[history-expansion] - else - style=$ZSH_HIGHLIGHT_STYLES[unknown-token] - fi - ;; - esac - else - case $arg in - '--'*) style=$ZSH_HIGHLIGHT_STYLES[double-hyphen-option];; - '-'*) style=$ZSH_HIGHLIGHT_STYLES[single-hyphen-option];; - "'"*"'") style=$ZSH_HIGHLIGHT_STYLES[single-quoted-argument];; - '"'*'"') style=$ZSH_HIGHLIGHT_STYLES[double-quoted-argument] - region_highlight+=("$start_pos $end_pos $style") - _zsh_highlight_highlight_string - substr_color=1 - ;; - '`'*'`') style=$ZSH_HIGHLIGHT_STYLES[back-quoted-argument];; - *"*"*) $highlight_glob && style=$ZSH_HIGHLIGHT_STYLES[globbing] || style=$ZSH_HIGHLIGHT_STYLES[default];; - *) if _zsh_highlight_check-path; then - style=$ZSH_HIGHLIGHT_STYLES[path] - elif [[ $arg[0,1] = $histchars[0,1] ]]; then - style=$ZSH_HIGHLIGHT_STYLES[history-expansion] - else - style=$ZSH_HIGHLIGHT_STYLES[default] - fi - ;; - esac - fi - [[ $substr_color = 0 ]] && region_highlight+=("$start_pos $end_pos $style") - [[ -n ${(M)ZSH_HIGHLIGHT_TOKENS_FOLLOWED_BY_COMMANDS:#"$arg"} ]] && new_expression=true - start_pos=$end_pos - done -} - - -# ------------------------------------------------------------------------------------------------- -# Setup functions -# ------------------------------------------------------------------------------------------------- - -# Intercept specified ZLE events to have highlighting triggered. -_zsh_highlight_bind-events() { - - # Resolve event names what have to be bound to. - zmodload zsh/zleparameter 2>/dev/null || { - echo 'zsh-syntax-highlighting:zmodload error. exiting.' >&2 - return -1 - } - local -a events; : ${(A)events::=${@:#(_*|orig-*|.run-help|.which-command)}} - - # Bind the events to _zsh_highlight-zle-buffer. - local clean_event - for event in $events; do - if [[ "$widgets[$event]" == completion:* ]]; then - eval "zle -C orig-$event ${${${widgets[$event]}#*:}/:/ } ; $event() { builtin zle orig-$event && _zsh_highlight-zle-buffer } ; zle -N $event" - else - case $event in - accept-and-menu-complete) - eval "$event() { builtin zle .$event && _zsh_highlight-zle-buffer } ; zle -N $event" - ;; - .*) - clean_event=$event[2,${#event}] # Remove the leading dot in the event name - case ${widgets[$clean_event]-} in - (completion|user):*) - ;; - *) - eval "$clean_event() { builtin zle $event && _zsh_highlight-zle-buffer } ; zle -N $clean_event" - ;; - esac - ;; - *) - ;; - esac - fi - done -} - -# Load highlighters from specified directory if it exists. -_zsh_highlight_load-highlighters() { - [[ -d $1 ]] && for highlighter_def ($1/*.zsh) . $highlighter_def +# Whether the cursor has moved or not. +# +# Returns 0 if the cursor has moved since _zsh_highlight was last called. +_zsh_highlight_cursor_moved() +{ + ((_ZSH_HIGHLIGHT_PRIOR_CURSOR != $CURSOR)) } @@ -289,11 +115,57 @@ _zsh_highlight_load-highlighters() { # Setup # ------------------------------------------------------------------------------------------------- -# Bind highlighting to all known events. -_zsh_highlight_bind-events "${(@f)"$(zle -la)"}" +# Load ZSH module zsh/zleparameter, needed to override user defined widgets. +zmodload zsh/zleparameter 2>/dev/null || { + echo 'zsh-syntax-highlighting: failed loading zsh/zleparameter, exiting.' >&2 + return -1 +} -# Register the main highlighter. -_zsh_highlight_add-highlighter _zsh_main-highlight _zsh_highlight_buffer-modified-p +# Resolve highlighters directory location. +highlighters_dir="${ZSH_HIGHLIGHT_HIGHLIGHTERS_DIR:-${${(%):-%N}:A:h}/highlighters}" +[[ -d $highlighters_dir ]] || { + echo "zsh-syntax-highlighting: highlighters directory '$highlighters_dir' not found, exiting." >&2 + return -1 +} -# Load additional highlighters if available. -_zsh_highlight_load-highlighters "${${(%):-%N}:h}/highlighters" +# Override ZLE widgets to make them invoke _zsh_highlight. +for event in ${${(f)"$(zle -la)"}:#(_*|orig-*|.run-help|.which-command)}; do + if [[ "$widgets[$event]" == completion:* ]]; then + eval "zle -C orig-$event ${${${widgets[$event]}#*:}/:/ } ; $event() { builtin zle orig-$event && _zsh_highlight } ; zle -N $event" + else + case $event in + accept-and-menu-complete) + eval "$event() { builtin zle .$event && _zsh_highlight } ; zle -N $event" + ;; + .*) + clean_event=$event[2,${#event}] # Remove the leading dot in the event name + case ${widgets[$clean_event]-} in + (completion|user):*) + ;; + *) + eval "$clean_event() { builtin zle $event && _zsh_highlight } ; zle -N $clean_event" + ;; + esac + ;; + *) + ;; + esac + fi +done +unset event clean_event + +# Load highlighters from highlighters directory and check they define required functions. +for highlighter_dir ($highlighters_dir/*/); do + highlighter="${highlighter_dir:t}" + [[ -f "$highlighter_dir/${highlighter}-highlighter.zsh" ]] && { + . "$highlighter_dir/${highlighter}-highlighter.zsh" + type "_zsh_highlight_${highlighter}_highlighter" &> /dev/null && + type "_zsh_highlight_${highlighter}_highlighter_predicate" &> /dev/null || { + echo "zsh-syntax-highlighting: '${highlighter}' highlighter should define both required functions '_zsh_highlight_${highlighter}_highlighter' and '_zsh_highlight_${highlighter}_highlighter_predicate' in '${highlighter_dir}/${highlighter}-highlighter.zsh'." >&2 + } + } +done +unset highlighter highlighter_dir highlighters_dir + +# Initialize the array of active highlighters if needed. +[[ $#ZSH_HIGHLIGHT_HIGHLIGHTERS -eq 0 ]] && ZSH_HIGHLIGHT_HIGHLIGHTERS=(main)