emacs-devel
[Top][All Lists]
Advanced

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

Re: status of dir-vars or dir-locals inclusion in emacs?


From: Tom Tromey
Subject: Re: status of dir-vars or dir-locals inclusion in emacs?
Date: Sun, 29 Jul 2007 16:59:34 -0600
User-agent: Gnus/5.11 (Gnus v5.11) Emacs/22.0.990 (gnu/linux)

>>>>> "rms" == Richard Stallman <address@hidden> writes:

rms> project.el looks like just the right thing.
rms> Could you write changes to etc/NEWS and the Emacs Manual
rms> to describe it?

Sorry for the delay on this.  I've been occupied elsewhere.

I haven't written the patch to Emacs yet but I am sending the updated
project.el.  I'll try to write the documentation patch soon.

I did want to ask whether this is something that should be enabled by
default.  It would be more useful that way, because Emacs would "just
work" to find settings for projects using this feature.

>     ;; - need a way to add to auto-mode-alist per-project
>     ;;   e.g., semantic wants GCC's .def files to be c-mode

rms> That seems secondary -- it is not hard to make files specify
rms> their major modes.

Yeah, good point.  I made a note.

>     ;; - should cache the mod time of settings file and then reload it
>     ;;   or at least offer user a way to invalidate cache

rms> Does that mean, in case the project settings change, it should update
rms> the bindings in the relevant buffers?

This is an interesting idea.  I was thinking of something simpler,
namely that after an update the new project settings should be picked
up for new buffers.

>     ;; - should let the user augment the project settings with personal ones

rms> Isn't this what `project-define-class' does?
rms> How would this be different from that?

Right now there are 2 ways to use project.el.

First, you can create a "settings.el" or ".settings.el" file in a
project's top-level directory.  This file holds a sexp that looks like
the argument to define-project-bindings.  The idea behind this mode of
operation is that you are working on a project that is friendly to
Emacs-using developers, so the Emacs settings are checked in with the
other source files.

The other mode is to explicitly define a project class (say in your
.emacs) using define-project-bindings, and then explicitly tell Emacs
that certain directories hold instances of that project.


The first mode here is only appropriate for shared settings -- things
like indentation settings and the like.  It would not be appropriate
for anything that is per-user -- say, if you wanted to set
compile-history on a per-project basis.

I'm not sure how useful this feature would be.  It depends on how many
settings like this there are.  Most of the ones I tend to use are
transient -- though of course maybe they wouldn't be if I had a
convenient way to save them.

Tom

;;; project.el --- per-project settings

;; Copyright (C) 2007 Tom Tromey <address@hidden>

;; Author: Tom Tromey <address@hidden>
;; Created: 27 Apr 2007
;; Version: 0.2
;; Keywords: tools

;; This file is not (yet) part of GNU Emacs.
;; However, it is distributed under the same license.

;; GNU Emacs is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
;; Boston, MA 02110-1301, USA.

;;; Commentary:

;; This package makes it easy to set variables on a per-mode basis for
;; an entire project.  This is much simpler than editing the local
;; variables section of every file in the project.  Settings can be
;; checked in to the project's version control and they will
;; automatically be found and used by Emacs.

;; To set up:
;;   (add-hook 'find-file-hooks #'project-find-file)

;; A project class can be defined manually using
;; `define-project-bindings'.  Then a directory can be associated with a
;; given class using `set-directory-project'.  This indirection lets
;; you easily share settings between multiple instances of a project,
;; for instance if you have multiple branches checked out.

;; The alist argument to `define-project-bindings' is an alist with a
;; special structure, defined below.

;; When a file is loaded, project.el will search up the directory
;; hierarchy.  If it finds a directory that was registered with
;; `set-directory-project', it will use the corresponding project
;; class.

