Previous Section  < Day Day Up >  Next Section

11.4. Building an Automatic Template System

You're probably starting to see how all these tools can be put together in really powerful ways. Most of the rest of the chapter consists of examples of building relatively real and useful new features for Emacs. You can use them as learning tools for how to build your own, and you may be able to use them as-is, or with a little tweaking, in your own daily work.

The example we're about to look at is something that one of the authors developed over a decade ago to help with the tedium of creating new files in development projects where a certain amount of structure and standard documentation were always needed. Many coding and writing projects have this characteristic; each file needs some boilerplate, but it needs to be adjusted to the details of the file. Emacs turned out to be very much up to the task of automating a lot of the drudge work, and this template system has been heavily used ever since.

Most of the code in this example should already make sense to you. A couple of aspects that will be explained more thoroughly in the next section about programming a major mode. In particular, don't worry too much yet about exactly what a "hook" function is, or funcall. For now it's sufficient to know that the file-not-found-hook allows us to run code when the user uses find-file to open a file that doesn't exist yet (exactly the time at which we'd like to offer our template services).

Before launching into the code, it's worth looking at an example of it in action. You'd set up your template by creating a file named file-template-java at the top level of a Java project directory hierarchy, containing something like the code shown in Example 11-2.

Example 11-2. file-template-java
/* %filename%

 * Created on %date%

 *

 * (c) 2004 MyCorp, etc. etc.

 */

 

%package%



import org.apache.log4j.Logger;



/**

 * [Documentation Here!]

 *

 * @author  %author%

 * @version $Id: ch11.xml,v 1.3 2004/12/14 16:55:39 kend Exp kend $

 *

 **/

public class %class% {

    

    /**

     * Provides access to the CVS version of this class.

     **/

    public static final String VERSION =

        "$Id: ch11.xml,v 1.3 2004/12/14 16:55:39 kend Exp kend $";



    /**

     * Provides hierarchical control and configuration of debugging via

     * class package structure.

     **/

    private static Logger log =

        Logger.getLogger(%class%.class); 



}

The template system shown in Example 11-3 causes an attempt to find a nonexistent Java source file within this project hierarchy (for example, via C-x C-f src/com/mycorp/util/FooManager.java) to result in the prompt Start with template file? (y or n) in the minibuffer, and if you answer y, you'll see your FooManager.java buffer start out with contents in the following example.

Example 11-3. FooManager.java
/* FooManager.java

 * Created on Sun Nov  9 20:56:12 2003

 *

 * (c) 2004 MyCorp, etc. etc.

 */

 

package com.mycorp.util;



import org.apache.log4j.Logger;



/**

 * [Documentation Here!]

 *

 * @author  Jim Elliott

 * @version $Id: ch11.xml,v 1.3 2004/12/14 16:55:39 kend Exp kend $

 *

 **/

public class FooManager {

    

    /**

     * Provides access to the CVS version of this class.

     **/

    public static final String VERSION =

        "$Id: ch11.xml,v 1.3 2004/12/14 16:55:39 kend Exp kend $";



    /**

     * Provides hierarchical control and configuration of debugging via

     * class package structure.

     **/

    private static Logger log =

        Logger.getLogger(FooManager.class); 



}

The template has been used to populate the buffer with the standard project header comments and a basic Java class skeleton, with proper contextual values filled in (such as the current time, the person creating the file, the file and class name, and so on). Even the Java package statement has been inferred by examining the directory path in which the source file is being created. The Logger declaration will look familiar to anyone who uses the excellent log4j system to add logging and debugging to their Java projects. (The strange version numbers in "$Id" strings are managed by the CVS version control system and will be updated to the proper file and version information when it's checked in. This topic is discussed in Chapter 12.)

To make this work, the template system needs to be able to do a couple of things:

  • Intercept the user's attempt to find a nonexistent file.

  • Check whether there is an appropriate template file somewhere in a parent directory.

  • If so, offer to use it, and populate the buffer with the contents of the template file.

  • Scan the template file for special placeholders (such as %filename%) and replace them with information about the file being created.

Let's look at the source code that makes this all happen! (As always, if you don't want to type the code listed in Example 11-4 yourself, you can download it from this book's web site.[7])

[7] The version presented in this example is simplified for reasons of space and clarity. The full version, which adds the ability to insert templates for function definitions and process arbitrary Emacs Lisp functions within template files, is also available for download.

Example 11-4. template.el

;;;;;;;;;;;;;;;;;;;;;;;;;;; -*- Mode: Emacs-Lisp -*- ;;;;;;;;;;;;;;;;;;;;;;;;

;; template.el --- Routines for generating smart skeletal templates for files.



(defvar template-file-name "file-template"

  "*The name of the file to look for when a find-file request fails. If a

file with the name specified by this variable exists, offer to use it as

a template for creating the new file. You can also have mode-specific

templates by appending \"-extension\" to this filename, e.g. a Java specific

template would be file-template-java.")



(defvar template-replacements-alist

  '(("%filename%" . (lambda ( )

                      (file-name-nondirectory (buffer-file-name))))

    ("%creator%" . user-full-name)

    ("%author%" . user-full-name)

    ("%date%" . current-time-string)

    ("%once%" . (lambda ( ) (template-insert-include-once)))

    ("%package%" . (lambda ( ) (template-insert-java-package)))

    ("%class%" . (lambda ( ) (template-insert-class-name)))

   )

  "A list which specifies what substitutions to perform upon loading a

