From mboxrd@z Thu Jan 1 00:00:00 1970 Path: news.gmane.org!not-for-mail From: Tom Tromey Newsgroups: gmane.emacs.devel Subject: Updated project-specific settings patch Date: Mon, 19 May 2008 11:07:48 -0600 Message-ID: Reply-To: tromey@redhat.com NNTP-Posting-Host: lo.gmane.org Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii X-Trace: ger.gmane.org 1211216991 21805 80.91.229.12 (19 May 2008 17:09:51 GMT) X-Complaints-To: usenet@ger.gmane.org NNTP-Posting-Date: Mon, 19 May 2008 17:09:51 +0000 (UTC) To: emacs-devel@gnu.org Original-X-From: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Mon May 19 19:10:26 2008 Return-path: Envelope-to: ged-emacs-devel@m.gmane.org Original-Received: from lists.gnu.org ([199.232.76.165]) by lo.gmane.org with esmtp (Exim 4.50) id 1Jy8sR-0005sX-B4 for ged-emacs-devel@m.gmane.org; Mon, 19 May 2008 19:10:24 +0200 Original-Received: from localhost ([127.0.0.1]:43659 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1Jy8rh-0005pK-53 for ged-emacs-devel@m.gmane.org; Mon, 19 May 2008 13:09:37 -0400 Original-Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id 1Jy8rD-0005Xc-5M for emacs-devel@gnu.org; Mon, 19 May 2008 13:09:07 -0400 Original-Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id 1Jy8qF-0005A3-L6 for emacs-devel@gnu.org; Mon, 19 May 2008 13:08:09 -0400 Original-Received: from [199.232.76.173] (port=57195 helo=monty-python.gnu.org) by lists.gnu.org with esmtp (Exim 4.43) id 1Jy8qC-00059z-MR for emacs-devel@gnu.org; Mon, 19 May 2008 13:08:04 -0400 Original-Received: from mx1.redhat.com ([66.187.233.31]:55420) by monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from ) id 1Jy8qC-0006JV-17 for emacs-devel@gnu.org; Mon, 19 May 2008 13:08:04 -0400 Original-Received: from int-mx1.corp.redhat.com (int-mx1.corp.redhat.com [172.16.52.254]) by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id m4JH7o8i004292 for ; Mon, 19 May 2008 13:07:50 -0400 Original-Received: from pobox.corp.redhat.com (pobox.corp.redhat.com [10.11.255.20]) by int-mx1.corp.redhat.com (8.13.1/8.13.1) with ESMTP id m4JH7nlP008024; Mon, 19 May 2008 13:07:49 -0400 Original-Received: from opsy.redhat.com (vpn-10-17.bos.redhat.com [10.16.10.17]) by pobox.corp.redhat.com (8.13.1/8.13.1) with ESMTP id m4JH7mja029501; Mon, 19 May 2008 13:07:49 -0400 Original-Received: by opsy.redhat.com (Postfix, from userid 500) id 5AC8E508450; Mon, 19 May 2008 11:07:48 -0600 (MDT) X-Attribution: Tom User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.1 (gnu/linux) X-Scanned-By: MIMEDefang 2.58 on 172.16.52.254 X-detected-kernel: by monty-python.gnu.org: Linux 2.6 (newer, 3) X-BeenThere: emacs-devel@gnu.org X-Mailman-Version: 2.1.5 Precedence: list List-Id: "Emacs development discussions." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Original-Sender: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Errors-To: emacs-devel-bounces+ged-emacs-devel=m.gmane.org@gnu.org Xref: news.gmane.org gmane.emacs.devel:97396 Archived-At: A long while ago I was working on a patch to allow project-specific settings for Emacs. Here's the updated version of this patch. I tried to incorporate all the feedback on previous versions of this patch into the current patch. In particular: * Now a patch to files.el, not a separate file. * Reuses the hack-local-variables code to query the user about risky settings. * Search for directory settings is limited to a single file name, ".dir-settings.el". Tom lisp/ChangeLog: 2008-05-19 Tom Tromey * 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 * custom.texi (Variables): Add Directory Variables to menu. (Directory Variables): New node. etc/ChangeLog: 2008-05-19 Tom Tromey * 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 17:04:22 -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 @@ -2636,20 +2640,22 @@ (offer-save (and (eq enable-local-variables t) unsafe-vars)) prompt char) (save-window-excursion - (let ((buf (get-buffer-create "*Local Variables*"))) + (let ((buf (get-buffer-create (if project "*Directory Variabes*" + "*Local Variables*")))) (pop-to-buffer buf) (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 +2777,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 +2912,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 +3016,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 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 17:04:23 -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 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 17:04:23 -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.