;; Otherwise, if a directory contains a file `settings.el' or
;; `.settings.el', then the contents of that file are used as an alist
;; (of the type `define-project-bindings' takes).  In this case, the
;; alist is first scrubbed of risky variable settings.

;; The alist referred to above is used to set local variables.  The
;; car of an entry in the alist is one of three things: the major mode
;; (a symbol); `nil', which lists variables applied to every file in
;; the project; or a string, which is a subdirectory of the project.

;; If the car is a mode name or `nil', the cdr is an alist mapping
;; variable names to variable values.

;; If the car is a string, the cdr is another alist of the form above.

;; Here's an example for GCC:

;; (define-project-bindings 'gcc
;;   '((nil . ((indent-tabs-mode . t)
;;          (tab-width . 8)
;;          (fill-column . 80)))
;;     (c-mode . ((style . "GNU")
;;             (new-file-skeleton . (blah))))
;;     (java-mode . ((style . "GNU")))
;;     ("libjava/classpath"
;;      . ((nil . ((change-log-default-name . "ChangeLog.gcj")))))
;; 
;;     ))

;; Note that in general you should only set project-related variables
;; in a settings.el file that is checked in.  Things like key bindings
;; (or electric keys for C mode) should probably not be set by the
;; project.

;;; ToDo:

;; - need a way to add to auto-mode-alist per-project
;;   e.g., semantic wants GCC's .def files to be c-mode
;;   (RMS points out that this is also easily done by adding comments
;;   to the particular files.)
;; - should cache the mod time of settings file and then reload it
;;   or at least offer user a way to invalidate cache
;; - should let the user augment the project settings with personal ones
;; - per-project and per-mode new file skeletons
;; - maybe an easy way to integrate with customize?
;; - make it easy to save per-project user settings like build
;;   commands, gdb commands, etc
;; - let user close a project and close all associated buffers; then
;;   reopen the project and have the buffers open?
;; - any role for bugzilla URLs, patch address and formatting?
;; - got a request to emulate Eclipse's S-C-r binding; maybe integrate
;;   with iswitch-fc or filename cache

;;; Code:

(defvar project-class-alist '()
  "Alist mapping project class names (symbols) to project variable alists.")

(defvar project-directory-alist '()
  "Alist mapping project directory roots to project classes.")

(defsubst project-get-alist (class)
  "Return the project variable alist for project CLASS."
  (cdr (assq class project-class-alist)))

(defun project-install-bindings-from-alist (mode-alist)
  "Apply local variable settings from MODE-ALIST."
  (mapc (lambda (pair)
          (let ((variable (car pair))
                (value (cdr pair)))
            (make-local-variable variable)
            (set variable value)))
        mode-alist))

(defun project-install-bindings (class root)
  "Set variables for the current buffer for the given project class.
CLASS is the project's class, a symbol.
ROOT is the project's root directory, a string.
This applies local variable settings in order from most generic
to most specific."
  (let* ((alist (project-get-alist class))
         (subdir (substring (buffer-file-name) 0 (length root))))
    ;; First use the 'nil' key to get generic variables.
    (project-install-bindings-from-alist (cdr (assq nil alist)))
    ;; Now apply the mode-specific variables.
    (project-install-bindings-from-alist (cdr (assq major-mode alist)))
    ;; Look for subdirectory matches in the class alist and apply
    ;; based on those.
    (mapc (lambda (elt)
            (and (stringp (car elt))
                 (string= (car elt) (substring (buffer-file-name) 0
                                               (length (car elt))))
                 (progn
                   ;; Again both generic and mode-specific.
                   (project-install-bindings-from-alist
                    (cdr (assq nil alist)))
                   (project-install-bindings-from-alist
                    (cdr (assq major-mode alist))))))
          alist)
    ;; Special case C and derived modes.  Note that CC-based modes
    ;; don't work with derived-mode-p.  FIXME: this is arguably an
    ;; Emacs bug.  Perhaps we should be running
    ;; hack-local-variables-hook here instead?
    (and (boundp 'c-buffer-is-cc-mode)
         c-buffer-is-cc-mode
         (c-postprocess-file-styles))))

;;;###autoload
(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."
  (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)))
  (setq project-directory-alist
        (cons (cons directory class)
              project-directory-alist)))

;;;###autoload
(defun define-project-bindings (class alist)
  "Map the project type CLASS to an alist of variable settings.
CLASS is the project class, a symbol.
ALIST is an alist that maps major modes to sub-alists.
Each sub-alist maps variable names to values.

Note that this does not filter risky variables.  This function
is intended for use by trusted code only."
  (let ((elt (assq class project-class-alist)))
    (if elt
        (setcdr elt alist)
      (setq project-class-alist
            (cons (cons class alist)
                  project-class-alist)))))

;; There's a few ways we could do this.  We could use VC (with a VC
;; extension) and look for the root directory.  Or we could chain
;; settings files.  For now we choose a simple approach and let the
;; project maintainers be smart.
(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 "settings.el"))
        (setq result (concat dir "settings.el")))
       ((file-exists-p (concat dir ".settings.el"))
        (setq result (concat dir ".settings.el")))
       (t
        (setq dir (file-name-directory (directory-file-name dir))))))
    result))

;; Taken from Emacs 22.
(defun project-safe-local-variable-p (sym val)
  "Non-nil if SYM is safe as a file-local variable with value VAL.
It is safe if any of these conditions are met:

 * There is a matching entry (SYM . VAL) in the
   `safe-local-variable-values' user option.

 * The `safe-local-variable' property of SYM is a function that
   evaluates to a non-nil value with VAL as an argument."
  (or (member (cons sym val) safe-local-variable-values)
      (let ((safep (get sym 'safe-local-variable)))
        (and (functionp safep) (funcall safep val)))))

(unless (fboundp 'safe-local-variable-p)
  (fset 'safe-local-variable-p 'project-safe-local-variable-p))

(defun project-filter-risky-variables (alist)
  "Filter risky variables from the project settings ALIST.
This knows the expected structure of a project settings alist.
Actually this filters unsafe variables."
  (mapc (lambda (elt)
          (let ((sub-alist (cdr elt)))
            (if (stringp (car sub-alist))
                ;; A string element maps to a secondary alist.
                (setcdr sub-alist
                        (project-filter-risky-variables (cdr sub-alist)))
              ;; Remove unsafe variables by setting their cars to nil.
              ;; FIXME: or look only at risky-local-variable-p?
              (mapc (lambda (sub-elt)
                      (unless (safe-local-variable-p (car sub-elt)
                                                     (cdr sub-elt))
                        (setcar sub-elt nil)))
                    sub-alist)
              ;; Now remove all the deleted risky variables.
              (setcdr elt (assq-delete-all nil sub-alist)))))
        alist)
  alist)

(defun project-define-from-project-file (settings-file)
  "Load a settings file and register a new project class and instance.
The class name is the same as the directory in which the settings file
was found.  The settings have risky local variables filtered out."
  (with-temp-buffer
    (insert-file-contents settings-file)
    (let* ((dir-name (file-name-directory settings-file))
           (class-name (intern dir-name))
           (alist (project-filter-risky-variables (read (current-buffer)))))
      (define-project-bindings class-name alist)
      (set-directory-project dir-name class-name)
      class-name)))

;; Put this on find-file-hooks.
;;;###autoload
(defun project-find-file ()
  "Set local variables in a buffer based on project settings."
  (when (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 class (project-define-from-project-file settings))
        (setq root-dir (file-name-directory (buffer-file-name))))
       ((consp settings)
        (setq root-dir (car settings))
        (setq class (cdr settings))))
      (when class
        (make-local-variable 'project-class)
        (setq project-class class)
        (project-install-bindings class root-dir)))))

;;; project.el ends here




reply via email to

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