From 725120ca3de9f30c8c6bbaeb237f9c803c12b442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Marks?= Date: Wed, 3 Jun 2026 16:43:37 -0400 Subject: [PATCH] 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. --- etc/NEWS | 22 +++++++++++++++++++ lisp/cus-edit.el | 56 +++++++++++++++++++++++++++++++++++++++--------- lisp/files.el | 4 +++- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 158081f1195..44c4ab282ba 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -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 diff --git a/lisp/cus-edit.el b/lisp/cus-edit.el index 3687f231f94..adc9d57b4ce 100644 --- a/lisp/cus-edit.el +++ b/lisp/cus-edit.el @@ -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) diff --git a/lisp/files.el b/lisp/files.el index 22313b71635..5aaa0d44a04 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -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.