Set user options in file/directory locals like 'setopt-local' (bug#81120)

This ensures defcustom ':set' functions are invoked.  The new
user option 'setopt-local-type-mismatch' can prompt users to
accept or discard type-mismatched values or to always accept or
discard such values.

* lisp/cus-edit.el (setopt-local-type-mismatch): New defcustom.
(setopt--set-local): Consult setopt-local-type-mismatch.
* lisp/files.el (hack-one-local-variable): Detect a custom
variable and call 'setopt--set-local'.
* etc/NEWS: Announce the change.
This commit is contained in:
Stéphane Marks 2026-06-03 16:43:37 -04:00 committed by Eli Zaretskii
parent 6e67ac222b
commit 725120ca3d
3 changed files with 71 additions and 11 deletions

View file

@ -49,6 +49,28 @@ behavior.
If this variable is nil, 'tty-cursor-movement-use-TAB-BS' has no effect,
and Emacs will never use TABs for any cursor-movement sequences.
---
** File- and directory-locals respect user option setters.
Values of variables that are user options mentioned in file-local
variable properties and directory-locals via ".dir-locals.el" are now
set similarly to 'setopt-local'; i.e., if a user option has a defcustom
':set' function, that function will be invoked.
---
** New user option 'setopt-local-type-mismatch'.
This option controls what 'setopt-local' does when it detects a type
mismatch between the specified value and the 'defcustom' of its user
option. Its backward-compatible default is nil which emits a warning
and accepts the type-mismatched value. If you customize it to non-nil,
you will be prompted to accept or ignore the value. If it is the symbol
'accept', warnings are inhibited and type-mismatched values are
accepted. If 'discard', warnings are inhibited and type-mismatched
values are discarded.
*** Specifying a minor mode as a local variables enables that mode,
unconditionally. The previous behavior, toggling the mode, was
neither reliable nor generally desirable.
* Editing Changes in Emacs 32.1

View file

@ -1104,6 +1104,22 @@ even if it doesn't match the type.)
(put variable 'custom-check-value (list value))
(funcall (or (get variable 'custom-set) #'set-default) variable value))
;;;###autoload
(defcustom setopt-local-type-mismatch nil
"Behavior of `setopt-local if value's type mismatches its definition.
If nil, emit a warning and assign the value.
If non-nil, prompt to accept or discard the value.
If the symbol `accept', ignore type mismatch warning and assign the value.
If the symbol `discard', ignore warning and discard the mismatched value.
Note: Accepting mismatched values may result in unexpected behavior."
:type '(choice (const :tag "Emit a warning and accept the type value" nil)
(const :tag "Prompt to accept or discard the value" t)
(const :tag "Ignore the warning and accept the value" accept)
(const :tag "Ignore the warning and discard the value" discard))
:version "32.1"
:safe #'symbolp
:group 'customize)
;;;###autoload
(defmacro setopt-local (&rest pairs)
"Set buffer local VARIABLE/VALUE pairs, and return the final VALUE.
@ -1135,17 +1151,37 @@ Signal an error if a `custom-set' form does not support the
;;;###autoload
(defun setopt--set-local (variable value)
"Set a buffer local VARIABLE to VALUE.
Consult `setopt-local-type-mismatch'."
(custom-load-symbol variable)
;; Check that the type is correct.
(when-let* ((type (get variable 'custom-type)))
(unless (widget-apply (widget-convert type) :match value)
(warn "Value does not match %S's type `%S': %S" variable type value)))
(condition-case _
(funcall (or (get variable 'custom-set)
(lambda (x v &optional _) (set-local x v)))
variable value 'buffer-local)
(wrong-number-of-arguments
(error "The setter of %S does not support setopt-local" variable))))
(let ((accept t))
;; Check that the type is correct.
(when-let* ((type (get variable 'custom-type)))
(unless (widget-apply (widget-convert type) :match value)
(let ((msg (format-message
"Value does not match %S's type `%S': %S"
variable type value)))
(cond
;; Fall through and try anyway.
((eq setopt-local-type-mismatch 'accept))
;; Silently discard the mismatched value.
((eq setopt-local-type-mismatch 'discard)
(setq accept nil))
;; Prompt to accept or discard the value.
(setopt-local-type-mismatch
(setq accept (eq ?a (car
(read-multiple-choice msg
'((?a "accept" "Accept")
(?d "discard" "Discard")))))))
(t
(warn msg))))))
(when accept
(condition-case _
(funcall (or (get variable 'custom-set)
(lambda (x v &optional _) (set-local x v)))
variable value 'buffer-local)
(wrong-number-of-arguments
(error "The setter of %S does not support setopt-local" variable))))))
;;;###autoload
(defun customize-save-variable (variable value &optional comment)

View file

@ -4623,7 +4623,9 @@ already the major mode."
;; so it is risky to put them on with a local variable list.
(if (stringp val)
(set-text-properties 0 (length val) nil val))
(set (make-local-variable var) val))))
(if (custom-variable-p var)
(setopt--set-local var val)
(set (make-local-variable var) val)))))
;;; Handling directory-local variables, aka project settings.