3

I am trying to come up with a Vim syntax for python function call, e.g.:

foo(notarg==0, arg2, kwarg='a', kwarg2='b')
dict(a=1, b=2, c=3)

where we want to highlight

  • (i) function name foo, dict
  • (ii) keyword parameters, e.g. kwarg, kwarg2

I know there is a great answer on a similar question. To repeat:

syn region FCall matchgroup=FName start='[[:alpha:]_]\i*\s*(' end=')' contains=FCall,FCallKeyword
syn match FCallKeyword /\i*\ze\s*=[^=]/ contained

Or, alternatively, in my own vim syntax file for python, I could manage to achieve a similar behavior:

syn match   pythonCall   /\<\h\i*\ze\s*(/    contains=pythonBuiltin,pythonBuiltinFunc,pythonBuiltinType
                                           \ nextgroup=pythonCallRegion skipwhite keepend
syn region  pythonCallRegion        contained matchgroup=pythonParamsDelim start=/(/  end=/)/ keepend extend
                                      \ contains=pythonCallComma,pythonCall,@pythonCallArgument,pythonCallArgKeyword
syn match   pythonCallArgKeyword    contained /\h\i*\ze\s*==\@!/

However, the problem is that when a previously existing syntax group (e.g. pythonBuiltinType) matches function name, the entire highlight for keyword arguments would not work:

For example:

dict(a=1, b=2)

where we have the following highlighting rule somewhere (e.g. python-mode)

syn keyword pythonBuiltinType dict ....

Given this, dict matches pythonBuiltinType, but not matches pythonCall. This happens for the both ways I just described. A screenshot:

What did I wrong here? Vim syntax is quite difficult, so your help would be greatly appreciated.

5
  • 2
    Does this snippet help?
    – user938271
    Dec 28, 2018 at 22:04
  • Wow, this works like a charm. Thanks a lot! A key idea is that we kind of "override" the syntax rule defined in other scripts (which is out of control) using the syn list and syn clear trick, in order to make it contained. This is pretty smart. Dec 28, 2018 at 22:31
  • 1
    In fact, I wish the original definition of pythonBuiltinType (e.g. from python-mode) had contained, which may give a cleaner solution. Maybe I should try updating all primitive syntax rules. Do you think, however, is there any way to make it work without touching them but playing with additory syntax (e.g. pythonCall)? Dec 28, 2018 at 22:34
  • I'm no expert on syntax highlighting, so I can't give you an answer. Maybe someone else will suggest such a solution. I couldn't find a better one because a keyword always has higher priority than a match or region (see :h :syn-keyword), and because the keywords in pythonBuiltinType are not nested: they match at the top level. If they were contained, you could probably override what you want.
    – user938271
    Dec 28, 2018 at 22:50
  • 1
    In the future, syntax highlighting could be performed by attaching properties to the text. Here's an excerpt from :h text-prop-intro: Instead of defining patterns to match the text, the highlighting is set by a script, possibly using the output of an external parser. You need a recent version of Vim to read this help file, and I don't think it's ready to replace syntax highlighting yet, because it's very recent and still under development. Maybe keep an eye on it, as it could help fix your issue later.
    – user938271
    Dec 28, 2018 at 22:50

1 Answer 1

0

The snippet link posted by @user938271 is broken after many years, so let me post a solution I ended up having here.

The problem is that the highlight group for built-in keywords (defined in python-mode) do not have the 'contained' property:

pythonBuiltinType xxx match /\C\<\%(list\|object\|str\| ... set\|bytearray\)\>/

so it cannot be contained in other syntax group (e.g. pythonCall).

We monkey-patch the syntax rule to have these highlight group have the contained property:

function! s:convert_syntax_keyword_containedin(syntaxGroup, syntaxCallGroup)
    " https://vi.stackexchange.com/questions/18408/ (courtesy of @user938271)
    let l:rule = execute(printf('syn list %s', a:syntaxGroup))
    let l:rule = matchstr(l:rule, 'xxx\zs.*')
    " if <xxx match> then, it is not 'syntax keyword' anymore. we should stop here
    if (l:rule =~ '^\s*match')
        return
    endif

    " convert it to regex rule
    let l:rule = substitute(l:rule, 'links\s\+to\s\+.*', '', '')
    let l:rule = join(split(l:rule), '\|')

    exe 'syn clear ' . a:syntaxGroup
    exe 'syn match ' . a:syntaxGroup     . ' /\C\<\%(' . l:rule . '\)\>/'
    exe 'syn match ' . a:syntaxCallGroup . ' /\C\<\%(' . l:rule . '\)\>/ contained'
endfunction

call s:convert_syntax_keyword_containedin('pythonBuiltinType', 'pythonCallBuiltinType')
call s:convert_syntax_keyword_containedin('pythonBuiltinFunc', 'pythonCallBuiltinFunc')

Now finally we can do like

syn match   pythonCall   /\<\h\i*\ze\s*(/    contains=pythonCallBuiltinType,pythonCallBuiltinFunc
                                           \ nextgroup=pythonCallRegion skipwhite keepend

See this commit 1dbf843 for the fully working changeset.

1
  • You might be interested in the relatively recent hlget()/hlset() API
    – D. Ben Knoble
    Sep 19, 2022 at 21:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.