(rmail-retry-failure): Bind inhibit-read-only.
[bpt/emacs.git] / lisp / forms.el
index b86f9bd..9466077 100644 (file)
@@ -1,8 +1,8 @@
-;;; forms.el -- Forms mode: edit a file as a form to fill in.
-;;; Copyright (C) 1991, 1993 Free Software Foundation, Inc.
+;;; forms.el --- Forms mode: edit a file as a form to fill in
 
-;; Author: Johan Vromans <jv@nl.net>
-;; Version: $Revision: 2.4 $
+;; Copyright (C) 1991, 1994, 1995 Free Software Foundation, Inc.
+
+;; Author: Johan Vromans <jvromans@squirrel.nl>
 
 ;; This file is part of GNU Emacs.
 
@@ -40,7 +40,7 @@
 ;;; Forms mode means visiting a data file which is supposed to consist
 ;;; of records each containing a number of fields.  The records are
 ;;; separated by a newline, the fields are separated by a user-defined
-;;; field separater (default: TAB).
+;;; field separator (default: TAB).
 ;;; When shown, a record is transferred to an Emacs buffer and
 ;;; presented using a user-defined form.  One record is shown at a
 ;;; time.
@@ -54,7 +54,7 @@
 ;;; will be buried, for it is never accessed directly.
 ;;;
 ;;; Forms mode is invoked using M-x forms-find-file control-file .
-;;; Alternativily `forms-find-file-other-window' can be used.
+;;; Alternatively `forms-find-file-other-window' can be used.
 ;;;
 ;;; You may also visit the control file, and switch to forms mode by hand
 ;;; with M-x forms-mode .
 ;;;                    If no write access to the data file is
 ;;;                    possible, view mode is enforced. 
 ;;;
+;;;     forms-check-number-of-fields            [bool, default t]
+;;;                   If non-nil, a warning will be issued whenever
+;;;                   a record is found that does not have the number
+;;;                   of fields specified by `forms-number-of-fields'.
+;;;
 ;;;    forms-multi-line                        [string, default "^K"]
 ;;;                    If non-null the records of the data file may
 ;;;                    contain fields that can span multiple lines in
 ;;;                    the form.
 ;;;                    This variable denotes the separator character
 ;;;                    to be used for this purpose.  Upon display, all
-;;;                    occurrencies of this character are translated
+;;;                    occurrences of this character are translated
 ;;;                    to newlines.  Upon storage they are translated
 ;;;                    back to the separator character.
 ;;;
 ;;;                    perform `beginning-of-buffer' or `end-of-buffer'
 ;;;                    to perform `forms-first-field' resp. `forms-last-field'.
 ;;;
+;;;    forms-read-file-filter                  [symbol, default nil]
+;;;                    If not nil: this should be the name of a 
+;;;                    function that is called after the forms data file
+;;;                    has been read.  It can be used to transform
+;;;                    the contents of the file into a format more suitable
+;;;                    for forms-mode processing.
+;;;
+;;;    forms-write-file-filter                 [symbol, default nil]
+;;;                    If not nil: this should be the name of a 
+;;;                    function that is called before the forms data file
+;;;                    is written (saved) to disk.  It can be used to undo
+;;;                    the effects of `forms-read-file-filter', if any.
+;;;
 ;;;    forms-new-record-filter                 [symbol, default nil]
 ;;;                    If not nil: this should be the name of a 
 ;;;                    function that is called when a new
 ;;;                    modified (using text-property `read-only').
 ;;;                    Also, the read-write fields are shown using a
 ;;;                    distinct face, if possible.
+;;;                    As of emacs 19.29, the `intangible' text property
+;;;                    is used to prevent moving into read-only fields.
 ;;;                    This variable defaults to t if running Emacs 19
 ;;;                    with text properties.
 ;;;                    The default face to show read-write fields is
 ;;; is left.  The contents of the form are parsed using information
 ;;; obtained from `forms-format-list', and the fields which are
 ;;; deduced from the form are modified.  Fields not shown on the forms
-;;; retain their origional values.  The newly formed record then
+;;; retain their original values.  The newly formed record then
 ;;; replaces the contents of the old record in `forms--file-buffer'.
 ;;; A parse routine `forms--parser' is built upon startup to parse
 ;;; the records.
 ;;; \C-c \C-l   forms-jump-record
 ;;; \C-c \C-n   forms-next-record
 ;;; \C-c \C-p   forms-prev-record
-;;; \C-c \C-s   forms-search
+;;; \C-c \C-r   forms-search-backward
+;;; \C-c \C-s   forms-search-forward
 ;;; \C-c \C-x   forms-exit
 ;;; 
 ;;; Read-only mode commands:
 ;;; l   forms-jump-record
 ;;; n   forms-next-record
 ;;; p   forms-prev-record
