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: Tue, 03 Jul 2007 18:25:11 -0600
User-agent: Gnus/5.11 (Gnus v5.11) Emacs/22.0.990 (gnu/linux)

>>>>> "Joakim" == Joakim Verona <address@hidden> writes:

Joakim> There was discussion some time ago about including either
Joakim> dir-locals.el or dir-vars.el.

Joakim> - What is the status of this?  Which one will be included?

One thing I don't like about dirvars is that, AFAICS, it only allows a
single setting of a variable for a project -- there's no way to have
different settings depending on the major mode.

Here's some code I've been playing with that does the same kind of
thing, in a slightly different way.  You can define a project class
and have it define settings and then define different directories as
instances of a defined class, or you can have a file in the top-level
directory of a project.

I do like how dirvars by default doesn't search upward on remote
directories.  I wish I'd thought of that :-).

Tom

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

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

;; Author: Tom Tromey <address@hidden>
;; Created: 27 Apr 2007
;; Version: 0.1
;; 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.

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

;; A project class can be defined manually using
;; `project-define-class'.  Then a directory can be associated with a
;; given class using `project-define-instance'.  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 `project-define-class' 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
;; `project-define-instance', 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 `project-define-class' 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:

;; (project-define-class '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
;; - 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

;;; 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-set-variables-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-set-variables (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-set-variables-from-alist (cdr (assq nil alist)))
    ;; Now apply the mode-specific variables.
    (project-set-variables-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-set-variables-from-alist
                    (cdr (assq nil alist)))
                   (project-set-variables-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 project-define-instance (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 project-define-class (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)))))
      (project-define-class class-name alist)
      (project-define-instance 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-set-variables class root-dir)))))

;;; project.el ends here




reply via email to

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