Programming ligatures in Emacs
emacs fonts ~3 minutes read

For a long time I was a fan of Hack font. It has really nice language support, great readability at size of 9pt, and zero with a dot. I love when zero comes with a dot. Many fonts use zero with a line, to differentiate it from capital O, but on small sizes it is not great, however dot looks fine when both small and big.

But one thing was lacking from Hack, and it is ligature support. I always wanted to try out ligatures, but there were no other fonts that were visually close to Hack, appealing to my taste, and had good programming ligatures support. I’ve tried Fira Code, but it was too curly for my liking, Iosevka was a bit too narrow, and there’s also Hæck, that is based on Hack, and has ligatures, so looked like a perfect candidate. But my eye was catch by JetBrains Mono. While it’s not the same as Hack, it is visually similar, has great ligature support, and looks a bit nicer in Emacs! Hack works for me in terminal, where I don’t need ligatures, while in Emacs something a little more thick is preferable. So I’ve chosen it, but ligatures are not working out of the box.

Enabling support for ligatures

There are several options for enabling ligatures in Emacs, listed at Fira Code Wiki page. There are four options:

  • composition mode in Emacs Mac port
  • prettify-symbols
  • composition char table
  • font-lock keywords

We’re interested in composition char table. The basic idea is to provide starting character, and a regular expression, that matches all ligatures starting with that character, and put it to the composition table. Here’s the code for JetBrains Mono:

(let ((ligatures `((?-  . ,(regexp-opt '("-|" "-~" "---" "-<<" "-<" "--" "->" "->>" "-->")))
                   (?/  . ,(regexp-opt '("/**" "/*" "///" "/=" "/==" "/>" "//")))
                   (?*  . ,(regexp-opt '("*>" "***" "*/")))
                   (?<  . ,(regexp-opt '("<-" "<<-" "<=>" "<=" "<|" "<||" "<|||::=" "<|>" "<:" "<>" "<-<"
                                         "<<<" "<==" "<<=" "<=<" "<==>" "<-|" "<<" "<~>" "<=|" "<~~" "<~"
                                         "<$>" "<$" "<+>" "<+" "</>" "</" "<*" "<*>" "<->" "<!--")))
                   (?:  . ,(regexp-opt '(":>" ":<" ":::" "::" ":?" ":?>" ":=")))
                   (?=  . ,(regexp-opt '("=>>" "==>" "=/=" "=!=" "=>" "===" "=:=" "==")))
                   (?!  . ,(regexp-opt '("!==" "!!" "!=")))
                   (?>  . ,(regexp-opt '(">]" ">:" ">>-" ">>=" ">=>" ">>>" ">-" ">=")))
                   (?&  . ,(regexp-opt '("&&&" "&&")))
                   (?|  . ,(regexp-opt '("|||>" "||>" "|>" "|]" "|}" "|=>" "|->" "|=" "||-" "|-" "||=" "||")))
                   (?.  . ,(regexp-opt '(".." ".?" ".=" ".-" "..<" "...")))
                   (?+  . ,(regexp-opt '("+++" "+>" "++")))
                   (?\[ . ,(regexp-opt '("[||]" "[<" "[|")))
                   (?\{ . ,(regexp-opt '("{|")))
                   (?\? . ,(regexp-opt '("??" "?." "?=" "?:")))
                   (?#  . ,(regexp-opt '("####" "###" "#[" "#{" "#=" "#!" "#:" "#_(" "#_" "#?" "#(" "##")))
                   (?\; . ,(regexp-opt '(";;")))
                   (?_  . ,(regexp-opt '("_|_" "__")))
                   (?\\ . ,(regexp-opt '("\\" "\\/")))
                   (?~  . ,(regexp-opt '("~~" "~~>" "~>" "~=" "~-" "~@")))
                   (?$  . ,(regexp-opt '("$>")))
                   (?^  . ,(regexp-opt '("^=")))
                   (?\] . ,(regexp-opt '("]#"))))))
  (dolist (char-regexp ligatures)
    (set-char-table-range composition-function-table (car char-regexp)
                          `([,(cdr char-regexp) 0 font-shape-gstring]))))

By enabling auto-composition-mode we can see the ligatures in action

  • Ligatures off:
  • Ligatures on:

Hint: open images in separate tabs and toggle between those to spot the difference on hard ones like ####.

But I don’t want these ligatures to be enabled everywhere, only in programming related modes, so here’s a config for composite package:

(use-package composite
  :hook (prog-mode . auto-composition-mode)
  :init (global-auto-composition-mode -1))

This takes care of most of errors, listed under composition char table section in Fira Code Wiki. Also by using regexp-opt function we avoid writing complex regular expressions with backslash escape hell, and handle this task to Emacs itself. It’s good when Emacs does boring stuff for us, isn’t it?