-;;; s   forms-search
+;;; r   forms-search-backward
+;;; s   forms-search-forward
 ;;; x   forms-exit
 ;;; 
 ;;; Of course, it is also possible to use the \C-c prefix to obtain the
 (provide 'forms)                       ;;; official
 (provide 'forms-mode)                  ;;; for compatibility
 
-(defconst forms-version (substring "$Revision: 2.4 $" 11 -2)
+(defconst forms-version (substring "$Revision: 2.23 $" 11 -2)
   "The version number of forms-mode (as string).  The complete RCS id is:
 
-  $Id: forms.el,v 2.4 1994/03/28 23:13:07 kwzh Exp kwzh $")
+  $Id: forms.el,v 2.23 1995/11/16 20:04:57 jvromans Exp kwzh $")
 
 (defvar forms-mode-hooks nil
   "Hook functions to be run upon entering Forms mode.")
 \f
 ;;; Optional variables with default values.
 
+(defvar forms-check-number-of-fields t
+  "*If non-nil, warn about records with wrong number of fields.")
+
 (defvar forms-field-sep "\t"
   "Field separator character (default TAB).")
 
 (defvar forms-read-only nil
   "Non-nil means: visit the file in view (read-only) mode.
-(Defaults to the write access on the data file).")
+\(Defaults to the write access on the data file).")
 
 (defvar forms-multi-line "\C-k"
   "If not nil: use this character to separate multi-line fields (default C-k).")
@@ -305,6 +330,15 @@ The replacement commands performs forms-next/prev-record.")
   "*Non-nil means redefine beginning/end-of-buffer in Forms mode.
 The replacement commands performs forms-first/last-record.")
 
+(defvar forms-read-file-filter nil
+  "The name of a function that is called after reading the data file.
+This can be used to change the contents of the file to something more
+suitable for forms processing.")
+
+(defvar forms-write-file-filter nil
+  "The name of a function that is called before writing the data file.
+This can be used to undo the effects of form-read-file-hook.")
+
 (defvar forms-new-record-filter nil
   "The name of a function that is called when a new record is created.")
 
@@ -354,7 +388,7 @@ Defaults to t if this emacs is capable of handling text properties.")
    "List of strings of the current record, as parsed from the file.")
 
 (defvar forms--search-regexp nil
-  "Last regexp used by forms-search.")
+  "Last regexp used by forms-search functions.")
 
 (defvar forms--format nil
   "Formatting routine.")
@@ -394,7 +428,8 @@ Commands:                        Equivalent keys in read-only mode:
  \\C-c \\C-l      forms-jump-record          l
  \\C-c \\C-n      forms-next-record          n
  \\C-c \\C-p      forms-prev-record          p
- \\C-c \\C-s      forms-search               s
+ \\C-c \\C-r      forms-search-reverse       r
+ \\C-c \\C-s      forms-search-forward       s
  \\C-c \\C-x      forms-exit                 x
 "
   (interactive)
@@ -428,10 +463,16 @@ Commands:                        Equivalent keys in read-only mode:
        (make-local-variable 'forms-forms-scroll)
        (make-local-variable 'forms-forms-jump)
        (make-local-variable 'forms-use-text-properties)
+
+       ;; Filter functions.
+       (make-local-variable 'forms-read-file-filter)
+       (make-local-variable 'forms-write-file-filter)
        (make-local-variable 'forms-new-record-filter)
        (make-local-variable 'forms-modified-record-filter)
 
        ;; Make sure no filters exist.
+       (setq forms-read-file-filter nil)
+       (setq forms-write-file-filter nil)
        (setq forms-new-record-filter nil)
        (setq forms-modified-record-filter nil)
 
@@ -444,22 +485,37 @@ Commands:                        Equivalent keys in read-only mode:
 
        ;; eval the buffer, should set variables
        ;;(message "forms: processing control file...")
-       (eval-current-buffer)
-
-       ;; check if the mandatory variables make sense.
+       ;; If enable-local-eval is not set to t the user is asked first.
+       (if (or (eq enable-local-eval t)
+               (yes-or-no-p 
+                (concat "Evaluate lisp code in buffer "
+                        (buffer-name) " to display forms ")))
+           (eval-current-buffer)
+         (error "`enable-local-eval' inhibits buffer evaluation"))
+
+       ;; Check if the mandatory variables make sense.
        (or forms-file
            (error (concat "Forms control file error: " 
                           "'forms-file' has not been set")))
-       (or forms-number-of-fields
-           (error (concat "Forms control file error: "
-                          "'forms-number-of-fields' has not been set")))
-       (or (and (numberp forms-number-of-fields)
-                (> forms-number-of-fields 0))
-           (error (concat "Forms control file error: "
-                          "'forms-number-of-fields' must be a number > 0")))
+
+       ;; Check forms-field-sep first, since it can be needed to
+       ;; construct a default format list.
        (or (stringp forms-field-sep)
            (error (concat "Forms control file error: "
                           "'forms-field-sep' is not a string")))
+
+       (if forms-number-of-fields
+           (or (and (numberp forms-number-of-fields)
+                    (> forms-number-of-fields 0))
+               (error (concat "Forms control file error: "
+                              "'forms-number-of-fields' must be a number > 0")))
+         (or (null forms-format-list)
+             (error (concat "Forms control file error: "
+                            "'forms-number-of-fields' has not been set"))))
+
+       (or forms-format-list
+           (forms--intuit-from-file))
+
        (if forms-multi-line
            (if (and (stringp forms-multi-line)
                     (eq (length forms-multi-line) 1))
@@ -514,6 +570,9 @@ Commands:                        Equivalent keys in read-only mode:
        ;;(message "forms: setting up... done.")
        ))
 
+  ;; initialization done
+  (setq forms--mode-setup t)
+
   ;; Copy desired faces to the actual variables used by the forms formatter.
   (if (fboundp 'make-face)
       (progn
@@ -551,6 +610,26 @@ Commands:                        Equivalent keys in read-only mode:
   ;; find the data file
   (setq forms--file-buffer (find-file-noselect forms-file))
 
+  ;; Pre-transform.
+  (let ((read-file-filter forms-read-file-filter)
+       (write-file-filter forms-write-file-filter))
+    (if read-file-filter
+       (save-excursion
+         (set-buffer forms--file-buffer)
+         (let ((inhibit-read-only t)
+               (file-modified (buffer-modified-p)))
+           (run-hooks 'read-file-filter)
+           (if (not file-modified) (set-buffer-modified-p nil)))
+         (if write-file-filter
+             (progn
+               (make-variable-buffer-local 'local-write-file-hooks)
+               (setq local-write-file-hooks (list write-file-filter)))))
+      (if write-file-filter
+         (save-excursion
+           (set-buffer forms--file-buffer)
+           (make-variable-buffer-local 'local-write-file-hooks)
+           (setq local-write-file-hooks (list write-file-filter))))))
+
   ;; count the number of records, and set see if it may be modified
   (let (ro)
     (setq forms--total-records
@@ -583,10 +662,27 @@ Commands:                        Equivalent keys in read-only mode:
   ;;(message "forms: proceeding setup (buffer)...")
   (set-buffer-modified-p nil)
 
-  ;; setup the first (or current) record to show
-  (if (< forms--current-record 1)
-      (setq forms--current-record 1))
-  (forms-jump-record forms--current-record)
+  (if (= forms--total-records 0)
+      ;;(message "forms: proceeding setup (new file)...")
+      (progn
+       (insert 
+        "GNU Emacs Forms Mode version " forms-version "\n\n"
+        (if (file-exists-p forms-file)
+            (concat "No records available in file \"" forms-file "\".\n\n")
+          (format "Creating new file \"%s\"\nwith %d field%s per record.\n\n"
+                  forms-file forms-number-of-fields
+                  (if (= 1 forms-number-of-fields) "" "s")))
+        "Use " (substitute-command-keys "\\[forms-insert-record]")
+        " to create new records.\n")
+       (setq forms--current-record 1)
+       (setq buffer-read-only t)
+       (set-buffer-modified-p nil))
+
+    ;; setup the first (or current) record to show
+    (if (< forms--current-record 1)
+       (setq forms--current-record 1))
+    (forms-jump-record forms--current-record)
+    )
 
   ;; user customising
   ;;(message "forms: proceeding setup (user hooks)...")
@@ -595,9 +691,7 @@ Commands:                        Equivalent keys in read-only mode:
 
   ;; be helpful
   (forms--help)
-
-  ;; initialization done
-  (setq forms--mode-setup t))
+)
 \f
 (defun forms--process-format-list ()
   ;; Validate `forms-format-list' and set some global variables.
@@ -607,7 +701,7 @@ Commands:                        Equivalent keys in read-only mode:
   ;; of the fields on the display. This array is used by 
   ;; `forms--parser-using-text-properties' to extract the fields data
   ;; from the form on the screen.
-  ;; Upon completion, `forms-format-list' is garanteed correct, so
+  ;; Upon completion, `forms-format-list' is guaranteed correct, so
   ;; `forms--make-format' and `forms--make-parser' do not need to perform
   ;; any checks.
 
@@ -800,7 +894,7 @@ Commands:                        Equivalent keys in read-only mode:
                (,@ (if (numberp (car forms-format-list))
                        nil
                      '((add-text-properties (point-min) (1+ (point-min))
-                                            '(front-sticky (read-only))))))
+                                            '(front-sticky (read-only intangible))))))
                ;; Prevent insertion after the last text.
                (remove-text-properties (1- (point)) (point)
                                        '(rear-nonsticky)))
@@ -884,8 +978,10 @@ Commands:                        Equivalent keys in read-only mode:
           (point))
         (list 'face forms--ro-face     ; read-only appearance
               'read-only (,@ (list (1+ forms--marker)))
+              'intangible t
               'insert-in-front-hooks '(forms--iif-hook)
-              'rear-nonsticky '(face read-only insert-in-front-hooks))))))
+              'rear-nonsticky '(face read-only insert-in-front-hooks
+                                intangible))))))
     
    ((numberp el)
     (` ((let ((here (point)))
@@ -911,8 +1007,10 @@ Commands:                        Equivalent keys in read-only mode:
           (point))
         (list 'face forms--ro-face
               'read-only (,@ (list (1+ forms--marker)))
+              'intangible t
               'insert-in-front-hooks '(forms--iif-hook)
-              'rear-nonsticky '(read-only face insert-in-front-hooks))))))
+              'rear-nonsticky '(read-only face insert-in-front-hooks
+                                intangible))))))
 
    ;; end of cond
    ))
@@ -995,9 +1093,9 @@ Commands:                        Equivalent keys in read-only mode:
        (if (setq there 
                  (next-single-property-change here 'read-only))
            (aset forms--recordv (aref forms--elements i)
-                 (buffer-substring here there))
+                 (buffer-substring-no-properties here there))
          (aset forms--recordv (aref forms--elements i)
-               (buffer-substring here (point-max)))))
+               (buffer-substring-no-properties here (point-max)))))
       (setq i (1+ i)))))
 
 (defun forms--make-parser-elt (el)
@@ -1019,7 +1117,7 @@ Commands:                        Equivalent keys in read-only mode:
   ;;     (setq here (point))
   ;;     (if (not (search-forward "\nmore text: " nil t nil))
   ;;       (error "Parse error: cannot find \"\\nmore text: \""))
-  ;;     (aset forms--recordv 5 (buffer-substring here (- (point) 12)))
+  ;;     (aset forms--recordv 5 (buffer-substring-no-properties here (- (point) 12)))
   ;;
   ;;    ;;  (tocol 40)
   ;;   (let ((forms--dyntext (car-safe forms--dynamic-text)))
@@ -1029,7 +1127,7 @@ Commands:                        Equivalent keys in read-only mode:
   ;;     (setq forms--dynamic-text (cdr-safe forms--dynamic-text)))
   ;;     ... 
   ;;     ;; final flush (due to terminator sentinel, see below)
-  ;;   (aset forms--recordv 7 (buffer-substring (point) (point-max)))
+  ;;   (aset forms--recordv 7 (buffer-substring-no-properties (point) (point-max)))
 
   (cond
    ((stringp el)
@@ -1039,7 +1137,7 @@ Commands:                        Equivalent keys in read-only mode:
                (if (not (search-forward (, el) nil t nil))
                    (error "Parse error: cannot find \"%s\"" (, el)))
                (aset forms--recordv (, (1- forms--field))
-                     (buffer-substring here
+                     (buffer-substring-no-properties here
                                        (- (point) (, (length el)))))))
          (` ((if (not (looking-at (, (regexp-quote el))))
                  (error "Parse error: not looking at \"%s\"" (, el)))
@@ -1055,7 +1153,7 @@ Commands:                        Equivalent keys in read-only mode:
    ((null el)
     (if forms--field
        (` ((aset forms--recordv (, (1- forms--field))
-                 (buffer-substring (point) (point-max)))))))
+                 (buffer-substring-no-properties (point) (point-max)))))))
    ((listp el)
     (prog1
        (if forms--field
@@ -1064,7 +1162,7 @@ Commands:                        Equivalent keys in read-only mode:
                  (if (not (search-forward forms--dyntext nil t nil))
                      (error "Parse error: cannot find \"%s\"" forms--dyntext))
                  (aset forms--recordv (, (1- forms--field))
-                       (buffer-substring here
+                       (buffer-substring-no-properties here
                                          (- (point) (length forms--dyntext)))))))
          (` ((let ((forms--dyntext (aref forms--dyntexts (, forms--dyntext))))
                (if (not (looking-at (regexp-quote forms--dyntext)))
@@ -1075,6 +1173,52 @@ Commands:                        Equivalent keys in read-only mode:
       (setq forms--field nil)))
    ))
 \f
+(defun forms--intuit-from-file ()
+  "Get number of fields and a default form using the data file."
+
+  ;; If `forms-number-of-fields' is not set, get it from the data file.
+  (if (null forms-number-of-fields)
+
+      ;; Need a file to do this.
+      (if (not (file-exists-p forms-file))
+         (error "Need existing file or explicit 'forms-number-of-records'.")
+
+       ;; Visit the file and extract the first record.
+       (setq forms--file-buffer (find-file-noselect forms-file))
+       (let ((read-file-filter forms-read-file-filter)
+             (the-record))
+         (setq the-record
+               (save-excursion
+                 (set-buffer forms--file-buffer)
+                 (let ((inhibit-read-only t))
+                   (run-hooks 'read-file-filter))
+                 (goto-char (point-min))
+                 (forms--get-record)))
+
+         ;; This may be overkill, but try to avoid interference with 
+         ;; the normal processing.
+         (kill-buffer forms--file-buffer)
+
+         ;; Count the number of fields in `the-record'.
+         (let (the-result
+               (start-pos 0)
+               found-pos
+               (field-sep-length (length forms-field-sep)))
+           (setq forms-number-of-fields 1)
+           (while (setq found-pos
+                        (string-match forms-field-sep the-record start-pos))
+             (progn
+               (setq forms-number-of-fields (1+ forms-number-of-fields))
+               (setq start-pos (+ field-sep-length found-pos))))))))
+
+  ;; Construct default format list.
+  (setq forms-format-list (list "Forms file \"" forms-file "\".\n\n"))
+  (let ((i 0))
+    (while (<= (setq i (1+ i)) forms-number-of-fields)
+      (setq forms-format-list
+           (append forms-format-list
+                   (list (format "%4d: " i) i "\n"))))))
+\f
 (defun forms--set-keymaps ()
   "Set the keymaps used in this mode."
 
@@ -1094,11 +1238,11 @@ Commands:                        Equivalent keys in read-only mode:
   (define-key forms-mode-map "\C-l" 'forms-jump-record)
   (define-key forms-mode-map "\C-n" 'forms-next-record)
   (define-key forms-mode-map "\C-p" 'forms-prev-record)
-  (define-key forms-mode-map "\C-s" 'forms-search)
+  (define-key forms-mode-map "\C-r" 'forms-search-backward)
+  (define-key forms-mode-map "\C-s" 'forms-search-forward)
   (define-key forms-mode-map "\C-x" 'forms-exit)
   (define-key forms-mode-map "<" 'forms-first-record)
   (define-key forms-mode-map ">" 'forms-last-record)
-  (define-key forms-mode-map "?" 'describe-mode)
   (define-key forms-mode-map "\C-?" 'forms-prev-record)
 
   ;; `forms-mode-ro-map' replaces the local map when in read-only mode.
@@ -1110,22 +1254,118 @@ Commands:                        Equivalent keys in read-only mode:
   (define-key forms-mode-ro-map "l" 'forms-jump-record)
   (define-key forms-mode-ro-map "n" 'forms-next-record)
   (define-key forms-mode-ro-map "p" 'forms-prev-record)
-  (define-key forms-mode-ro-map "s" 'forms-search)
+  (define-key forms-mode-ro-map "r" 'forms-search-backward)
+  (define-key forms-mode-ro-map "s" 'forms-search-forward)
   (define-key forms-mode-ro-map "x" 'forms-exit)
   (define-key forms-mode-ro-map "<" 'forms-first-record)
   (define-key forms-mode-ro-map ">" 'forms-last-record)
   (define-key forms-mode-ro-map "?" 'describe-mode)
   (define-key forms-mode-ro-map " " 'forms-next-record)
   (forms--mode-commands1 forms-mode-ro-map)
+  (forms--mode-menu-ro forms-mode-ro-map)
 
   ;; This is the normal, local map.
   (setq forms-mode-edit-map (make-keymap))
   (define-key forms-mode-edit-map "\t"   'forms-next-field)
   (define-key forms-mode-edit-map "\C-c" forms-mode-map)
   (forms--mode-commands1 forms-mode-edit-map)
+  (forms--mode-menu-edit forms-mode-edit-map)
   )
 
-(defun forms--mode-commands1 (map)
+(defun forms--mode-menu-ro (map)
+;;; Menu initialisation
+;  (define-key map [menu-bar] (make-sparse-keymap))
+  (define-key map [menu-bar forms]
+    (cons "Forms" (make-sparse-keymap "Forms")))
+  (define-key map [menu-bar forms menu-forms-exit]
+    '("Exit" . forms-exit))
+  (define-key map [menu-bar forms menu-forms-sep1]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-save]
+    '("Save Data" . forms-save-buffer))
+  (define-key map [menu-bar forms menu-forms-print]
+    '("Print Data" . forms-print))
+  (define-key map [menu-bar forms menu-forms-describe]
+    '("Describe Mode" . describe-mode))
+  (define-key map [menu-bar forms menu-forms-toggle-ro]
+    '("Toggle View/Edit" . forms-toggle-read-only))
+  (define-key map [menu-bar forms menu-forms-jump-record]
+    '("Jump" . forms-jump-record))
+  (define-key map [menu-bar forms menu-forms-search-backward]
+    '("Search Backward" . forms-search-backward))
+  (define-key map [menu-bar forms menu-forms-search-forward]
+    '("Search Forward" . forms-search-forward))
+  (define-key map [menu-bar forms menu-forms-delete-record]
+    '("Delete" . forms-delete-record))
+  (define-key map [menu-bar forms menu-forms-insert-record]
+    '("Insert" . forms-insert-record))
+  (define-key map [menu-bar forms menu-forms-sep2]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-last-record]
+    '("Last Record" . forms-last-record))
+  (define-key map [menu-bar forms menu-forms-first-record]
+    '("First Record" . forms-first-record))
+  (define-key map [menu-bar forms menu-forms-prev-record]
+    '("Previous Record" . forms-prev-record))
+  (define-key map [menu-bar forms menu-forms-next-record]
+    '("Next Record" . forms-next-record))
+  (define-key map [menu-bar forms menu-forms-sep3]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-prev-field]
+    '("Previous Field" . forms-prev-field))
+  (define-key map [menu-bar forms menu-forms-next-field]
+    '("Next Field" . forms-next-field))
+  (put 'forms-insert-record 'menu-enable '(not forms-read-only))
+  (put 'forms-delete-record 'menu-enable '(not forms-read-only))
+)
+(defun forms--mode-menu-edit (map)
+;;; Menu initialisation
+;  (define-key map [menu-bar] (make-sparse-keymap))
+  (define-key map [menu-bar forms]
+    (cons "Forms" (make-sparse-keymap "Forms")))
+  (define-key map [menu-bar forms menu-forms-edit--exit]
+    '("Exit" . forms-exit))
+  (define-key map [menu-bar forms menu-forms-edit-sep1]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-edit-save]
+    '("Save Data" . forms-save-buffer))
+  (define-key map [menu-bar forms menu-forms-edit-print]
+    '("Print Data" . forms-print))
+  (define-key map [menu-bar forms menu-forms-edit-describe]
+    '("Describe Mode" . describe-mode))
+  (define-key map [menu-bar forms menu-forms-edit-toggle-ro]
+    '("Toggle View/Edit" . forms-toggle-read-only))
+  (define-key map [menu-bar forms menu-forms-edit-jump-record]
+    '("Jump" . forms-jump-record))
+  (define-key map [menu-bar forms menu-forms-edit-search-backward]
+    '("Search Backward" . forms-search-backward))
+  (define-key map [menu-bar forms menu-forms-edit-search-forward]
+    '("Search Forward" . forms-search-forward))
+  (define-key map [menu-bar forms menu-forms-edit-delete-record]
+    '("Delete" . forms-delete-record))
+  (define-key map [menu-bar forms menu-forms-edit-insert-record]
+    '("Insert" . forms-insert-record))
+  (define-key map [menu-bar forms menu-forms-edit-sep2]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-edit-last-record]
+    '("Last Record" . forms-last-record))
+  (define-key map [menu-bar forms menu-forms-edit-first-record]
+    '("First Record" . forms-first-record))
+  (define-key map [menu-bar forms menu-forms-edit-prev-record]
+    '("Previous Record" . forms-prev-record))
+  (define-key map [menu-bar forms menu-forms-edit-next-record]
+    '("Next Record" . forms-next-record))
+  (define-key map [menu-bar forms menu-forms-edit-sep3]
+    '("----"))
+  (define-key map [menu-bar forms menu-forms-edit-prev-field]
+    '("Previous Field" . forms-prev-field))
+  (define-key map [menu-bar forms menu-forms-edit-next-field]
+    '("Next Field" . forms-next-field))
+  (put 'forms-insert-record 'menu-enable '(not forms-read-only))
+  (put 'forms-delete-record 'menu-enable '(not forms-read-only))
+)
+
+(defun forms--mode-commands1 (map) 
   "Helper routine to define keys."
   (define-key map [TAB] 'forms-next-field)
   (define-key map [S-tab] 'forms-prev-field)
@@ -1163,36 +1403,23 @@ Commands:                        Equivalent keys in read-only mode:
                                   (current-local-map)
                                   (current-global-map))))
   ;;
-  ;; save-buffer -> forms--save-buffer
-  (make-local-variable 'local-write-file-hooks)
-  (add-hook 'local-write-file-hooks
-           (function
-            (lambda (nil)
-              (forms--checkmod)
-              (save-excursion
-                (set-buffer forms--file-buffer)
-                (save-buffer))
-              t)))
-  ;; We have our own revert function - use it
+  ;; Save buffer
+  (local-set-key "\C-x\C-s" 'forms-save-buffer)
+  ;;
+  ;; We have our own revert function - use it.
   (make-local-variable 'revert-buffer-function)
-  (setq revert-buffer-function 'forms-revert-buffer)
+  (setq revert-buffer-function 'forms--revert-buffer)
 
   t)
 
 (defun forms--help ()
   "Initial help for Forms mode."
-  ;; We should use
   (message (substitute-command-keys (concat
   "\\[forms-next-record]:next"
   "   \\[forms-prev-record]:prev"
   "   \\[forms-first-record]:first"
   "   \\[forms-last-record]:last"
   "   \\[describe-mode]:help"))))
-  ; but it's too slow ....
-;  (if forms-read-only
-;      (message "SPC:next   DEL:prev   <:first   >:last   ?:help   q:exit")
-;    (message "C-c n:next   C-c p:prev   C-c <:first   C-c >:last   C-c ?:help   C-c q:exit"))
-;  )
 
 (defun forms--trans (subj arg rep)
   "Translate in SUBJ all chars ARG into char REP.  ARG and REP should
@@ -1212,9 +1439,7 @@ Commands:                        Equivalent keys in read-only mode:
     (forms--checkmod)
     (if (and save
             (buffer-modified-p forms--file-buffer))
-       (save-excursion
-         (set-buffer forms--file-buffer)
-         (save-buffer)))
+       (forms-save-buffer))
     (save-excursion
       (set-buffer forms--file-buffer)
       (delete-auto-save-file-if-necessary)
@@ -1237,7 +1462,7 @@ Commands:                        Equivalent keys in read-only mode:
   (let ((here (point)))
     (prog2
      (end-of-line)
-     (buffer-substring here (point))
+     (buffer-substring-no-properties here (point))
      (goto-char here))))
 
 (defun forms--show-record (the-record)
@@ -1268,9 +1493,11 @@ Commands:                        Equivalent keys in read-only mode:
   ;; Verify the number of fields, extend forms--the-record-list if needed.
   (if (= (length forms--the-record-list) forms-number-of-fields)
       nil
-    (beep)
-    (message "Warning: this record has %d fields instead of %d"
-            (length forms--the-record-list) forms-number-of-fields)
+    (if (null forms-check-number-of-fields)
+       nil
+      (beep)
+      (message "Warning: this record has %d fields instead of %d"
+              (length forms--the-record-list) forms-number-of-fields))
     (if (< (length forms--the-record-list) forms-number-of-fields)
        (setq forms--the-record-list 
              (append forms--the-record-list
@@ -1333,6 +1560,10 @@ As a side effect: sets `forms--the-record-list'."
       (setq the-record
            (mapconcat 'identity forms--the-record-list forms-field-sep))
 
+      (if (string-match (regexp-quote forms-field-sep)
+                       (mapconcat 'identity forms--the-record-list ""))
+         (error "Field separator occurs in record - update refused!"))
+
       ;; Handle multi-line fields, if allowed.
       (if forms-multi-line
          (forms--trans the-record "\n" forms-multi-line))
@@ -1366,16 +1597,19 @@ As a side effect: sets `forms--the-record-list'."
 (defun forms-find-file (fn)
   "Visit a file in Forms mode."
   (interactive "fForms file: ")
-  (find-file-read-only fn)
-  (or forms--mode-setup (forms-mode t)))
+  (let ((enable-local-eval t)
+       (enable-local-variables t))
+    (find-file-read-only fn)
+    (or forms--mode-setup (forms-mode t))))
 
 ;;;###autoload
 (defun forms-find-file-other-window (fn)
   "Visit a file in Forms mode in other window."
   (interactive "fFbrowse file in other window: ")
-  (find-file-other-window fn)
-  (eval-current-buffer)
-  (or forms--mode-setup (forms-mode t)))
+  (let ((enable-local-eval t)
+       (enable-local-variables t))
+    (find-file-other-window fn)
+    (or forms--mode-setup (forms-mode t))))
 
 (defun forms-exit (query)
   "Normal exit from Forms mode.  Modified buffers are saved."
@@ -1472,7 +1706,7 @@ As a side effect: re-calculates the number of records in the data file."
 (defun forms-toggle-read-only (arg)
   "Toggles read-only mode of a forms mode buffer.
 With an argument, enables read-only mode if the argument is positive.
-Otherwise enables edit mode if the visited file is writeable."
+Otherwise enables edit mode if the visited file is writable."
 
   (interactive "P")
 
@@ -1568,18 +1802,18 @@ it is called to fill (some of) the fields with default values."
          (goto-line ln)
          ;; Use delete-region instead of kill-region, to avoid
          ;; adding junk to the kill-ring.
-         (delete-region (save-excursion (beginning-of-line) (point))
-                        (save-excursion (end-of-line) (1+ (point)))))
+         (delete-region (progn (beginning-of-line) (point))
+                        (progn (beginning-of-line 2) (point))))
        (setq forms--total-records (1- forms--total-records))
        (if (> forms--current-record forms--total-records)
            (setq forms--current-record forms--total-records))
        (forms-jump-record forms--current-record)))
   (message ""))
 
-(defun forms-search (regexp)
-  "Search REGEXP in file buffer."
+(defun forms-search-forward (regexp)
+  "Search forward for record containing REGEXP."
   (interactive 
-   (list (read-string (concat "Search for" 
+   (list (read-string (concat "Search forward for
                                  (if forms--search-regexp
                                   (concat " ("
                                           forms--search-regexp
@@ -1608,7 +1842,55 @@ it is called to fill (some of) the fields with default values."
          (re-search-forward regexp nil t))))
   (setq forms--search-regexp regexp))
 
-(defun forms-revert-buffer (&optional arg noconfirm)
+(defun forms-search-backward (regexp)
+  "Search backward for record containing REGEXP."
+  (interactive 
+   (list (read-string (concat "Search backward for" 
+                                 (if forms--search-regexp
+                                  (concat " ("
+                                          forms--search-regexp
+                                          ")"))
+                                 ": "))))
+  (if (equal "" regexp)
+      (setq regexp forms--search-regexp))
+  (forms--checkmod)
+
+  (let (the-line the-record here
+                (fld-sep forms-field-sep))
+    (if (save-excursion
+         (set-buffer forms--file-buffer)
+         (setq here (point))
+         (beginning-of-line)
+         (if (null (re-search-backward regexp nil t))
+             (progn
+               (goto-char here)
+               (message (concat "\"" regexp "\" not found."))
+               nil)
+           (setq the-record (forms--get-record))
+           (setq the-line (1+ (count-lines (point-min) (point))))))
+       (progn
+         (setq forms--current-record the-line)
+         (forms--show-record the-record)
+         (re-search-forward regexp nil t))))
+  (setq forms--search-regexp regexp))
+
+(defun forms-save-buffer (&optional args)
+  "Forms mode replacement for save-buffer.
+It saves the data buffer instead of the forms buffer.
+Calls `forms-write-file-filter' before writing out the data."
+  (interactive "p")
+  (forms--checkmod)
+  (let ((read-file-filter forms-read-file-filter))
+    (save-excursion
+      (set-buffer forms--file-buffer)
+      (let ((inhibit-read-only t))
+       (save-buffer args)
+       (if read-file-filter
+           (run-hooks 'read-file-filter))
+       (set-buffer-modified-p nil))))
+  t)
+
+(defun forms--revert-buffer (&optional arg noconfirm)
   "Reverts current form to un-modified."
   (interactive "P")
   (if (or noconfirm
@@ -1624,7 +1906,8 @@ it is called to fill (some of) the fields with default values."
   (let ((i 0)
        (here (point))
        there
-       (cnt 0))
+       (cnt 0)
+       (inhibit-point-motion-hooks t))
 
     (if (zerop arg)
        (setq cnt 1)
@@ -1650,7 +1933,8 @@ it is called to fill (some of) the fields with default values."
   (let ((i (length forms--markers))
        (here (point))
        there
-       (cnt 0))
+       (cnt 0)
+       (inhibit-point-motion-hooks t))
 
     (if (zerop arg)
        (setq cnt 1)
@@ -1668,13 +1952,39 @@ it is called to fill (some of) the fields with default values."
                    (throw 'done t))))))
        nil
       (goto-char (aref forms--markers (1- (length forms--markers)))))))
+
+(defun forms-print ()
+  "Send the records to the printer with 'print-buffer', one record per page."
+  (interactive)
+  (let ((inhibit-read-only t)
+       (save-record forms--current-record)
+       (nb-record 1)
+       (record nil))
+    (while (<= nb-record forms--total-records)
+      (forms-jump-record nb-record)
+      (setq record (buffer-string))
+      (save-excursion
+       (set-buffer (get-buffer-create "*forms-print*"))
+       (goto-char (buffer-end 1))
+       (insert record)
+       (setq buffer-read-only nil)
+       (if (< nb-record forms--total-records)
+           (insert "\n\f\n")))
+      (setq nb-record (1+ nb-record)))
+    (save-excursion
+      (set-buffer "*forms-print*")
+      (print-buffer)
+      (set-buffer-modified-p nil)
+      (kill-buffer (current-buffer)))
+    (forms-jump-record save-record)))
+
 ;;;
 ;;; Special service
 ;;;
 (defun forms-enumerate (the-fields)
   "Take a quoted list of symbols, and set their values to sequential numbers.
 The first symbol gets number 1, the second 2 and so on.
-It returns the higest number.
+It returns the highest number.
 
 Usage: (setq forms-number-of-fields
              (forms-enumerate