mirror of
https://git.savannah.gnu.org/git/emacs.git
synced 2025-01-01 11:14:55 +00:00
Implement Project-local variables.
This commit is contained in:
parent
ad97b375e8
commit
1b21ee065d
@ -796,6 +796,7 @@ here we describe other aspects of Emacs variables.
|
||||
of Emacs to run on particular occasions.
|
||||
* Locals:: Per-buffer values of variables.
|
||||
* File Variables:: How files can specify variable values.
|
||||
* Directory Variables:: How variable values can be specified by directory.
|
||||
@end menu
|
||||
|
||||
@node Examining
|
||||
@ -1262,6 +1263,65 @@ customizable list of eval forms which are safe. Emacs does not ask
|
||||
for confirmation when it finds these forms for the @code{eval}
|
||||
variable.
|
||||
|
||||
@node Directory Variables
|
||||
@subsection Per-Directory Local Variables
|
||||
@cindex local variables in directories
|
||||
@cindex directory local variables
|
||||
|
||||
Emacs provides a way to specify local variable values per-directory.
|
||||
This can be done one of two ways.
|
||||
|
||||
The first approach is to put a special file, named
|
||||
@file{.dir-settings.el}, in a directory. When opening a file, Emacs
|
||||
searches for @file{.dir-settings.el} starting in the file's directory
|
||||
and then moving up the directory hierarchy. If
|
||||
@file{.dir-settings.el} is found, Emacs applies variable settings from
|
||||
the file to the new buffer. If the file is remote, Emacs skips this
|
||||
search, because it would be too slow.
|
||||
|
||||
The file should hold a specially-constructed list. This list maps
|
||||
Emacs mode names (symbols) to alists; each alist maps variable names
|
||||
to values. The special mode name @samp{nil} means that the alist
|
||||
should be applied to all buffers. Finally, a string key can be used
|
||||
to specify an alist which applies to a relative subdirectory in the
|
||||
project.
|
||||
|
||||
@example
|
||||
((nil . ((indent-tabs-mode . t)
|
||||
(tab-width . 4)
|
||||
(fill-column . 80)))
|
||||
(c-mode . ((c-file-style . "BSD")))
|
||||
(java-mode . ((c-file-style . "BSD")))
|
||||
("src/imported"
|
||||
. ((nil . ((change-log-default-name . "ChangeLog.local"))))))
|
||||
@end example
|
||||
|
||||
This example shows some settings for a hypothetical project. This
|
||||
sets @samp{indent-tabs-mode} to @samp{t} for any file in the source
|
||||
tree, and it sets the indentation style for any C or Java source file
|
||||
to @samp{BSD}. Finally, it specifies a different @file{ChangeLog}
|
||||
file name for any file in the project that appears beneath the
|
||||
directory @file{src/imported}.
|
||||
|
||||
The second approach to directory-local settings is to explicitly
|
||||
define a project class using @code{define-project-bindings}, and then
|
||||
to tell Emacs which directory roots correspond to that class, using
|
||||
@code{set-directory-project}. You can put calls to these functions in
|
||||
your @file{.emacs}; this can useful when you can't put
|
||||
@file{.dir-settings.el} in the directory for some reason. For
|
||||
example, you could apply settings to an unwriteable directory this
|
||||
way:
|
||||
|
||||
@example
|
||||
(define-project-bindings 'unwriteable-directory
|
||||
'((nil . ((some-useful-setting . value)))))
|
||||
|
||||
(set-directory-project "/usr/include/" 'unwriteable-directory)
|
||||
@end example
|
||||
|
||||
Unsafe directory-local variables are handled in the same way as
|
||||
unsafe file-local variables.
|
||||
|
||||
@node Key Bindings
|
||||
@section Customizing Key Bindings
|
||||
@cindex key bindings
|
||||
|
3
etc/NEWS
3
etc/NEWS
@ -180,6 +180,9 @@ run processes remotely.
|
||||
** The new command `display-time-world' starts an updating time display
|
||||
using several time zones, in a buffer.
|
||||
|
||||
** Directory-local variables are now found in .dir-settings.el. See
|
||||
also `set-directory-project' and `define-project-bindings'.
|
||||
|
||||
** The new function `format-seconds' converts a number of seconds into a
|
||||
readable string of days, hours, etc.
|
||||
|
||||
|
260
lisp/files.el
260
lisp/files.el
@ -1973,6 +1973,8 @@ in that case, this function acts as if `enable-local-variables' were t."
|
||||
(let ((enable-local-variables (or (not find-file) enable-local-variables)))
|
||||
(report-errors "File mode specification error: %s"
|
||||
(set-auto-mode))
|
||||
(report-errors "Project local-variables error: %s"
|
||||
(hack-project-variables))
|
||||
(report-errors "File local-variables error: %s"
|
||||
(hack-local-variables)))
|
||||
;; Turn font lock off and on, to make sure it takes account of
|
||||
@ -2623,11 +2625,13 @@ asking you for confirmation."
|
||||
|
||||
(put 'c-set-style 'safe-local-eval-function t)
|
||||
|
||||
(defun hack-local-variables-confirm (all-vars unsafe-vars risky-vars)
|
||||
(defun hack-local-variables-confirm (all-vars unsafe-vars risky-vars project)
|
||||
"Get confirmation before setting up local variable values.
|
||||
ALL-VARS is the list of all variables to be set up.
|
||||
UNSAFE-VARS is the list of those that aren't marked as safe or risky.
|
||||
RISKY-VARS is the list of those that are marked as risky."
|
||||
RISKY-VARS is the list of those that are marked as risky.
|
||||
PROJECT is a directory name if these settings come from directory-local
|
||||
settings; nil otherwise."
|
||||
(if noninteractive
|
||||
nil
|
||||
(let ((name (if buffer-file-name
|
||||
@ -2641,15 +2645,16 @@ RISKY-VARS is the list of those that are marked as risky."
|
||||
(set (make-local-variable 'cursor-type) nil)
|
||||
(erase-buffer)
|
||||
(if unsafe-vars
|
||||
(insert "The local variables list in " name
|
||||
(insert "The local variables list in " (or project name)
|
||||
"\ncontains values that may not be safe (*)"
|
||||
(if risky-vars
|
||||
", and variables that are risky (**)."
|
||||
"."))
|
||||
(if risky-vars
|
||||
(insert "The local variables list in " name
|
||||
(insert "The local variables list in " (or project name)
|
||||
"\ncontains variables that are risky (**).")
|
||||
(insert "A local variables list is specified in " name ".")))
|
||||
(insert "A local variables list is specified in "
|
||||
(or project name) ".")))
|
||||
(insert "\n\nDo you want to apply it? You can type
|
||||
y -- to apply the local variables list.
|
||||
n -- to ignore the local variables list.")
|
||||
@ -2771,6 +2776,50 @@ and VAL is the specified value."
|
||||
mode-specified
|
||||
result))))
|
||||
|
||||
(defun hack-local-variables-apply (result project)
|
||||
"Apply an alist of local variable settings.
|
||||
RESULT is the alist.
|
||||
Will query the user when necessary."
|
||||
(dolist (ignored ignored-local-variables)
|
||||
(setq result (assq-delete-all ignored result)))
|
||||
(if (null enable-local-eval)
|
||||
(setq result (assq-delete-all 'eval result)))
|
||||
(when result
|
||||
(setq result (nreverse result))
|
||||
;; Find those variables that we may want to save to
|
||||
;; `safe-local-variable-values'.
|
||||
(let (risky-vars unsafe-vars)
|
||||
(dolist (elt result)
|
||||
(let ((var (car elt))
|
||||
(val (cdr elt)))
|
||||
;; Don't query about the fake variables.
|
||||
(or (memq var '(mode unibyte coding))
|
||||
(and (eq var 'eval)
|
||||
(or (eq enable-local-eval t)
|
||||
(hack-one-local-variable-eval-safep
|
||||
(eval (quote val)))))
|
||||
(safe-local-variable-p var val)
|
||||
(and (risky-local-variable-p var val)
|
||||
(push elt risky-vars))
|
||||
(push elt unsafe-vars))))
|
||||
(if (eq enable-local-variables :safe)
|
||||
;; If caller wants only the safe variables,
|
||||
;; install only them.
|
||||
(dolist (elt result)
|
||||
(unless (or (member elt unsafe-vars)
|
||||
(member elt risky-vars))
|
||||
(hack-one-local-variable (car elt) (cdr elt))))
|
||||
;; Query, except in the case where all are known safe
|
||||
;; if the user wants no query in that case.
|
||||
(if (or (and (eq enable-local-variables t)
|
||||
(null unsafe-vars)
|
||||
(null risky-vars))
|
||||
(eq enable-local-variables :all)
|
||||
(hack-local-variables-confirm
|
||||
result unsafe-vars risky-vars project))
|
||||
(dolist (elt result)
|
||||
(hack-one-local-variable (car elt) (cdr elt))))))))
|
||||
|
||||
(defun hack-local-variables (&optional mode-only)
|
||||
"Parse and put into effect this buffer's local variables spec.
|
||||
If MODE-ONLY is non-nil, all we do is check whether the major mode
|
||||
@ -2862,45 +2911,7 @@ is specified, returning t if it is specified."
|
||||
;; variables (if MODE-ONLY is nil.)
|
||||
(if mode-only
|
||||
result
|
||||
(dolist (ignored ignored-local-variables)
|
||||
(setq result (assq-delete-all ignored result)))
|
||||
(if (null enable-local-eval)
|
||||
(setq result (assq-delete-all 'eval result)))
|
||||
(when result
|
||||
(setq result (nreverse result))
|
||||
;; Find those variables that we may want to save to
|
||||
;; `safe-local-variable-values'.
|
||||
(let (risky-vars unsafe-vars)
|
||||
(dolist (elt result)
|
||||
(let ((var (car elt))
|
||||
(val (cdr elt)))
|
||||
;; Don't query about the fake variables.
|
||||
(or (memq var '(mode unibyte coding))
|
||||
(and (eq var 'eval)
|
||||
(or (eq enable-local-eval t)
|
||||
(hack-one-local-variable-eval-safep
|
||||
(eval (quote val)))))
|
||||
(safe-local-variable-p var val)
|
||||
(and (risky-local-variable-p var val)
|
||||
(push elt risky-vars))
|
||||
(push elt unsafe-vars))))
|
||||
(if (eq enable-local-variables :safe)
|
||||
;; If caller wants only the safe variables,
|
||||
;; install only them.
|
||||
(dolist (elt result)
|
||||
(unless (or (member elt unsafe-vars)
|
||||
(member elt risky-vars))
|
||||
(hack-one-local-variable (car elt) (cdr elt))))
|
||||
;; Query, except in the case where all are known safe
|
||||
;; if the user wants no quuery in that case.
|
||||
(if (or (and (eq enable-local-variables t)
|
||||
(null unsafe-vars)
|
||||
(null risky-vars))
|
||||
(eq enable-local-variables :all)
|
||||
(hack-local-variables-confirm
|
||||
result unsafe-vars risky-vars))
|
||||
(dolist (elt result)
|
||||
(hack-one-local-variable (car elt) (cdr elt)))))))
|
||||
(hack-local-variables-apply result nil)
|
||||
(run-hooks 'hack-local-variables-hook)))))
|
||||
|
||||
(defun safe-local-variable-p (sym val)
|
||||
@ -3004,6 +3015,167 @@ already the major mode."
|
||||
(if (stringp val)
|
||||
(set-text-properties 0 (length val) nil val))
|
||||
(set (make-local-variable var) val))))
|
||||
|
||||
;;; Handling directory local variables, aka project settings.
|
||||
|
||||
(defvar project-class-alist '()
|
||||
"Alist mapping project class names (symbols) to project variable lists.")
|
||||
|
||||
(defvar project-directory-alist '()
|
||||
"Alist mapping project directory roots to project classes.")
|
||||
|
||||
(defsubst project-get-alist (class)
|
||||
"Return the project variable list for project CLASS."
|
||||
(cdr (assq class project-class-alist)))
|
||||
|
||||
(defun project-collect-bindings-from-alist (mode-alist settings)
|
||||
"Collect local variable settings from MODE-ALIST.
|
||||
SETTINGS is the initial list of bindings.
|
||||
Returns the new list."
|
||||
(dolist (pair mode-alist settings)
|
||||
(let* ((variable (car pair))
|
||||
(value (cdr pair))
|
||||
(slot (assq variable settings)))
|
||||
(if slot
|
||||
(setcdr slot value)
|
||||
;; Need a new cons in case we setcdr later.
|
||||
(push (cons variable value) settings)))))
|
||||
|
||||
(defun project-collect-binding-list (binding-list root settings)
|
||||
"Collect entries from BINDING-LIST into SETTINGS.
|
||||
ROOT is the root directory of the project.
|
||||
Return the new settings list."
|
||||
(let* ((file-name (buffer-file-name))
|
||||
(sub-file-name (if file-name
|
||||
(substring file-name (length root)))))
|
||||
(dolist (entry binding-list settings)
|
||||
(let ((key (car entry)))
|
||||
(cond
|
||||
((stringp key)
|
||||
;; Don't include this in the previous condition, because we
|
||||
;; want to filter all strings before the next condition.
|
||||
(when (and sub-file-name
|
||||
(>= (length sub-file-name) (length key))
|
||||
(string= key (substring sub-file-name 0 (length key))))
|
||||
(setq settings (project-collect-binding-list (cdr entry)
|
||||
root settings))))
|
||||
((or (not key)
|
||||
(derived-mode-p key))
|
||||
(setq settings (project-collect-bindings-from-alist (cdr entry)
|
||||
settings))))))))
|
||||
|
||||
(defun set-directory-project (directory class)
|
||||
"Declare that the project rooted at DIRECTORY is an instance of CLASS.
|
||||
DIRECTORY is the name of a directory, a string.
|
||||
CLASS is the name of a project class, a symbol.
|
||||
|
||||
When a file beneath DIRECTORY is visited, the mode-specific
|
||||
settings from CLASS will be applied to the buffer. The settings
|
||||
for a class are defined using `define-project-bindings'."
|
||||
(setq directory (file-name-as-directory (expand-file-name directory)))
|
||||
(unless (assq class project-class-alist)
|
||||
(error "No such project class `%s'" (symbol-name class)))
|
||||
(push (cons directory class) project-directory-alist))
|
||||
|
||||
(defun define-project-bindings (class list)
|
||||
"Map the project type CLASS to a list of variable settings.
|
||||
CLASS is the project class, a symbol.
|
||||
LIST is a list that declares variable settings for the class.
|
||||
An element in LIST is either of the form:
|
||||
(MAJOR-MODE . ALIST)
|
||||
or
|
||||
(DIRECTORY . LIST)
|
||||
|
||||
In the first form, MAJOR-MODE is a symbol, and ALIST is an alist
|
||||
whose elements are of the form (VARIABLE . VALUE).
|
||||
|
||||
In the second form, DIRECTORY is a directory name (a string), and
|
||||
LIST is a list of the form accepted by the function.
|
||||
|
||||
When a file is visited, the file's class is found. A directory
|
||||
may be assigned a class using `set-directory-project'. Then
|
||||
variables are set in the file's buffer according to the class'
|
||||
LIST. The list is processed in order.
|
||||
|
||||
* If the element is of the form (MAJOR-MODE . ALIST), and the
|
||||
buffer's major mode is derived from MAJOR-MODE (as determined
|
||||
by `derived-mode-p'), then all the settings in ALIST are
|
||||
applied. A MAJOR-MODE of nil may be used to match any buffer.
|
||||
`make-local-variable' is called for each variable before it is
|
||||
set.
|
||||
|
||||
* If the element is of the form (DIRECTORY . LIST), and DIRECTORY
|
||||
is an initial substring of the file's directory, then LIST is
|
||||
applied by recursively following these rules."
|
||||
(let ((elt (assq class project-class-alist)))
|
||||
(if elt
|
||||
(setcdr elt list)
|
||||
(push (cons class list) project-class-alist))))
|
||||
|
||||
(defun project-find-settings-file (file)
|
||||
"Find the settings file for FILE.
|
||||
This searches upward in the directory tree.
|
||||
If a settings file is found, the file name is returned.
|
||||
If the file is in a registered project, a cons from
|
||||
`project-directory-alist' is returned.
|
||||
Otherwise this returns nil."
|
||||
(let ((dir (file-name-directory file))
|
||||
(result nil))
|
||||
(while (and (not (string= dir "/"))
|
||||
(not result))
|
||||
(cond
|
||||
((setq result (assoc dir project-directory-alist))
|
||||
;; Nothing else.
|
||||
nil)
|
||||
((file-exists-p (concat dir ".dir-settings.el"))
|
||||
(setq result (concat dir ".dir-settings.el")))
|
||||
(t
|
||||
(setq dir (file-name-directory (directory-file-name dir))))))
|
||||
result))
|
||||
|
||||
(defun project-define-from-project-file (settings-file)
|
||||
"Load a settings file and register a new project class and instance.
|
||||
SETTINGS-FILE is the name of the file holding the settings to apply.
|
||||
The new class name is the same as the directory in which SETTINGS-FILE
|
||||
is found. Returns the new class name."
|
||||
(with-temp-buffer
|
||||
;; We should probably store the modtime of SETTINGS-FILE and then
|
||||
;; reload it whenever it changes.
|
||||
(insert-file-contents settings-file)
|
||||
(let* ((dir-name (file-name-directory settings-file))
|
||||
(class-name (intern dir-name))
|
||||
(list (read (current-buffer))))
|
||||
(define-project-bindings class-name list)
|
||||
(set-directory-project dir-name class-name)
|
||||
class-name)))
|
||||
|
||||
(defun hack-project-variables ()
|
||||
"Set local variables in a buffer based on project settings."
|
||||
(when (and (buffer-file-name) (not (file-remote-p (buffer-file-name))))
|
||||
;; Find the settings file.
|
||||
(let ((settings (project-find-settings-file (buffer-file-name)))
|
||||
(class nil)
|
||||
(root-dir nil))
|
||||
(cond
|
||||
((stringp settings)
|
||||
(setq root-dir (file-name-directory (buffer-file-name)))
|
||||
(setq class (project-define-from-project-file settings)))
|
||||
((consp settings)
|
||||
(setq root-dir (car settings))
|
||||
(setq class (cdr settings))))
|
||||
(when class
|
||||
(let ((bindings
|
||||
(project-collect-binding-list (project-get-alist class)
|
||||
root-dir nil)))
|
||||
(when bindings
|
||||
(hack-local-variables-apply bindings root-dir)
|
||||
;; Special case C and derived modes. Note that CC-based
|
||||
;; modes don't work with derived-mode-p. In general I
|
||||
;; think modes could use an auxiliary method which is
|
||||
;; called after local variables are hacked.
|
||||
(and (boundp 'c-buffer-is-cc-mode)
|
||||
c-buffer-is-cc-mode
|
||||
(c-postprocess-file-styles))))))))
|
||||
|
||||
|
||||
(defcustom change-major-mode-with-file-name t
|
||||
|
Loading…
Reference in New Issue
Block a user