The objective is to do text highlighting when TODO and NOTE tokens are found somewhere in a txt file. This is what I have in the .emacs.el file:

(add-hook 'text-mode-hook 'font-lock-mode)
(setq fixme-modes
      '(text-mode c++-mode c-mode emacs-lisp-mode octave-mode python-mode))
(make-face 'font-lock-fixme-face)
(make-face 'font-lock-note-face)
(mapc (lambda (mode)
         '(("\\<\\(TODO\\)" 1 'font-lock-fixme-face t)
           ("\\<\\(NOTE\\)" 1 'font-lock-note-face t))))
(modify-face 'font-lock-fixme-face "Red" nil nil t nil t nil nil)
(modify-face 'font-lock-note-face "Dark Green" nil nil t nil t nil nil)

;;(global-font-lock-mode 1)
(add-hook 'text-mode-hook 'flyspell-mode)
(add-hook 'emacs-lisp-mode-hook 'eldoc-mode)

The final two lines were added for debugging purposes, to check if other features could work in text-mode.

While editing C++, Python, and Octave source files - it works perfectly.

PS: I'm new to writing Elisp code!


Why does font-lock-mode work with various programming languages but it does not in text-mode?

font-lock-mode works fine in text-mode, it's just that text-mode does not initialise font-lock-mode by setting font-lock-defaults. Quoth (info "(elisp) Font Lock Basics"):

   There are several variables that control how Font Lock mode
highlights text.  But major modes should not set any of these variables
directly.  Instead, they should set ‘font-lock-defaults’ as a
buffer-local variable.  The value assigned to this variable is used, if
and when Font Lock mode is enabled, to set all the other variables.

 -- Variable: font-lock-defaults
     This variable is set by modes to specify how to fontify text in
     that mode.  It automatically becomes buffer-local when set.  If its
     value is ‘nil’, Font Lock mode does no highlighting, and you can
     use the ‘Faces’ menu (under ‘Edit’ and then ‘Text Properties’ in
     the menu bar) to assign faces explicitly to text in the buffer.

and (info "(elisp) Search-based Fontification"):

The variable which directly controls search-based fontification is
‘font-lock-keywords’, which is typically specified via the KEYWORDS
element in ‘font-lock-defaults’.

 -- Variable: font-lock-keywords
     The value of this variable is a list of the keywords to highlight.
     Lisp programs should not set this variable directly.  Normally, the
     value is automatically set by Font Lock mode, using the KEYWORDS
     element in ‘font-lock-defaults’.  The value can also be altered
     using the functions ‘font-lock-add-keywords’ and
     ‘font-lock-remove-keywords’ (see Customizing Keywords).

So the problem here is that c++-mode, c-mode, emacs-lisp-mode, octave-mode, and python-mode all initialise font-lock-defaults to some non-nil value with their custom fontification rules, but text-mode doesn't (because it doesn't by itself have any fontifification rules).

Indeed, with the following your code should work (this is just for illustration, not recommended):

(add-hook 'text-mode-hook (lambda () (setq font-lock-defaults '(nil))))

So far so good. The real subtlety, though, lies in what font-lock-add-keywords does. With a non-nil argument, it actually modifies font-lock-keywords-alist, not the variables font-lock-defaults or font-lock-keywords which font-lock-mode uses to determine whether and how to do any fontification. This is because font-lock-add-keywords implicitly expects the mode in question to have already initialised font-lock-defaults appropriately.

By contrast, with a nil argument, font-lock-add-keywords forcefully initialises and reenables font-lock-mode if it has to:

;; ...
(when (and font-lock-mode
           (not (or font-lock-keywords font-lock-defaults)))
  ;; The major mode has not set any keywords, so when we enabled
  ;; font-lock-mode it only enabled the font-core.el part, not the
  ;; font-lock-mode-internal.  Try again.
  (font-lock-mode -1)
  (setq-local font-lock-defaults '(nil t))
  (font-lock-mode 1))
;; ...

This is kind of hinted at in the docstring of font-lock-keywords:

A user-level keywords list is what a major mode or the user would
set up.  Normally the list would come from `font-lock-defaults'.
through selection of a fontification level and evaluation of any
contained expressions.  You can also alter it by calling
`font-lock-add-keywords' or `font-lock-remove-keywords' with MODE = nil.

So one simple workaround is to call font-lock-add-keywords with a nil argument, as shown in (info "(emacs) Font Lock"):

(defvar my-fixme-hooks
  '(text-mode-hook prog-mode-hook)
  "List of mode hooks in which to fontify \"FIXME\" keywords.")

(defface my-fixme-face
  '((t :foreground "Red" :weight bold :underline t))
  "Default face for \"FIXME\" keywords.")

(defface my-note-face
  '((t :foreground "Dark Green" :inherit my-fixme-face))
  "Face for \"NOTE\" keywords.")

(defun my-fixme-fontify ()
  "Fontify \"FIXME\" keywords in the current buffer."
   '(("\\<\\(TODO\\)" 1 'my-fixme-face t)
     ("\\<\\(NOTE\\)" 1 'my-note-face t))))

(dolist (hook my-fixme-hooks)
  (add-hook hook #'my-fixme-fontify))

The only problem with this is that it applies not only to the mode hooks in my-fixme-hooks, but also to all their derived modes. If this is undesirable, I think the simplest workaround is to set font-lock-defaults as I showed at the start (but only if it's nil).

Either way, I think the situation and documentation is confusing/subtle enough that I recommend reporting it upstream via M-x report-emacs-bug RET. Even if font-lock-add-keywords is behaving 100% as desired, it would be nice if the caveat in question were documented more clearly. TIA.

  • +1 - nice answer!
    – NickD
    Jun 9 '21 at 14:34

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.