changeset 95113:032aad659f02

Implement Project-local variables.
author Michael Olson <mwolson@gnu.org>
date Mon, 19 May 2008 22:36:09 +0000
parents 0835cb21d60e
children e25c4a7e07a0
files doc/emacs/custom.texi etc/NEWS lisp/files.el
diffstat 3 files changed, 279 insertions(+), 44 deletions(-) [+]
line wrap: on
line diff
--- a/doc/emacs/custom.texi	Mon May 19 18:38:55 2008 +0000
+++ b/doc/emacs/custom.texi	Mon May 19 22:36:09 2008 +0000
@@ -796,6 +796,7 @@
 		          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 @@
 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
--- a/etc/NEWS	Mon May 19 18:38:55 2008 +0000
+++ b/etc/NEWS	Mon May 19 22:36:09 2008 +0000
@@ -180,6 +180,9 @@
 ** 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.
 
--- a/lisp/files.el	Mon May 19 18:38:55 2008 +0000
+++ b/lisp/files.el	Mon May 19 22:36:09 2008 +0000
@@ -1973,6 +1973,8 @@
   (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 @@
 
 (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 @@
 	  (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 @@
 	  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 @@
       ;; 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 @@
          (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