Êíèãà: Learning GNU Emacs, 3rd Edition

11.4 Building an Automatic Template System

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.4 2004/12/17 16:10:05 kend Exp $
 *
 **/
public class %class% {
    /**
     * Provides access to the CVS version of this class.
     **/
    public static final String VERSION =
        "$Id: ch11.xml,v 1.4 2004/12/17 16:10:05 kend Exp $";
    /**
     * 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.4 2004/12/17 16:10:05 kend Exp $
 *
 **/
public class FooManager {
    /**
     * Provides access to the CVS version of this class.
     **/
    public static final String VERSION =
        "$Id: ch11.xml,v 1.4 2004/12/17 16:10:05 kend Exp $";
    /**
     * 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.[79])

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
arbitrary 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
                   "nnn#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)
        (substring name (match-beginning 1) (match-end 1))
      "")))
(provide 'template)

You'll notice that this code makes heavy use of the regular expression facilities, which is no surprise. The first section sets up some variables that configure the operation of the template system. template-file-name determines the file name (or prefix) that is used to search for templates; the default value of file-template is probably fine. template-replacements-alist sets up the standard placeholders, and the mechanism by which they get replaced by appropriate values. Adding entries to this list is one way to extend the system. Each entry consists of the placeholder to be replaced, followed by the Lisp function to be executed to produce its replacement. The way this function can be stored in a list and executed when appropriate later is one of the great things about Lisp and is discussed in more depth in the calculator mode example in the next section. The placeholders supported are:

%filename%

Gets replaced by the name of the file being created.

%creator%, %author%

These are synonyms; both get replaced by the name of the user creating the file.

%date%

Turns into the current date and time when the file is created.

%once%

Expands into boilerplate code for the C preprocessor to cause a header file to include itself only once, even if it's been included multiple times by other header files. (This sort of thing has been taken care of in more modern environments like Objective C and Java but can still be handy when working with traditional C compilers.)

%package%

Is replaced by the Java package which contains the file being created (assuming the file is a Java class). This package is determined by examining the directory structure in which the file is being placed.

%class%

Becomes the name of the Java class being defined in the file, assuming it's a Java source file.

The first function, find-template-file, is responsible for searching the directory hierarchy above the file being created, looking for a file with the right name to be considered a file template (if template-file-name has been left at its default value, this looks for either a file named file-template or file-template-ext where ext is the extension at the end of the name of the file being created). It just keeps lopping the last directory off the path in which it's looking, starting with the location of the new file, and seeing if it can read a file with one of those names in the current directory, until it runs out of directories.

The function template-file-not-found-hook is the "main program" of the template system. It gets "hooked in" to the normal Emacs find-file process, and called whenever find-file doesn't find the file the user asked for (in other words, a new file is being created). It uses condition-case (a mechanism similar to exception handling in C++ and Java) to make sure it gets a chance to clean up after itself if the user cancels the process of filling in the template file. It checks whether the template file can be found, asks users if they want to use it, and (if they do) loads it into the new buffer and performs the placeholder substitutions. For an explanation of the list manipulation and funcall code that makes the substitutions work, read the discussion of Calculator mode in the next section. Finally, it jumps to the beginning of the new buffer and marks it as unchanged (because, as far as users are concerned, it's a brand new buffer on which they've not yet had to expend any effort).

Immediately after the function definition is the chunk of code that hooks it into the find-file mechanism. The file-not-found-hooks is a variable that Emacs uses to keep track of things to do when a requested file is not found. (Giving you opportunities to change or enhance normal behavior through "hooks" is a wonderful trait of Emacs that is discussed in more depth following the Calculator mode example later in this chapter.) Our code checks to make sure it's not already hooked up (so you don't end up having it run twice or more if you re-load the library file during an Emacs session), and then installs our hook at the end of the list if it's not there.

The rest of the file is helper functions to handle the more complex placeholders. template-insert-java-package figures out the value that should replace %package%, while template-insert-class-name figures out the Java class name that replaces %class%.

The last function call in the file, (provide 'template), records the fact that a "feature" named "template" has been loaded successfully. The provide function works with require to allow libraries to be loaded just once. When the function (require 'template) is executed, Emacs checks whether the feature "template" has ever been provided. If it has, it does nothing, otherwise, it calls load-library to load it. It's a good practice to have your libraries support this mechanism, so that they can be gracefully and efficiently used by other libraries through the require mechanism. You'll find this pattern throughout the Emacs library sources.

Îãëàâëåíèå êíèãè


Ãåíåðàöèÿ: 1.987. Çàïðîñîâ Ê ÁÄ/Cache: 3 / 1
ïîäåëèòüñÿ
Ââåðõ Âíèç