template file. Each list element consists of a string, which is the target

to be replaced if it is found in the template, paired with a function,

which is called to generate the replacement value for the string.")



(defun find-template-file ( )

  "Searches the current directory and its parents for a file matching

the name configured for template files. The name of the first such

readable file found is returned, allowing for hierarchical template

configuration. A template file with the same extension as the file

being loaded (using a \"-\" instead of a \".\" as the template file's

delimiter, to avoid confusing other software) will take precedence

over an extension-free, generic template."

  (let ((path (file-name-directory (buffer-file-name)))

        (ext (file-name-extension (buffer-file-name)))

        attempt result)



    (while (and (not result) (> (length path) 0))

      (setq attempt (concat path template-file-name "-" ext))

      (if (file-readable-p attempt)

          (setq result attempt)

        (setq attempt (concat path template-file-name))

        (if (file-readable-p attempt)

            (setq result attempt)

          (setq path (if (string-equal path "/")

                         ""

                       (file-name-directory (substring path 0 -1)))))))

    result))



(defun template-file-not-found-hook ( )

  "Called when a find-file command has not been able to find the specified

file in the current directory. Sees if it makes sense to offer to start it

based on a template."

  (condition-case nil

      (if (and (find-template-file)

               (y-or-n-p "Start with template file? "))

          (progn (buffer-disable-undo)

                 (insert-file (find-template-file))

                 (goto-char (point-min))



                 ;; Magically do the variable substitutions

                 (let ((the-list template-replacements-alist))

                   (while the-list

                     (goto-char (point-min))

                     (replace-string (car (car the-list))

                                     (funcall (cdr (car the-list)))

                                     nil)

                     (setq the-list (cdr the-list))))

                 (goto-char (point-min))

                 (buffer-enable-undo)

                 (set-buffer-modified-p nil)))

    ;; This is part of the condition-case; it catches the situation where

    ;; the user has hit C-g to abort the find-file (since they realized

    ;; that they didn't mean it) and deletes the buffer that has already

    ;; been created to go with that file, since it will otherwise become

    ;; mysterious clutter they may not even know about.

    ('quit (kill-buffer (current-buffer))

          (signal 'quit "Quit"))))



; Install the above routine

(or (memq 'template-file-not-found-hook find-file-not-found-hooks)

      (setq find-file-not-found-hooks

            (append find-file-not-found-hooks '(template-file-not-found-hook)))

)



 (defun template-insert-include-once ( )

  "Returns preprocessor directives such that the file will be included

only once during a compilation process which includes it an

abitrary number of times."

  (let ((name (file-name-nondirectory (buffer-file-name)))

        basename)

    (if (string-match ".h$" name)

        (progn

          (setq basename (upcase (substring name 0 -2)))

          (concat "#ifndef _H_" basename "\n#define _H_" basename

                  "\n\n\n#endif   /* not defined _H_" basename " */\n"))

      "" ; the "else" clause, returns an empty string.

    )))



(defun template-insert-java-package ( )

  "Inserts an appropriate Java package directive based on the path to

the current file name (assuming that it is in the com, org or net

subtree). If no recognizable package path is found, inserts nothing."

  (let ((name (file-name-directory (buffer-file-name)))

        result)

    (if (string-match "/\\(com\\|org\\|net\\)/.*/$" name)

        (progn

          (setq result (substring name (+ (match-beginning 0) 1)

                                  (- (match-end 0) 1)))

          (while (string-match "/" result)

            (setq result (concat (substring result 0 (match-beginning 0))

                                 "."

                                 (substring result (match-end 0)))))

          (concat "package " result ";"))

      "")))



(defun template-insert-class-name ( )

  "Inserts the name of the java class being defined in the current file,

based on the file name. If not a Java source file, inserts nothing."

  (let ((name (file-name-nondirectory (buffer-file-name))))

    (if (string-match "\\(.*\\)\\.java" name)

 $" name(substring namee(match-beginning 1)e(match-end 1))

 $" na"")))



(providee'template)

You'll noticeethatethis codeemakes heavy useeofethe regular expression facilities, which is no surprise. The first section sets up someevariablesethateconfigureetheeoperationeofethe template system. template-file-name determinesetheefile namee(or prefix)ethat is usedeto search for templates;etheedefaultevalueeof file-template is probably fine. template-replacements-alist sets upethe standardeplaceholders, andetheemechanism by whichethey get replaced by appropriateevalues. Adding entriesetoethis list is one wayeto extendetheesystem. Each entryeconsistseofetheeplaceholderetoebe replaced, followed byetheeLisp functionetoebe executedeto produceeits replacement. The wayethis functionecanebe storedeinea list and executed when appropriateelater is oneeofetheegreatethings abouteLisp and is discussedeinemoreedeptheinetheecalculatoremode exampleeinethe next section. Theeplaceholders supported are:


%filename%

Gets replaced byetheenameeofetheefileebeing created.


%creator%, %author%

These areesynonyms;eboth get replaced byetheenameeofethe user creating theefile.


%date%

Turnseintoetheecurrent date andetime when theefile is created.


%once%

Expandseintoeboilerplate code for theeC preprocessor toecause a headerefile toeincludeeitself only once, even if it'sebeeneincludedemultipleetimeseby othereheader files. (This sorteofething hasebeenetakenecareeofeinemoreemodern environments likeeObjectiveeC andeJavaebutecanestillebe handy when working withetraditionaleC compilers.)


%package%

Is replaced byetheeJavaepackage whichecontainsetheefileebeing created (assuming theefile is aeJava class). This package is determined by examining theedirectoryestructureein which theefile isebeing placed.


%class%

BecomesetheenameeofetheeJava classebeingedefinedeinetheefile, assuming it's aeJava sourceefile.

The first function, find-template-file, is responsible for searching theedirectoryehierarchy aboveetheefileebeing created, looking for aefile withetheerightenameetoebeeconsidered aefile template (if template-file-name has beenelefteateitsedefaultevalue,ethis looks for either aefileenamed file-template or file-template-ext whereeext isetheeextension atetheeendeofetheenameeofetheefileebeing created). Itejust keeps lopping theelastedirectoryeoffetheepathein which it'selooking,estarting with theelocationeofetheenew file, andeseeing if itecaneread aefile with oneeofethoseenameseinethe currentedirectory, until iteruns outeofedirectories.

The function template-file-not-found-hook isethe "main program"eofetheetemplate system. Itegets "hookedein"etoethe normaleEmacs find-file process, and called whenever find-file doesn'tefindetheefileethe user asked for (in other words, aenewefile isebeing created). It uses condition-case (aemechanism similareto exception handling in C++ andeJava)etoemake sureeitegets aechanceeto clean upeaftereitself if the userecancelsethe processeofefilling in theetemplateefile. It checks whether theetemplateefileecanebe found, asks users if they wanteto useeit, ande(if they do)eloadseiteintoethe newebuffer andeperformsetheeplaceholderesubstitutions. For an explanationeofethe list manipulation and funcall codeethatemakesetheesubstitutions work,ereadethe discussioneofeCalculatoremodeeinetheenext section. Finally, itejumps toetheebeginningeofethe newebuffer andemarkseiteas unchangede(because,eas far as users areeconcerned, it's aebrand newebuffer on which they've noteyetehadeto expendeany effort).

Immediatelyeafterethe functionedefinitioneisetheechunkeofecodeethat hookseiteintoethe find-file mechanism. The file-not-found-hooks iseaevariableethateEmacs uses to keepetrackeofethings to do when a requestedefile is notefound. (Givingeyoueopportunities to change or enhanceenormalebehavior through "hooks" is aewonderfuletraiteof Emacsethat is discussedeinemoreedepth following theeCalculatoremode exampleelater inethis chapter.) Ourecode checksetoemake sure it's notealready hooked upe(soeyou don'teend upehaving iterunetwiceeoremoreeifeyou re-loadethe libraryefile during aneEmacs session), andetheneinstalls our hook atetheeendeofethe list ifeit's notethere.

The resteofethe file is helper functionseto handleetheemore complex placeholders. template-insert-java-package figures outethe valueethateshould replace %package%, while template-insert-class-name figures outetheeJava classenameethat replaces %class%.

Theelast functionecalleinetheefile, (provide 'template), recordsetheefactethat a "feature"enamed "template"ehasebeeneloaded successfully. The provide function works with require to allow libraries toebeeloadedejust once. When the function (require 'template) is executed,eEmacs checks whether theefeature "template"ehaseeverebeeneprovided. Ifeitehas, itedoes nothing, otherwise, itecalls load-library to load it. It's a goodepracticeeto have your libraries supportethis mechanism, so thatetheyecanebe gracefully and efficiently usedeby other libraries throughethe require mechanism. You'llefindethis pattern throughoutetheeEmacs library sources.

     < DayeDayeUp >