Can I cascade vim syntax highlighting rules like in nano?

77 Views Asked by At

I have a custom file type, which contains lots of fields like

FieldName: FieldValue

I'm trying to create syntax highlighting for this filetype, in nano and vim, such that "Fieldname" appears in blue, and anything from the colon onwards, including the colon, appears in yellow. In nano I achieve this easily due to its 'cascading' nature:

color yellow     start="^FieldName: " end="$"   # Make the whole thing yellow
color brightblue       "^FieldName"             # Then change the first part to blue

In vim, it doesn't look like rules cascade, but I can achieve the above using 'matchgroup', which allows treating 'boundary' matches differently to 'content':

syn region Yellow matchgroup=Blue start="^FieldName" end="$"                                                                                                                                          

So far so good. The problem arises in the special case that some FieldValues need to be coloured differently, because they are special. In nano, I can simply add another rule onto the cascade:

color yellow         start="^FieldNAme: " end="$"     # Make the whole thing yellow
color bold,lightyellow,red "^FieldName: SpecialValue" # Then change up to the special value to red
color yellow               "^FieldName: "             # Then up to the colon to yellow again
color brightblue           "^FieldName"               # Then up to the fieldname to blue

However, when I try to do the same in vim, I am stuck. I have no idea how to match groups at multiple levels. Is there a way to enable such cascading behaviour in vim? Or, alternatively, can someone point me in the right direction to achieve the above effect?

2

There are 2 best solutions below

1
Matt On BEST ANSWER

In Vim if inside another syntax match/region then only some of submatches tried. These matches must appear in contains=xxx,yyy,zzz attribute of an upper level match (or, alternatively, they can have an attribute containedin=aaa, where aaa is an upper-level match). Also the sub-matches should usually have contained attribute that forbids matching them on the global level (i.e. uncontained).

As a final note, if a collision arises (i.e. two or more submatches can match at the same position) then the last one wins. So the order matters.

Upd. This example is pretty straightforward and doesn't need many rules. Yet exact regex definitions may vary, so giving it rather for illustrative purposes.

Var.1

syn match MyFileFieldWhole /^\w\+: .*/ contains=MyFileFieldName,MyFileFieldSpecial
syn match MyFileFieldName /^\w\+/ contained
syn keyword MyFileFieldSpecial SpecialValue contained

hi def MyFileFieldWhole guifg=yellow
hi def MyFileFieldName guifg=lightblue
hi def MyFileFieldSpecial guifg=red

Var.2

syn match MyFileFieldName /^\w\+: /me=e-2 nextgroup=MyFileFieldValue
syn match MyFileFieldValue /: .*/ contains=MyFileFieldSpecial contained
syn keyword MyFileFieldSpecial SpecialValue contained

hi def MyFileFieldName guifg=lightblue
hi def MyFileFieldValue guifg=yellow
hi def MyFileFieldSpecial guifg=red
0
Tasos Papastylianou On

This is how I solved the above problem, (using the same pseudofields as above, and showing only the relevant rules here). Hopefully this might prove useful to others with the same challenge.

" Vim syntax file
" Language   : myfiletype
" Filenames  : *.myfileextension
" Maintainer : Me
" Last Change: 2022-06-19

" Quit when a (custom) syntax file was already loaded
if exists("b:current_syntax")
  finish
endif

" SpecialValue is special, put it in red box. Fieldname in blue.
syn match Group1 "^FieldName: SpecialValue$"    contains=Group2
syn match Group2 "^FieldName: \zeSpecialValue$" contains=Group3 contained
syn match Group3 "^FieldName\ze: SpecialValue$" contained

" all other values in plain yellow, with fieldname in blue.
syn match Group2 "^FieldName: \(SpecialValue$\)\@!.*$"    contains=Group3
syn match Group3 "^FieldName\ze: \(SpecialValue$\)\@!.*$" contained

" Group-code color definitions ==
hi def Group3             ctermfg=blue
hi def Group2             ctermfg=darkyellow
hi def Group1 cterm=bold  ctermfg=yellow  ctermbg=darkred

let b:current_syntax = "myfiletype"

Some explanatory notes:

  • I used syn-match instead of syn-region in the end, as the former guaranteed single-line matching.

  • The \ze bit acts as a 'cut'; the regexp first matches fully, then gets 'cut' up until the \ze part, which is then used for the highlighting.

  • The \@! causes the regexp to succeed when the previous atom (in this case the bit in parentheses) is NOT present.

  • The various 'containment' levels, where the 'outer match' is the same, but at each level the 'inner match' caused by \ze is shorter and shorter, manages to emulate the 'cascading' effect I was after. However, an added case explicitly excluding the specialvalue had to be added, to account for 'default' cases where the special value is not present (By contrast, in nano, no explicit exclusion was necessary, as long as this 'default' rule was placed at the top of the 'cascade').

  • The above example demonstrates rules accounting for a single specialvalue. If you have multiple specialvalues to exclude (which was the case in my actual problem), you can convert \(SpecialValue$\)\@! to \(SpecialValue1\|SpecialValue2\)\@!, etc, to account for the explicit exclusion of all special values in the 'default' rule.