4

After installing the JetBrains Mono Font and setting it as my default face. I am wandering how to enable Ligatures in Emacs.

I tried a solution based on this answer that show how to do this with Fira Code, with what I think are the required modifications I am speaking about:


EDIT: @Piquan's answer, suggested to me that I wasn't clear about what are the required modifications to the fira-code-mode, so here they are:

  • Search and replace from fira-code to jetbrains-mono
  • Change the code points from #Xe100 . #Xe16f to #X10001 . #X1009c
  • Update the jetbrains-ligature-mode--ligatures constant to reflect the actual character map of the JetBrains Mono font as I see them (using FontForge).
  • Since the #X10001 . #X1009c contains some diacritics I didn't wish to be part of the resulting prettify-symbols-alist, I put some nils into the said constant, and introduced an (if ...) expression to the jetbrains-ligature-mode--make-alist function to ignore those nils and just increment the code point.
  • AFAIK, the reason the Fira Code Ligature was created for Emacs compatibility, is to be compatible with the upstream haskling-mode that used those code points. But since I modified the numbers to be compatible with the JetBrains Mono original font, this is not needed.
  • I should be able to do without jetbrains-ligature-mode--setup function, but I figured out it shouldn't hurt to keep it (and indeed removing it as a troubleshooting step didn't help).

Thus the resulting code is:

