I occasionally like to tinker with syntax files (especially conceals; I use λ for anonymous functions (lambda:, fn) in several languages).

However, one complaint with conceal is that I can only use a single character. When concealing, e.g., fn, having one character breaks some vertical alignment. One solution is to use λ. instead, keeping the two characters and aligning with the lambda-calculus origins.

In at least two syntax files I use, fn is part of a syn-keyword definition. In order to use the λ. above, I need to do something like this:

syn match group /fn\@=/ conceal cchar=λ
syn match group /fn\@<=/ conceal cchar=.

However, syn-matchs have lower priority, so these don't apply. The original

syn keyword group fn conceal cchar=λ

works just fine, because it is a keyword defined later.


I would like to either

  • remove fn from a syntax keyword group, or
  • override a keyword from a match

One possible solution is

syn clear group
" re-add everything else
" syn keyword group ...

But this fails when group has more than just keywords and is very complex. It's also not scalable at all.

I'm open to some kind of meta-programming/parsing the output of syn list, but it needs to be quasi-readable/understandable.

Here's a clojure example:

; should be concealed?
fn [x]
; definitely should be
(fn [x] x)
; definitely should be
( fn [x])
; shouldn't be, though my current keyword solution would
(let [fn 1] fn)
; same
(let [ fn 1] fn)
; definitely shouldn't be
(let [afn 1] afn)

And here's one from sml

(* should conceal *)
val id = fn x => x
(* also should conceal *)
val _ = (fn x => x) id
  technically this isn't valid SML, but it should conceal
  and it demonstrates a beginning-of-line issue
fn x => x

  • Which language uses fn for lambdas? (Just trying to find the best reproducer for your issue...) – filbranden May 7 '20 at 17:29
  • 1
    @filbranden at least sml and clojure (i cant remember if common lisp does or not) – D. Ben Knoble May 7 '20 at 18:47
  • I don’t know LISP. What’s the difference between the should and shouldn’t examples? – Rich May 8 '20 at 6:57
  • Presumably the shouldn’t ones aren’t anonymous functions, but how can I tell the difference, is what I mean. – Rich May 8 '20 at 6:58
  • @Rich the difference is that, in the (let [fn 1] ...) versions, the identifier fn is being temporarily re-bound to 1. I won't mind if they get concealed, since I consider that an extreme abuse and would never make it past one of my code-reviews. The last example (let [afn 1] ...) is different, since the identifier is afn and that's "fine" stylistically, but shouldn't be concealed – D. Ben Knoble May 8 '20 at 22:14

Here's a solution using a different technique to the two you suggested:

syntax match lambda /\_.\<fn\>/ containedin=ALL
syntax match lam /f/ containedin=lambda contained conceal cchar=λ
syntax match bda /n/ containedin=lambda contained conceal cchar=.

Syntax items which start in an earlier position in the buffer take priority over other items, so we set up a group that will start just before the fn, and then add our conceal groups inside that.

Unfortunately you don't mention what language or syntax files you're using so it's not fully tested, but it works for me in the following block of PHP:

$modifier = 5;
array_map(fn($x) => $x * $modifier, $numbers);


The above was written before the specific languages and examples were added to the question, and doesn't work well when used within Vim's standard Clojure highlighting. Here's a solution that works (exclusively) inside a Clojure s-expression:

syntax region lambda matchgroup=clojureParen start="(\zefn" end=")" containedin=clojureSexp
syntax match lam /f/ contained containedin=lambda conceal cchar=λ nextgroup=bda
syntax match bda /n/ contained conceal cchar=. nextgroup=aaa
syntax match aaa /[^)]*/ contains=@clojureTop contained

Unfortunately, it's (necessarily) a lot more tightly integrated with the existing syntax items, because of the way the various nested groups connect.

It works by creating a lambda group that can be found within and which spans an entire clojureSexp, when this clojureSexp begins with an instance of fn. Inside this group are then the lam and bda groups which match the f and n respectively, and these are followed by an aaa group which contains the rest of the normal Clojure syntax items that can be found within an s-expression.

And here's a version that highlights all the other cases:

syntax match lambda /\_[^(]\<fn\>/ containedin=ALL
syntax match lambda /\_[^(]\{2}\<fn\>/ containedin=ALL transparent contains=NONE
syntax match lam /f/ containedin=lambda contained conceal cchar=λ
syntax match bda /n/ containedin=lambda contained conceal cchar=.

Combining the two is left as an exercise for the reader. The simplest method is just to use two entirely separate sets of syntax groups with different names, but there's probably a more succinct solution:

enter image description here

  • That's pretty much what I had in mind, using contained for the conceals. You could do that from a syn keyword too I believe. – filbranden May 7 '20 at 18:53
  • @filbranden Using keyword for lam and bda doesn't seem to work (if that's what you mean). I'm not sure why. – Rich May 7 '20 at 18:55
  • 1
    Contained is a neat trick (keeps the regexs cleaner). I’ll test this in a moment; I knew items starting earlier take precedence, but didnt think to abuse that here. Is there a potential problem with . matching too much? Eg afn (some random identifier/var name/whatever)? Or in lisps (fn (although here conceal would be expected)? – D. Ben Knoble May 7 '20 at 19:19
  • 1
    @filbranden Ah, no, unfortunately keyword items cannot contain any other items of any type. – Rich May 7 '20 at 20:23
  • 1
    @D.BenKnoble An earlier, slightly more complicated, version worked fine at the start of the line. I’ll test again in Clojure and see if I can tweak it. – Rich May 7 '20 at 20:25

Here's a completely different, arguably even more hacky, but much simpler solution:

syntax keyword lam fn conceal cchar=λ containedin=ALL nextgroup=bda
execute "syntax match bda ' ' conceal cchar=\uff0e"

It relies on:

  1. There always being a space after the fn keyword: we're matching this space as well as the fn.
  2. Unicode support: we're replacing the space with a full width dot.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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