1

I'd like to locally remap a face to other custom face. The docstring reads:

(face-remap-set-base FACE &rest SPECS)

Set the base remapping of FACE in the current buffer to SPECS. This causes the remappings specified by ‘face-remap-add-relative’ to apply on top of the face specification given by SPECS.

The remaining arguments, SPECS, should form a list of faces. Each list element should be either a face name or a property list of face attribute/value pairs, like in a ‘face’ text property.

Armed with this knowledge, I tried:

(defface my/exceptional-face
  '((t (:background "red")))
  "My exceptional face.")
    
(face-remap-set-base 'font-lock-keyword-face '(my/exceptional-face))

In a Lisp buffer, M-x eval-buffer RET, I'd expect the keywords to show up with a red background. However, this fails with:

(wrong-type-argument listp my/exceptional-face)

Indeed, the face-remap-set-base function reads:

 (defun face-remap-set-base (face &rest specs)
  (while (and (consp specs) (not (null (car specs))) (null (cdr specs)))
    (setq specs (car specs)))
  (if (or (null specs)
      (and (eq (car specs) face) (null (cdr specs)))) ; Error raised here
      ;; Set entry back to default
      (face-remap-reset-base face)
    ;; Set the base remapping
    ...snip...))

It thus seems that there's no way I can pass a single face to this function. Using:

(face-remap-set-base 'font-lock-keyword-face '(my/exceptional-face nil))

…seems to work though. Is that the expected behavior, or is this a bug?

Drew
  • 75,699
  • 9
  • 109
  • 225
Michaël
  • 314
  • 1
  • 10

1 Answers1

1

Remove the quoted list from around its elements. Just provide the elements (as an implicit list) as additional args to face-remap-set-base:

(face-remap-set-base 'font-lock-keyword-face 'my/exceptional-face)

The code defining function face-remap-set-base binds its single &rest parameter SPECS to the list (my/exceptional-face).

The doc says:

The remaining arguments, SPECS, should form a list of faces.

What that means is what &rest means: provide faces individually as separate additional arguments. Don't provide an explicit list of faces as a single argument.

You provide any number of faces as additional actual arguments, and the function receives, as its single &rest argument SPECS, a list of those actual args that you passed.

See the Elisp manual, node Argument List, for an explanation of &rest.

In the code you provided the list (my/exceptional-face) that you passed resulted in the &rest parameter SPECS being bound to this list: ((my/exceptional-face)). Function face-remap-set-base figured that this was a list of one face, that face (or face spec) being the list (my/exceptional-face). A face spec that's a list needs a certain form, and (my/exceptional-face) doesn't have that form -- hence the error:

(wrong-type-argument listp my/exceptional-face)

See node Defining Faces of the Elisp manual for the proper list form of a face spec.


Oddly enough, I don't find anywhere in the Elisp manual or the Eintr manual (An Introduction to Programming in Emacs Lisp) where it is explained that passing multiple optional args to a function results in a single &rest list argument being received. But it's fundamental to Lisp (pretty much any Lisp, not just Elisp).

The Common Lisp doc tells you this about a &rest parameter:

all remaining arguments are made into a list for the &rest parameter


However, though all I said is I think true, the code I suggested that you use also fails.

I think there's a bug in the code of face-remap-set-base. I submitted bug report #45264. We'll find out....

I note that there are zero uses of face-remap-set-base in the Emacs Lisp code. Perhaps it was never tested...

(Or perhaps I'm mistaken, and someone else will provide a correct answer.)

Drew
  • 75,699
  • 9
  • 109
  • 225
  • Thanks for the investigation Drew, appreciated! I was aware of `&rest` arguments being passed as a list, although it seems that on SE, most people use these functions with a single list (e.g., [here](https://emacs.stackexchange.com/questions/10033/change-mode-line-buffer-id-face-for-inactive-windows)). Similarly, I've seen [here](https://emacs.stackexchange.com/questions/14638) people using `(face-remap-set-base 'f1 (face-all-attributes 'f2))`, but I couldn't make this to work—is it because `face-remap`s expect a list rather than an alist? – Michaël Dec 16 '20 at 00:44
  • It seems to me that `(elisp)Argument List` explains "that passing multiple optional args to a function results in a single &rest list argument being received" clearly enough (since 26.3 at minimum). Specifically the example which states "any arguments after the first four are collected into a list and ‘e’ is bound to that list". – phils Sep 07 '22 at 11:09