Colorful ielm - font-locking comint

If you ever used ielm, or other comint-mode derivatives, you will notice that the text you input is not highlighted according to the major-mode.

If I type (setq foo bar) into ielm, the setq won't be highlighted.

Why is this? And how do we change this?

Naive solution

Look at font-lock-keywords in ielm and it is suspiciously near-empty. We could copy over emacs-lisp's keywords:

(setq-local font-lock-keywords `(,@lisp-el-font-lock-keywords-2

But what if I type in (princ "(setq foo bar)")? The output will inherit the highlighting.

Naively enabling font locking in comint buffers can lead to a mess of syntax highlighting in the output. While the example above is contrived, it is in general not a trivial problem.

I wrote and support hy-mode, a lisp embedded in Python. When the interpreter is given "–spy", the translation of the Hy code to Python is given in the output before the result of the Hy code. This translation would inherit Hy's syntax highlighting and look like a mess.

Python-mode's solution

python-mode actually implements fontification of shell input. How do they do it?

They add a post-command-hook that essentially extracts the current input being entered, fontifies it according to python, then reinserts it into the prompt.

There is quite a bit going on to make this work in practice - check out python-shell-font-lock-post-command-hook if you are interested.

I had success using this approach for hy-mode but always thought it was a kludge and difficult to understand and work with. Can't I just use font-lock-mode directly?

My solution

I came up with a hookless, pure font-lock-mode solution that should work for arbitrary modes.

I convert every font-lock-keyword MATCHER component to check that we are within a prompt before calling the MATCHER if it is a function or matching on it if it is a regex.

(require 'dash)

(defun kwd->comint-kwd (kwd)
  "Converts a `font-lock-keywords' KWD for `comint-mode' input fontification."
  (-let (((matcher . match-highlights) kwd))
    ;; below is ` quoted but breaks my blogs syntax higlighting, so removing it!
    ;; make sure to capture first paren in a ` if copying!
    ((lambda (limit)
       ;; Matcher can be a function or a regex
       (when ,(if (symbolp matcher)
                  `(,matcher limit)
                `(re-search-forward ,matcher limit t))
         ;; While the SUBEXP can be anything, this search always can use zero
         (-let ((start (match-beginning 0))
                ((comint-last-start . comint-last-end) comint-last-prompt)
                (state (syntax-ppss)))
           (and (> start comint-last-start)
                ;; Make sure not in comment or string
                ;; have to manually do this in custom MATCHERs
                (not (or (nth 3 state) (nth 4 state)))))))

(setq my-ielm-font-lock-kwds
      `(,@(-map #'kwd->comint-kwd lisp-el-font-lock-keywords-2)
        ,@(-map #'kwd->comint-kwd lisp-cl-font-lock-keywords-2)))

(defun set-my-ielm-kwds ()
  (setq-local font-lock-keywords my-ielm-font-lock-kwds))

Now ielm, my own hy-mode, etc. highlights shell input without messing with the output if I call set-my-ielm-kwds in an ielm buffer.

comments powered by Disqus