From 87d844e855d35c6ed346ccfcb04791281c91224b Mon Sep 17 00:00:00 2001 From: Mauro Aranda Date: Mon, 23 Oct 2023 09:45:12 -0300 Subject: [PATCH] Add easy customization for dir-locals files (Bug#66702) * lisp/cus-edit.el (custom--editable-field-p): New utility function. (custom-dirlocals-widget, custom-dirlocals-file-widget) (custom-dirlocals-commands, custom-dirlocals-tool-bar-map): New variables. (custom-dirlocals-map, custom-dirlocals-field-map): New keymaps. (Custom-dirlocals-menu): New menu. (custom-dirlocals-key, custom-dynamic-cons, custom-dirlocals): New widgets. (custom-dirlocals-maybe-update-cons, custom-dirlocals-symbol-action) (custom-dirlocals-change-file, custom-dirlocals--set-widget-vars) (custom-dirlocals-get-options, custom-dirlocals-validate): New functions. (custom-dirlocals-with-buffer): New macro. (Custom-dirlocals-revert-buffer, Custom-dirlocals-save) (customize-dirlocals): New commands. * doc/emacs/custom.texi (Directory Variables): Document customize-dirlocals. * etc/NEWS: Announce. --- doc/emacs/custom.texi | 5 + etc/NEWS | 5 + lisp/cus-edit.el | 289 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) diff --git a/doc/emacs/custom.texi b/doc/emacs/custom.texi index adecc873163..6018bc4e014 100644 --- a/doc/emacs/custom.texi +++ b/doc/emacs/custom.texi @@ -1515,6 +1515,11 @@ Directory Variables valid filename, either @file{.dir-locals.el} or @file{.dir-locals-2.el}. +@findex customize-dirlocals +There's also a command to pop up an Easy Customization buffer +(@pxref{Easy Customization}) to edit directory local variables, +@code{customize-dirlocals}. + @findex dir-locals-set-class-variables @findex dir-locals-set-directory-class Another method of specifying directory-local variables is to define diff --git a/etc/NEWS b/etc/NEWS index d0880669752..63fe609ab68 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -894,6 +894,11 @@ When this is non-nil, the lines of key sequences are displayed with the most recent line first. This is can be useful when working with macros with many lines, such as from 'kmacro-edit-lossage'. +** Customize ++++ +*** New command customize-dirlocals +This command pops up a buffer to edit the settings in .dir-locals.el + * New Modes and Packages in Emacs 30.1 diff --git a/lisp/cus-edit.el b/lisp/cus-edit.el index 706e08d5657..c2c7a5531c3 100644 --- a/lisp/cus-edit.el +++ b/lisp/cus-edit.el @@ -512,6 +512,13 @@ custom-menu-filter (push name result))) (nreverse result))) +(defun custom--editable-field-p (widget) + "Non-nil if WIDGET is an editable-field widget, or inherits from it." + (let ((type (widget-type widget))) + (while (and type (not (eq type 'editable-field))) + (setq type (widget-type (get type 'widget-type)))) + type)) + ;;; Unlispify. (defvar custom-prefix-list nil @@ -5645,6 +5652,288 @@ custom-save-icons (prin1 value (current-buffer))) (insert ")\n"))))) +;;; Directory Local Variables. +;; The following code provides an Easy Customization interface to manage +;; `.dir-locals.el' files. +;; The main command is `customize-dirlocals'. It presents a Custom-like buffer +;; but with a few tweaks. Variables are inserted in a repeat widget, and +;; update its associated widget (the one for editing the value) upon the user +;; hitting RET or TABbing out of it. +;; This is unlike the `cus-theme.el' interface for editing themes, that prompts +;; the user for the variable to then create the appropriate widget. +(defvar-local custom-dirlocals-widget nil + "Widget that holds the dir-locals customizations.") + +(defvar-local custom-dirlocals-file-widget nil + "Widget that holds the name of the dir-locals file being customized.") + +(defvar-keymap custom-dirlocals-map + :doc "Keymap used in the \"*Customize Dirlocals*\" buffer." + :full t + :parent widget-keymap + "SPC" #'scroll-up-command + "S-SPC" #'scroll-down-command + "DEL" #'scroll-down-command + "C-x C-s" #'Custom-dirlocals-save + "q" #'Custom-buffer-done + "n" #'widget-forward + "p" #'widget-backward) + +(defvar custom-dirlocals-field-map + (let ((map (copy-keymap custom-field-keymap))) + (define-key map "\C-x\C-s" #'Custom-dirlocals-save) + (define-key map "\C-m" #'widget-field-activate) + map) + "Keymap for the editable fields in the \"*Customize Dirlocals*\" buffer .") + +(defvar custom-dirlocals-commands + '((" Save Settings " Custom-dirlocals-save t + "Save Settings to the dir-locals file." "save" "Save" t) + (" Undo Edits " Custom-dirlocals-revert-buffer t + "Revert buffer, undoing any editions." + "refresh" "Undo" t) + (" Help for Customize " Custom-help t "Get help for using Customize." + "help" "Help" t) + (" Exit " Custom-buffer-done t "Exit Customize." "exit" "Exit" t)) + "Alist of specifications for Customize menu items, tool bar icons and buttons. +See `custom-commands' for further explanation.") + +(easy-menu-define + Custom-dirlocals-menu (list custom-dirlocals-map + custom-dirlocals-field-map) + "Menu used in dirlocals customization buffers." + (nconc (list "Custom" + (customize-menu-create 'customize)) + (mapcar (lambda (arg) + (let ((tag (nth 0 arg)) + (command (nth 1 arg)) + (visible (nth 2 arg)) + (help (nth 3 arg)) + (active (nth 6 arg))) + (vector tag command :visible (eval visible) + :active `(eq t ',active) + :help help))) + custom-dirlocals-commands))) + +(defvar custom-dirlocals-tool-bar-map nil + "Keymap for the toolbar in \"*Customize Dirlocals*\" buffer.") + +(define-widget 'custom-dirlocals-key 'menu-choice + "Menu to choose between possible keys in a dir-locals file. + +Possible values are nil, a symbol (standing for a major mode) or a directory +name." + :tag "Specification" + :value nil + :help-echo "Select a key for the dir-locals specification." + :args '((const :tag "All modes" nil) + (symbol :tag "Major mode" fundamental-mode) + (directory :tag "Subdirectory"))) + +(define-widget 'custom-dynamic-cons 'cons + "A cons widget that changes its 2nd type based on the 1st type." + :value-create #'custom-dynamic-cons-value-create) + +(defun custom-dynamic-cons-value-create (widget) + "Select an appropriate 2nd type for the cons WIDGET and create WIDGET. + +The appropriate types are: +- A symbol, if the value to represent is a minor-mode. +- A boolean, if the value to represent is either the unibyte value or the + subdirs value. +- A widget type suitable for editing a variable, in case of specifying a + variable's value. +- A sexp widget, if none of the above happens." + (let* ((args (widget-get widget :args)) + (value (widget-get widget :value)) + (val (car value))) + (cond + ((eq val 'mode) (setf (nth 1 args) + '(symbol :keymap custom-dirlocals-field-map + :tag "Minor mode"))) + ((eq val 'unibyte) (setf (nth 1 args) '(boolean))) + ((eq val 'subdirs) (setf (nth 1 args) '(boolean))) + ((custom-variable-p val) + (let ((w (widget-convert (custom-variable-type val)))) + (when (custom--editable-field-p w) + (widget-put w :keymap custom-dirlocals-field-map)) + (setf (nth 1 args) w))) + (t (setf (nth 1 args) '(sexp :keymap custom-dirlocals-field-map)))) + (widget-put (nth 0 args) :keymap custom-dirlocals-field-map) + (widget-group-value-create widget))) + +(defun custom-dirlocals-maybe-update-cons () + "If focusing out from the first widget in a cons widget, update its value." + (when-let ((w (widget-at))) + (when (widget-get w :custom-dirlocals-symbol) + (widget-value-set (widget-get w :parent) + (cons (widget-value w) "")) + (widget-setup)))) + +(define-widget 'custom-dirlocals 'editable-list + "An editable list to edit settings in a dir-locals file." + :entry-format "%i %d %v" + :insert-button-args '(:help-echo "Insert new specification here.") + :append-button-args '(:help-echo "Append new specification here.") + :delete-button-args '(:help-echo "Delete this specification.") + :args '((group :format "%v" + custom-dirlocals-key + (repeat + :tag "Settings" + :inline t + (custom-dynamic-cons + :tag "Setting" + (symbol :action custom-dirlocals-symbol-action + :custom-dirlocals-symbol t) + ;; Will change according to the option being customized. + (sexp :tag "Value")))))) + +(defun custom-dirlocals-symbol-action (widget &optional _event) + "Action for the symbol WIDGET. + +Sets the value of its parent, a cons widget, in order to create an +appropriate widget to edit the value of WIDGET. + +Moves point into the widget that holds the value." + (setq widget (or widget (widget-at))) + (widget-value-set (widget-get widget :parent) + (cons (widget-value widget) "")) + (widget-setup) + (widget-forward 1)) + +(defun custom-dirlocals-change-file (widget &optional _event) + "Switch to a buffer to customize the dir-locals file in WIDGET." + (customize-dirlocals (expand-file-name (widget-value widget)))) + +(defun custom-dirlocals--set-widget-vars () + "Set local variables for the Widget library." + (custom--initialize-widget-variables) + (add-hook 'widget-forward-hook #'custom-dirlocals-maybe-update-cons nil t)) + +(defmacro custom-dirlocals-with-buffer (&rest body) + "Arrange to execute BODY in a \"*Customize Dirlocals*\" buffer." + ;; We don't use `custom-buffer-create' because the settings here + ;; don't go into the `custom-file'. + `(progn + (switch-to-buffer "*Customize Dirlocals*") + (kill-all-local-variables) + (let ((inhibit-read-only t)) + (erase-buffer)) + (remove-overlays) + (custom-dirlocals--set-widget-vars) + ,@body + (setq-local tool-bar-map + (or custom-dirlocals-tool-bar-map + ;; Set up `custom-dirlocals-tool-bar-map'. + (let ((map (make-sparse-keymap))) + (mapc + (lambda (arg) + (tool-bar-local-item-from-menu + (nth 1 arg) (nth 4 arg) map custom-dirlocals-map + :label (nth 5 arg))) + custom-dirlocals-commands) + (setq custom-dirlocals-tool-bar-map map)))) + (setq-local revert-buffer-function #'Custom-dirlocals-revert-buffer) + (use-local-map custom-dirlocals-map) + (widget-setup))) + +(defun custom-dirlocals-get-options () + "Return all options inside a custom-dirlocals widget." + (let* ((groups (widget-get custom-dirlocals-widget :children)) + (repeats (mapcar (lambda (group) + (nth 1 (widget-get group :children))) + groups))) + (mapcan (lambda (repeat) + (mapcar (lambda (w) + (nth 1 (widget-get w :children))) + (widget-get repeat :children))) + repeats))) + +(defun custom-dirlocals-validate () + "Non-nil if all customization options validate. + +If at least an option doesn't validate, signals an error and moves point +to the widget with the invalid value." + (dolist (opt (custom-dirlocals-get-options)) + (when-let ((w (widget-apply opt :validate))) + (goto-char (widget-get w :from)) + (error "%s" (widget-get w :error)))) + t) + +(defun Custom-dirlocals-revert-buffer (&rest _ignored) + "Revert the buffer for Directory Local Variables customization." + (interactive) + (customize-dirlocals (widget-get custom-dirlocals-file-widget :value))) + +(defun Custom-dirlocals-save (&rest _ignore) + "Save the settings to the dir-locals file being customized." + (interactive) + (when (custom-dirlocals-validate) + (let* ((file (widget-value custom-dirlocals-file-widget)) + (old (widget-get custom-dirlocals-widget :value)) + (dirlocals (widget-value custom-dirlocals-widget))) + (dolist (spec old) + (let ((mode (car spec)) + (settings (cdr spec))) + (dolist (setting settings) + (delete-dir-local-variable mode (car setting) file)))) + (dolist (spec dirlocals) + (let ((mode (car spec)) + (settings (cdr spec))) + (dolist (setting (reverse settings)) + (when (memq (car setting) '(mode eval)) + (delete-dir-local-variable mode (car setting) file)) + (add-dir-local-variable mode (car setting) (cdr setting) file))))) + ;; Write the dir-locals file and kill its buffer, to come back to + ;; our own buffer. + (write-file (expand-file-name buffer-file-name) nil) + (kill-buffer))) + +;;;###autoload +(defun customize-dirlocals (&optional filename) + "Customize Directory Local Variables in the current directory. + +With optional argument FILENAME non-nil, customize the `.dir-locals.el' file +that FILENAME specifies." + (interactive) + (let* ((file (or filename (expand-file-name ".dir-locals.el"))) + (dirlocals (when (file-exists-p file) + (with-current-buffer (find-file-noselect file) + (goto-char (point-min)) + (prog1 + (condition-case _ + (read (current-buffer)) + (end-of-file nil)) + (kill-buffer)))))) + (custom-dirlocals-with-buffer + (widget-insert + "This buffer is for customizing the Directory Local Variables in:\n") + (setq custom-dirlocals-file-widget + (widget-create `(file :action ,#'custom-dirlocals-change-file + ,file))) + (widget-insert + (substitute-command-keys + " +To select another file, edit the above field and hit RET. + +After you enter a user option name under the symbol field, +be sure to press \\`RET' or \\`TAB', so that the field that holds the +value changes to an appropriate field for the option. + +Type \\`C-x C-s' when you've finished editing it, to save the +settings to the file.")) + (widget-insert "\n\n\n") + (widget-create 'push-button :tag " Revert " + :action #'Custom-dirlocals-revert-buffer) + (widget-insert " ") + (widget-create 'push-button :tag " Save Settings " + :action #'Custom-dirlocals-save) + (widget-insert "\n\n") + (setq custom-dirlocals-widget + (widget-create 'custom-dirlocals :value dirlocals)) + (setq default-directory (file-name-directory file)) + (goto-char (point-min))))) + (provide 'cus-edit) ;;; cus-edit.el ends here -- 2.34.1