emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: Updated project-specific settings patch


From: Tom Tromey
Subject: Re: Updated project-specific settings patch
Date: Mon, 19 May 2008 13:23:42 -0600
User-agent: Gnus/5.11 (Gnus v5.11) Emacs/22.1 (gnu/linux)

>>>>> "Stefan" == Stefan Monnier <address@hidden> writes:

Stefan> Thank you.  It looks good.  I just have a few minor wishes
Stefan> below, but otherwise, feel free to install it.

I don't have write access.  I was told off-list to apply for it, but I
don't feel that comfortable doing so -- my patch acceptance rate has
not really been all that great.  However, if you think I ought to do
that, let me know and I will.

>> +    (let ((buf (get-buffer-create (if project "*Directory Variabes*"
>> +                                    "*Local Variables*"))))

Stefan> Better use the same buffer name *Local Variables*.

I made this change.  And anyway I had a typo in the new buffer name --
oops.

>> +       ((file-exists-p (concat dir ".dir-settings.el"))
>> +    (setq result (concat dir ".dir-settings.el")))

Stefan> In most cases, we'd prefer (expand-file-name ".dir-settings.el" dir)
Stefan> over the use of `concat'.  The only exception I know is when `dir' is
Stefan> relative and it's important for the result also be relative.

No problem.

New patch appended.

Stefan> One more thing: since we want to include CEDET and CEDET has its own
Stefan> notion of "project data", we want to try and get those two things
Stefan> to converge.

Stefan> So please try and work out with address@hidden what convergence might
Stefan> look like.

I've CC'd him.

I looked through CEDET a bit but project-local settings do not seem to
be extensively documented.  I think you have to use EDE to get them,
and from what I can see EDE only supports a couple types of projects.

Also, it appears that a Project.ede is needed in every directory.

Confirmation (or anti-confirmation) of these assertions would be
helpful.

Tom

lisp/ChangeLog:
2008-05-19  Tom Tromey  <address@hidden>

        * files.el (normal-mode): Call hack-project-variables.
        (hack-local-variables-confirm): Add 'project' argument.
        (hack-local-variables-apply): New function.
        (hack-local-variables): Use it.
        (project-class-alist, project-directory-alist): New variables.
        (project-get-alist): New function.
        (project-collect-bindings-from-alist)
        (project-collect-binding-list, set-directory-project)
        (project-find-settings-file, project-define-from-project-file)
        (hack-project-variables): New functions.

doc/emacs/ChangeLog:
2008-05-19  Tom Tromey  <address@hidden>

        * custom.texi (Variables): Add Directory Variables to menu.
        (Directory Variables): New node.

etc/ChangeLog:
2008-05-19  Tom Tromey  <address@hidden>

        * NEWS: Mention directory-local variables.

Index: lisp/files.el
===================================================================
RCS file: /sources/emacs/emacs/lisp/files.el,v
retrieving revision 1.976
diff -u -r1.976 files.el
--- lisp/files.el       6 May 2008 07:57:35 -0000       1.976
+++ lisp/files.el       19 May 2008 19:20:46 -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 (expand-file-name ".dir-settings.el" dir))
+       (setq result (expand-file-name ".dir-settings.el" dir)))
+       (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
Index: doc/emacs/custom.texi
===================================================================
RCS file: /sources/emacs/emacs/doc/emacs/custom.texi,v
retrieving revision 1.9
diff -u -r1.9 custom.texi
--- doc/emacs/custom.texi       5 Apr 2008 23:01:19 -0000       1.9
+++ doc/emacs/custom.texi       19 May 2008 19:20:47 -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.
 
address@hidden Directory Variables
address@hidden Per-Directory Local Variables
address@hidden local variables in directories
address@hidden 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
address@hidden, 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
address@hidden 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.
+
address@hidden
+((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"))))))
address@hidden 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
address@hidden  You can put calls to these functions in
+your @file{.emacs}; this can useful when you can't put
address@hidden in the directory for some reason.  For
+example, you could apply settings to an unwriteable directory this
+way:
+
address@hidden
+(define-project-bindings 'unwriteable-directory
+   '((nil . ((some-useful-setting . value)))))
+
+(set-directory-project "/usr/include/" 'unwriteable-directory)
address@hidden 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
Index: etc/NEWS
===================================================================
RCS file: /sources/emacs/emacs/etc/NEWS,v
retrieving revision 1.1746
diff -u -r1.1746 NEWS
--- etc/NEWS    15 May 2008 07:26:32 -0000      1.1746
+++ etc/NEWS    19 May 2008 19:20:48 -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.
 




reply via email to

[Prev in Thread] Current Thread [Next in Thread]