(defun jetbrains-ligature-mode--make-alist (list)
   "Generate prettify-symbols alist from LIST."
   (let ((idx -1))
     (mapcar
      (lambda (s)
        (setq idx (1+ idx))
        (if s
            (let* ((code (+ #X10001 idx))
                   (width (string-width s))
                   (prefix ())
                   (suffix '(?\s (Br . Br)))
                   (n 1))
              (while (< n width)
                (setq prefix (append prefix '(?\s (Br . Bl))))
                (setq n (1+ n)))
              (cons s (append prefix suffix (list (decode-char 'ucs code)))))))
      list)))

 (defconst jetbrains-ligature-mode--ligatures
   '("-->" "//" "/**" "/*" "*/" "<!--" ":=" "->>" "<<-" "->" "<-"
     "<=>" "==" "!=" "<=" ">=" "=:=" "!==" "&&" "||" "..." ".."
     nil nil nil nil nil nil nil nil nil nil nil nil nil nil
     "|||" "///" "&&&" "===" "++" "--" "=>" "|>" "<|" "||>" "<||"
     "|||>" "<|||" ">>" "<<" nil nil "::=" "|]" "[|" "{|" "|}"
     "[<" ">]" ":?>" ":?" nil "/=" "[||]" "!!" "?:" "?." "::"
     "+++" "??" "###" "##" ":::" "####" ".?" "?=" "=!=" "<|>"
     "<:" ":<" ":>" ">:" "<>" "***" ";;" "/==" ".=" ".-" "__"
     "=/=" "<-<" "<<<" ">>>" "<=<" "<<=" "<==" "<==>" "==>" "=>>"
     ">=>" ">>=" ">>-" ">-" "<~>" "-<" "-<<" "=<<" "---" "<-|"
     "<=|" "/\\" "\\/" "|=>" "|~>" "<~~" "<~" "~~" "~~>" "~>"
     "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "</>" "</" "/>"
     "<->" "..<" "~=" "~-" "-~" "~@" "^=" "-|" "_|_" "|-" "||-"
     "|=" "||=" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#="
     "&="))

(defvar jetbrains-ligature-mode--old-prettify-alist)

(defun jetbrains-ligature-mode--enable ()
    "Enable JetBrains Mono ligatures in current buffer."
    (setq-local jetbrains-ligature-mode--old-prettify-alist prettify-symbols-alist)
       (setq-local prettify-symbols-alist (append (jetbrains-ligature-mode--make-alist jetbrains-ligature-mode--ligatures) jetbrains-ligature-mode--old-prettify-alist))
       (prettify-symbols-mode t))

(defun jetbrains-ligature-mode--disable ()
    "Disable JetBrains Mono ligatures in current buffer."
    (setq-local prettify-symbols-alist jetbrains-ligature-mode--old-prettify-alist)
    (prettify-symbols-mode -1))

(define-minor-mode jetbrains-ligature-mode
    "JetBrains Mono ligatures minor mode"
    :lighter " JetBrains Mono"
    (setq-local prettify-symbols-unprettify-at-point 'right-edge)
    (if jetbrains-ligature-mode
        (jetbrains-ligature-mode--enable)
      (jetbrains-ligature-mode--disable)))

(defun jetbrains-ligature-mode--setup ()
    "Setup JetBrains Mono Symbols"
    (set-fontset-font t '(#X10001 . #X1009c) "JetBrains Mono"))

(provide 'jetbrains-ligature-mode)

However the best I got so far is that the sequence is replaced with an empty space (although with the correct length).

What am I missing?


I am using Emacs 26.3 (from Kevin Kelley's PPA) on Kubuntu 18.04

2
  • Perhaps this will be improved with the font rendering and text shaping improvements coming to emacs 27. Perhaps it won’t. Commented Jan 24, 2020 at 22:57
  • 1
    I attempted a similar thing, and ran into the same problem you did. I’m thinking it might be because the Unicode value in the JetBrains Mono font is -1 (right click -> glyph info) for the ligatures. In the Fira Code Symbol font the values are their indexes in the font. My guess is the prettify doesn’t see the ligatures as valid characters because of this, but editors like VS Code with native ligature support don’t need a valid value. I haven’t had a chance yet to test this theory, but if I get the chance before you do I’ll update here.
    – user43024
    Commented Feb 4, 2020 at 15:47

2 Answers 2

6

You want to do this with composition rules, not with prettify-symbols. Try this instead:

(defconst jetbrains-ligature-mode--ligatures
   '("-->" "//" "/**" "/*" "*/" "<!--" ":=" "->>" "<<-" "->" "<-"
     "<=>" "==" "!=" "<=" ">=" "=:=" "!==" "&&" "||" "..." ".."
     "|||" "///" "&&&" "===" "++" "--" "=>" "|>" "<|" "||>" "<||"
     "|||>" "<|||" ">>" "<<" "::=" "|]" "[|" "{|" "|}"
     "[<" ">]" ":?>" ":?" "/=" "[||]" "!!" "?:" "?." "::"
     "+++" "??" "###" "##" ":::" "####" ".?" "?=" "=!=" "<|>"
     "<:" ":<" ":>" ">:" "<>" "***" ";;" "/==" ".=" ".-" "__"
     "=/=" "<-<" "<<<" ">>>" "<=<" "<<=" "<==" "<==>" "==>" "=>>"
     ">=>" ">>=" ">>-" ">-" "<~>" "-<" "-<<" "=<<" "---" "<-|"
     "<=|" "/\\" "\\/" "|=>" "|~>" "<~~" "<~" "~~" "~~>" "~>"
     "<$>" "<$" "$>" "<+>" "<+" "+>" "<*>" "<*" "*>" "</>" "</" "/>"
     "<->" "..<" "~=" "~-" "-~" "~@" "^=" "-|" "_|_" "|-" "||-"
     "|=" "||=" "#{" "#[" "]#" "#(" "#?" "#_" "#_(" "#:" "#!" "#="
     "&="))

(sort jetbrains-ligature-mode--ligatures (lambda (x y) (> (length x) (length y))))

(dolist (pat jetbrains-ligature-mode--ligatures)
  (set-char-table-range composition-function-table
                      (aref pat 0)
                      (nconc (char-table-range composition-function-table (aref pat 0))
                             (list (vector (regexp-quote pat)
                                           0
                                    'compose-gstring-for-graphic)))))

In order to improve the performance of redisplay, you could take all the ligature patterns that start with the same character, and use regexp-opt to produce one regular expression matching all of them, ie use

(regexp-opt '("###" "## "####" "#{" "#[" "#(" "#?" "#_" "#_(" "#:" "#!" "#="))

as the pattern for #.

6
  • Thanks this is exactly what I was looking for! Your code works perfectly for me, as of Emacs 26.3. Commented May 11, 2020 at 16:38
  • This worked great for me until I typed four asterisks in a row (org-mode) and emacs froze solid... Commented Sep 23, 2020 at 0:56
  • Using which version of emacs? (presumably from 'emacs -Q'). If you still get this in emacs-27, please M-x report-emacs-bug.
    – rpluim
    Commented Sep 23, 2020 at 8:18
  • The order in which the ligatures appear is important for all of them to work properly, In other words, this kinda works, but for example +++ renders as ++ ligature followed by a +. But making the +++ ligature appear before the ++ ligature minor fixes this issue for me. Commented Apr 17, 2021 at 19:11
  • That's a good suggestion (but I removed your hash-quoting of the lambda, it's not necessary)
    – rpluim
    Commented Apr 18, 2021 at 11:34
0

Per the upstream Fira Code instructions, you have to install the Fira Code Symbol font to use the mechanism being used here. The Fira Code Symbol font is a separate font that has the Fira Code ligatures, but puts them as regular characters in the font so that Emacs can access them. (You may note that the original Fira Code example referred to "Fira Code Symbol" rather than "Fira Code" in the set-fontset-font call.) Without that font, this mechanism won't work.

Unfortunately, I don't know of an analogous symbol font for JetBrains Mono yet, so this solution wouldn't apply. Of course, JetBrains Mono is quite new, so somebody may make one soon!

If somebody does create one, you'll need to make sure that the constant base that you use (#X10001 in your post), and the order of the elements of jetbrains-ligature-mode--ligatures, both match the (theoretical future) JetBrains Mono Symbol font.

2
  • #Piquan, thanks for your answer. I think our understanding of the fira-code-mode is more or less similar. I updated my question to clarify what I changed in it to get to my code. Does this look sensible, or does the range #Xe100 . #Xe16f is somewhat more real from #X10001 . #X1009c in this context?
    – Chen Levy
    Commented Jan 25, 2020 at 10:14
  • 1
    Thanks for clarifying. I haven't looked at the font in FontForge, but I have in other tools. I don't understand the tools closely, but as far as I can tell, the ligatures in JetBrains Mono have glyph values (like all glyphs must), but those aren't assigned to any individual Unicode codepoints. So in other words, there's not a single Unicode character that Emacs could print that would cause that ligature to be printed. The numbers computed in jetbrains-ligature-mode--make-alist are supposed to correspond to Unicode codepoints, not glyph numbers.
    – Piquan
    Commented Jan 25, 2020 at 10:15

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.