;; promela-mode.el --- major mode for editing PROMELA program files
;; $Revision: 1.11 $ $Date: 2001/07/09 18:36:45 $ $Author: engstrom $

;; Author: Eric Engstrom <eric.engstrom@honeywell.com>
;; Maintainer: Eric Engstrom
;; Keywords: spin, promela, tools

;; Copyright (C) 1998-2003  Eric Engstrom / Honeywell Laboratories

;; ... Possibly insert GPL here someday ...

;;; Commentary:

;;	This file contains code for a GNU Emacs major mode for editing
;;	PROMELA (SPIN) program files.

;;	Type "C-h m" in Emacs (while in a buffer in promela-mode) for
;;	information on how to configure indentation and fontification,
;;	or look at the configuration variables below.

;;	To use, place promela-mode.el in a directory in your load-path.
;;	Then, put the following lines into your .emacs and promela-mode
;;	will be automatically loaded when editing a PROMELA program.

;;	(autoload 'promela-mode "promela-mode" "PROMELA mode" nil t)
;;	(setq auto-mode-alist
;;	      (append
;;	       (list (cons "\\.promela$"  'promela-mode)
;;		     (cons "\\.spin$"     'promela-mode)
;;		     (cons "\\.pml$"      'promela-mode)
;;		     ;; (cons "\\.other-extensions$"     'promela-mode)
;;	       	     )
;;	       auto-mode-alist))

;;	If you wish for promela-mode to be used for files with other
;;	extensions you add your own patterned after the code above.

;;      Note that promela-mode adhears to the font-lock "standards" and
;;      defines several "levels" of fontification or colorization.  The
;;      default is fairly gaudy, so I can imagine that some folks would
;;      like a bit less.  FMI: see `font-lock-maximum-decoration'

;; This mode is known to work under the following versions of emacs:
;;   - XEmacs:        19.16, 20.x, 21.x
;;   - FSF/GNU Emacs: 19.34
;;   - NTEmacs (FSF): 20.[67]
;; That is not to say there are no bugs specific to one of those versions :-)

;; Please send any comments, bugs, patches or other requests to
;; Eric Engstrom at engstrom@htc.honeywell.com

;; To-Do:
;;  - compile/syntax-check/verify? (suggested by R.Goldman)
;;  - indentation - splitting lines at logical operators (M. Rangarajan)
;;    [ This might "devolve" to indentation after "->" or ";" 
;;      being as is, but anything else indent even more? ]
;;       :: SomeReallyLongArrayRef[this].typedefField != SomeReallyLongConstant -> /* some-comment */
;;    [ Suggestion would be to break the first line after the !=, therefore: ]
;;       :: SomeReallyLongArrayRef[this].typedefField 
;;	      != SomeReallyLongConstant -> /* some-comment */
;;    [ at this point I'm not so sure about this change... EE: 2001/05/19 ] 

;;; -------------------------------------------------------------------------
;;; Code:

;; NOTE: same as CVS revision:
(defconst promela-mode-version "$Revision: 1.11 $"
  "Promela-mode version number.")

;; -------------------------------------------------------------------------
;; The following constant values can be modified by the user in a .emacs file

(defconst promela-block-indent 2
  "*Controls indentation of lines within a block (`{') construct")

(defconst promela-selection-indent 2
  "*Controls indentation of options within a selection (`if')
or iteration (`do') construct")

(defconst promela-selection-option-indent 3
  "*Controls indentation of lines after options within selection or
iteration construct (`::')")

(defconst promela-comment-col 32
  "*Defines the desired comment column for comments to the right of text.")

(defconst promela-tab-always-indent t
  "*Non-nil means TAB in Promela mode should always reindent the current line,
regardless of where in the line point is when the TAB command is used.")

(defconst promela-auto-match-delimiter t
  "*Non-nil means typing an open-delimiter (i.e. parentheses, brace, quote, etc)
should also insert the matching closing delmiter character.")

;; That should be about it for most users...
;; unless you wanna hack elisp, the rest of this is probably uninteresting


;; -------------------------------------------------------------------------
;; help determine what emacs we have here...

(defconst promela-xemacsp (string-match "XEmacs" (emacs-version))
  "Non-nil if we are running in the XEmacs environment.")

;;;(defconst promela-xemacs20p (and promela-xemacsp (>= emacs-major-version 20))
;;  "Non-nil if we are running in an XEmacs environment version 20 or greater.")

;; -------------------------------------------------------------------------
;; promela-mode font faces/definitions

;; make use of font-lock stuff, so say that explicitly
(require 'font-lock)

;; BLECH!  YUCK!   I just wish these guys could agree to something....
;; Faces available in:         ntemacs emacs  xemacs xemacs xemacs    
;;     font-lock- xxx -face     20.6   19.34  19.16   20.x   21.x
;;       -builtin-                X                             
;;       -constant-               X                             
;;       -comment-                X      X      X      X      X
;;       -doc-string-                           X      X      X
;;       -function-name-          X      X      X      X      X
;;       -keyword-                X      X      X      X      X
;;       -preprocessor-                         X      X      X
;;       -reference-                     X      X      X      X
;;       -signal-name-                          X      X!20.0 
;;       -string-                 X      X      X      X      X
;;       -type-                   X      X      X      X      X
;;       -variable-name-          X      X      X      X      X
;;       -warning-                X                           X

;;; Compatibility on faces between versions of emacs-en
(unless promela-xemacsp

  (defvar font-lock-preprocessor-face 'font-lock-preprocessor-face
    "Face name to use for preprocessor statements.")
  ;; For consistency try to define the preprocessor face == builtin face
  (condition-case nil
      (copy-face 'font-lock-builtin-face 'font-lock-preprocessor-face)
    (error
     (defface font-lock-preprocessor-face
       '((t (:foreground "blue" :italic nil :underline t)))
       "Face Lock mode face used to highlight preprocessor statements."
       :group 'font-lock-highlighting-faces)))
  
  (defvar font-lock-reference-face 'font-lock-reference-face
    "Face name to use for constants and reference and label names.")
  ;; For consistency try to define the reference face == constant face
  (condition-case nil
      (copy-face 'font-lock-constant-face 'font-lock-reference-face)
    (error
     (defface font-lock-reference-face
       '((((class grayscale) (background light))
          (:foreground "LightGray" :bold t :underline t))
         (((class grayscale) (background dark))
          (:foreground "Gray50" :bold t :underline t))
         (((class color) (background light)) (:foreground "CadetBlue"))
         (((class color) (background dark)) (:foreground "Aquamarine"))
         (t (:bold t :underline t)))
       "Font Lock mode face used to highlight constancs, references and labels."
       :group 'font-lock-highlighting-faces)))

  )

;; send-poll "symbol" face is custom to promela-mode 
;; but check for existence to allow someone to override it
(defvar promela-fl-send-poll-face 'promela-fl-send-poll-face
  "Face name to use for Promela Send or Poll symbols: `!' or `?'")
(copy-face (if promela-xemacsp 'modeline 'region)
           'promela-fl-send-poll-face)

;; some emacs-en don't define or have regexp-opt available.  
(unless (functionp 'regexp-opt)
  (defmacro regexp-opt (strings)
    "Cheap imitation of `regexp-opt' since it's not availble in this emacs"
    `(mapconcat 'identity ,strings "\\|")))
  

;; -------------------------------------------------------------------------
;; promela-mode font lock specifications/regular-expressions
;;   - for help, look at definition of variable 'font-lock-keywords
;;   - some fontification ideas from -- [engstrom:20010309.1435CST]
;;	 Pat Tullman (tullmann@cs.utah.edu) and
;;	 Ny Aina Razermera Mamy (ainarazr@cs.uoregon.edu)
;;     both had promela-mode's that I discovered after starting this one...
;;     (but neither did any sort of indentation ;-)

(defconst promela-font-lock-keywords-1 nil
  "Subdued level highlighting for Promela mode.")

(defconst promela-font-lock-keywords-2 nil
  "Medium level highlighting for Promela mode.")

(defconst promela-font-lock-keywords-3 nil
  "Gaudy level highlighting for Promela mode.")

;; set each of those three variables now..
(let ((promela-keywords
       (eval-when-compile 
         (regexp-opt 
          '("active" "assert" "atomic" "break" "d_step"
            "do" "dproctype" "else" "empty" "enabled"
            "eval" "fi" "full" "goto" "hidden" "if" "init"
            "inline" "len" "local" "mtype" "nempty" "never"
            "nfull" "od" "of" "pcvalue" "printf" "priority"
            "proctype" "provided" "run" "show" "skip" 
            "timeout" "trace" "typedef" "unless" "xr" "xs"))))
      (promela-types
       (eval-when-compile
         (regexp-opt '("bit" "bool" "byte" "short"
                       "int" "unsigned" "chan")))))

  ;; really simple fontification (strings and comments come for "free")
  (setq promela-font-lock-keywords-1
    (list
     ;; Keywords:
     (cons (concat "\\<\\(" promela-keywords "\\)\\>")
           'font-lock-keyword-face)
     ;; Types:
     (cons (concat "\\<\\(" promela-types "\\)\\>")
           'font-lock-type-face)
     ;; Special constants:
     '("\\<_\\(np\\|pid\\|last\\)\\>" . font-lock-reference-face)))

  ;; more complex fontification
  ;; add function (proctype) names, lables and goto statements
  ;; also add send/receive/poll fontification
  (setq promela-font-lock-keywords-2
   (append promela-font-lock-keywords-1
    (list
     ;; ANY Pre-Processor directive (lazy method: any line beginning with "#[a-z]+")
     '("^\\(#[ \t]*[a-z]+\\)"		1 'font-lock-preprocessor-face t)

     ;; "Functions" (proctype-s and inline-s)
     (list (concat "\\<\\("
                   (eval-when-compile
                     (regexp-opt '("run" "dproctype" "proctype" "inline")))
                   "\\)\\>[ \t]*\\(\\sw+\\)?")
           ;;'(1 'font-lock-keyword-face nil)
           '(2 'font-lock-function-name-face nil t))

    ;; Labels and GOTO labels
    '("^\\(\\sw+\\):" 1 'font-lock-reference-face)
    '("\\<\\(goto\\)\\>[ \t]+\\(\\sw+\\)"
      ;;(1 'font-lock-keyword-face nil)
      (2 'font-lock-reference-face nil t))

    ;; Send, Receive and Poll
    '("\\(\\sw+\\)\\(\\[[^\\?!]+\\]\\)?\\(\\??\\?\\|!?!\\)\\(\\sw+\\)"
      (1 'font-lock-variable-name-face nil t)
      (3 'promela-fl-send-poll-face nil t)
      (4 'font-lock-reference-face nil t)
      )
    )))

  ;; most complex fontification
  ;; add pre-processor directives, typed variables and hidden/typedef decls.
  (setq promela-font-lock-keywords-3
   (append promela-font-lock-keywords-2
    (list
     ;; ANY Pre-Processor directive (lazy method: any line beginning with "#[a-z]+")
     ;;'("^\\(#[ \t]*[a-z]+\\)"		1 'font-lock-preprocessor-face t)
     ;; "defined" in an #if or #elif and associated macro names
     '("^#[ \t]*\\(el\\)?if\\>"
       ("\\<\\(defined\\)\\>[ \t]*(?\\(\\sw+\\)" nil nil
        (1 'font-lock-preprocessor-face nil t)
        (2 'font-lock-reference-face nil t)))
     '("^#[ \t]*ifn?def\\>"
       ("[ \t]*\\(\\sw+\\)" nil nil
        (1 'font-lock-reference-face nil t)))
     ;; Filenames in #include <...> directives
     '("^#[ \t]*include[ \t]+<\\([^>\"\n]+\\)>"	1 'font-lock-string-face nil t)
     ;; Defined functions and constants/types (non-functions)
     '("^#[ \t]*define[ \t]+"
       ("\\(\\sw+\\)(" 			nil nil (1 'font-lock-function-name-face nil t))
       ("\\(\\sw+\\)[ \t]+\\(\\sw+\\)"	nil nil (1 'font-lock-variable-name-face)
       						(2 'font-lock-reference-face nil t))
       ("\\(\\sw+\\)[^(]?"		nil nil (1 'font-lock-reference-face nil t)))

     ;; Types AND variables
     ;;   - room for improvement: (i.e. don't currently):
     ;;     highlight user-defined types and asociated variable declarations
     (list (concat "\\<\\(" promela-types "\\)\\>")
          ;;'(1 'font-lock-type-face)
          ;; now match the variables after the type definition, if any
          '(promela-match-variable-or-declaration
            nil nil
            (1 'font-lock-variable-name-face) ;; nil t)
            (2 font-lock-reference-face nil t)))
    
    ;; Typedef/hidden types and declarations
    '("\\<\\(typedef\\|hidden\\)\\>[ \t]*\\(\\sw+\\)?"
      ;;(1 'font-lock-keyword-face nil)
      (2 'font-lock-type-face nil t)
      ;; now match the variables after the type definition, if any
      (promela-match-variable-or-declaration
       nil nil
       (1 'font-lock-variable-name-face nil t)
       (2 'font-lock-reference-face nil t)))
    )))
  )

(defvar promela-font-lock-keywords promela-font-lock-keywords-1
  "Default expressions to highlight in Promela mode.")

;; Font-lock matcher functions:
(defun promela-match-variable-or-declaration (limit)
  "Match, and move over, any declaration/definition item after point.
Matches after point, but ignores leading whitespace characters.
Does not move further than LIMIT.

The expected syntax of a declaration/definition item is `word' (preceded
by optional whitespace) optionally followed by a `= value' (preceded and
followed by more optional whitespace)

Thus the regexp matches after point:	word [ = value ]
					^^^^     ^^^^^
Where the match subexpressions are:	  1        2

The item is delimited by (match-beginning 1) and (match-end 1).
If (match-beginning 2) is non-nil, the item is followed by a `value'."
  (when (looking-at "[ \t]*\\(\\sw+\\)[ \t]*=?[ \t]*\\(\\sw+\\)?[ \t]*,?")
    (goto-char (min limit (match-end 0)))))


;; -------------------------------------------------------------------------
;; "install" promela-mode font lock specifications

;; FMI: look up 'font-lock-defaults
(defconst promela-font-lock-defaults
  '(
    (promela-font-lock-keywords 
     promela-font-lock-keywords-1
     promela-font-lock-keywords-2
     promela-font-lock-keywords-3)		  ;; font-lock stuff (keywords)
    nil						  ;; keywords-only flag
    nil						  ;; case-fold keyword searching
    ;;((?_ . "w") (?$ . "."))			  ;; mods to syntax table
    nil						  ;; mods to syntax table (see below)
    nil						  ;; syntax-begin
    (font-lock-mark-block-function . mark-defun))
)

;; "install" the font-lock-defaults based upon version of emacs we have
(cond (promela-xemacsp
       (put 'promela-mode 'font-lock-defaults promela-font-lock-defaults))
      ((not (assq 'promela-mode font-lock-defaults-alist))
       (setq font-lock-defaults-alist
             (cons
              (cons 'promela-mode promela-font-lock-defaults)
              font-lock-defaults-alist))))


;; -------------------------------------------------------------------------
;; other promela-mode specific definitions

(defconst promela-defun-prompt-regexp
  "^[ \t]*\\(d?proctype\\|init\\|inline\\|never\\|trace\\|typedef\\|mtype\\s-+=\\)[^{]*"
  "Regexp describing the beginning of a Promela top-level definition.")

(defvar promela-mode-syntax-table nil
  "Syntax table in use in PROMELA-mode buffers.")
(if promela-mode-syntax-table
    ()
  (setq promela-mode-syntax-table (make-syntax-table))
  (modify-syntax-entry ?\\ "\\"   promela-mode-syntax-table)
  (modify-syntax-entry ?/  ". 14" promela-mode-syntax-table)
  (modify-syntax-entry ?*  ". 23" promela-mode-syntax-table)
  (modify-syntax-entry ?+  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?-  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?=  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?%  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?<  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?>  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?&  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?|  "."    promela-mode-syntax-table)
  (modify-syntax-entry ?.  "_"    promela-mode-syntax-table)
  (modify-syntax-entry ?_  "w"    promela-mode-syntax-table)
  (modify-syntax-entry ?\' "\""   promela-mode-syntax-table)
  )

(defvar promela-mode-abbrev-table nil
  "*Abbrev table in use in promela-mode buffers.")
(if promela-mode-abbrev-table
    nil
  (define-abbrev-table 'promela-mode-abbrev-table
    '(
;; Commented out for now - need to think about what abbrevs make sense
;;     ("assert" 	"ASSERT" 	promela-check-expansion 0)
;;     ("d_step"	"D_STEP"	promela-check-expansion 0)
;;     ("break" 	"BREAK" 	promela-check-expansion 0)
;;     ("do" 		"DO" 		promela-check-expansion 0)
;;     ("proctype"	"PROCTYPE" 	promela-check-expansion 0)
      )))

(defvar promela-mode-map nil
  "Keymap for promela-mode.")
(if promela-mode-map
    nil
  (setq promela-mode-map (make-sparse-keymap))
  (define-key promela-mode-map "\t"		'promela-indent-command)
  (define-key promela-mode-map "\C-m"		'promela-newline-and-indent)
  ;(define-key promela-mode-map 'backspace	'backward-delete-char-untabify)
  (define-key promela-mode-map "\C-c\C-p"	'promela-beginning-of-block)
  ;(define-key promela-mode-map "\C-c\C-n"	'promela-end-of-block)
  (define-key promela-mode-map "\M-\C-a"	'promela-beginning-of-defun)
  ;(define-key promela-mode-map "\M-\C-e"	'promela-end-of-defun)
  (define-key promela-mode-map "\C-c("		'promela-toggle-auto-match-delimiter)
  (define-key promela-mode-map "{"		'promela-open-delimiter)
  (define-key promela-mode-map "}" 		'promela-close-delimiter)
  (define-key promela-mode-map "("		'promela-open-delimiter)
  (define-key promela-mode-map ")" 		'promela-close-delimiter)
  (define-key promela-mode-map "["		'promela-open-delimiter)
  (define-key promela-mode-map "]" 		'promela-close-delimiter)
  (define-key promela-mode-map ";"		'promela-insert-and-indent)
  (define-key promela-mode-map ":"		'promela-insert-and-indent)
  ;; 
  ;; this is preliminary at best - use at your own risk:
  (define-key promela-mode-map "\C-c\C-s"	'promela-syntax-check)
  ;;
  ;;(define-key promela-mode-map "\C-c\C-d"	'promela-mode-toggle-debug)
  ;;(define-key promela-mode-map "\C-c\C-r"	'promela-mode-revert-buffer)
  )

(defvar promela-matching-delimiter-alist
  '( (?(  . ?))
     (?[  . ?])
     (?{  . "\n}")
     ;(?<  . ?>)
     (?\' . ?\')
     (?\` . ?\`)
     (?\" . ?\") )
  "List of pairs of matching open/close delimiters - for auto-insert")


;; -------------------------------------------------------------------------
;; Promela-mode itself

(defun promela-mode ()
  "Major mode for editing PROMELA code.
\\{promela-mode-map}

Variables controlling indentation style:
  promela-block-indent
	Relative offset of lines within a block (`{') construct.

  promela-selection-indent
  	Relative offset of option lines within a selection (`if')
	or iteration (`do') construct.

  promela-selection-option-indent
	Relative offset of lines after/within options (`::') within
 	selection or iteration constructs.

  promela-comment-col
	Defines the desired comment column for comments to the right of text.

  promela-tab-always-indent
	Non-nil means TAB in PROMELA mode should always reindent the current
	line, regardless of where in the line the point is when the TAB
	command is used.

  promela-auto-match-delimiter
	Non-nil means typing an open-delimiter (i.e. parentheses, brace,
        quote, etc) should also insert the matching closing delmiter
        character.

Turning on PROMELA mode calls the value of the variable promela-mode-hook with
no args, if that value is non-nil.

For example: '
	(setq promela-mode-hook '(lambda ()
			(setq promela-block-indent 2)
			(setq promela-selection-indent 0)
			(setq promela-selection-option-indent 2)
			(local-set-key \"\\C-m\" 'promela-indent-newline-indent)
			))'

will indent block two steps, will make selection options aligned with DO/IF
and sub-option lines indent to a column after the `::'.  Also, lines will
be reindented when you hit RETURN.

Note that promela-mode adhears to the font-lock \"standards\" and
defines several \"levels\" of fontification or colorization.  The
default is fairly gaudy, so if you would prefer a bit less, please see
the documentation for the variable: `font-lock-maximum-decoration'.
"
  (interactive)
  (kill-all-local-variables)
  (setq mode-name  		"Promela")
  (setq major-mode 		'promela-mode)
  (use-local-map		promela-mode-map)
  (set-syntax-table 		promela-mode-syntax-table)
  (setq local-abbrev-table 	promela-mode-abbrev-table)

  ;; Make local variables
  (make-local-variable 'case-fold-search)
  (make-local-variable 'paragraph-start)
  (make-local-variable 'paragraph-separate)
  (make-local-variable 'paragraph-ignore-fill-prefix)
  (make-local-variable 'indent-line-function)
  (make-local-variable 'indent-region-function)
  (make-local-variable 'parse-sexp-ignore-comments)
  (make-local-variable 'comment-start)
  (make-local-variable 'comment-end)
  (make-local-variable 'comment-column)
  (make-local-variable 'comment-start-skip)
  (make-local-variable 'comment-indent-hook)
  (make-local-variable 'defun-prompt-regexp)
  (make-local-variable 'compile-command)
  ;; Now set their values
  (setq case-fold-search 		t
        paragraph-start 		(concat "^$\\|" page-delimiter)
        paragraph-separate 		paragraph-start
        paragraph-ignore-fill-prefix 	t
        indent-line-function 		'promela-indent-command
	;;indent-region-function 	'promela-indent-region
        parse-sexp-ignore-comments 	t
        comment-start 			"/* "
        comment-end 			" */"
        comment-column 			32
        comment-start-skip 		"/\\*+ *"
	;;comment-start-skip 		"/\\*+ *\\|// *"
        ;;comment-indent-hook 		'promela-comment-indent
        defun-prompt-regexp 		promela-defun-prompt-regexp
        )

  ;; Turn on font-lock mode
  ;; (and promela-font-lock-mode (font-lock-mode))
  (font-lock-mode)

  ;; Finally, run the hooks and be done.
  (run-hooks 'promela-mode-hook))


;; -------------------------------------------------------------------------
;; Interactive functions
;;

(defun promela-mode-version ()
  "Print the current version of promela-mode in the minibuffer"
  (interactive)
  (message (concat "Promela-Mode: " promela-mode-version)))

(defun promela-beginning-of-block ()
  "Move backward to start of containing block.
Containing block may be `{', `do' or `if' construct, or comment."
  (interactive)
  (goto-char (promela-find-start-of-containing-block-or-comment)))

(defun promela-beginning-of-defun (&optional arg)
  "Move backward to the beginning of a defun.
With argument, do it that many times.
Negative arg -N means move forward to Nth following beginning of defun.
Returns t unless search stops due to beginning or end of buffer.

See also 'beginning-of-defun.

This is a Promela-mode specific version since default (in xemacs 19.16 and
NT-Emacs 20) don't seem to skip comments - they will stop inside them.

Also, this makes sure that the beginning of the defun is actually the
line which starts the proctype/init/etc., not just the open-brace."
  (interactive "p")
  (beginning-of-defun arg)
  (if (not (looking-at promela-defun-prompt-regexp))
      (re-search-backward promela-defun-prompt-regexp nil t))
  (if (promela-inside-comment-p)
      (goto-char (promela-find-start-of-containing-comment))))

(defun promela-indent-command ()
  "Indent the current line as PROMELA code."
  (interactive)
  (if (and (not promela-tab-always-indent)
           (save-excursion
             (skip-chars-backward " \t")
             (not (bolp))))
      (tab-to-tab-stop)
    (promela-indent-line)))

(defun promela-newline-and-indent ()
  "Promela-mode specific newline-and-indent which expands abbrevs before
running a regular newline-and-indent."
  (interactive)
  (if abbrev-mode
      (expand-abbrev))
  (newline-and-indent))

(defun promela-indent-newline-indent ()
  "Promela-mode specific newline-and-indent which expands abbrevs and
indents the current line before running a regular newline-and-indent."
  (interactive)
  (save-excursion (promela-indent-command))
  (if abbrev-mode
      (expand-abbrev))
  (newline-and-indent))

(defun promela-insert-and-indent ()
  "Insert the last character typed and re-indent the current line"
  (interactive)
  (insert last-command-char)
  (save-excursion (promela-indent-command)))

(defun promela-open-delimiter ()
  "Inserts the open and matching close delimiters, indenting as appropriate."
  (interactive)
  (insert last-command-char)
  (if (and promela-auto-match-delimiter (not (promela-inside-comment-p)))
      (save-excursion
        (insert (cdr (assq last-command-char promela-matching-delimiter-alist)))
        (promela-indent-command))))

(defun promela-close-delimiter ()
  "Inserts and indents a close delimiter."
  (interactive)
  (insert last-command-char)
  (if (not (promela-inside-comment-p))
      (save-excursion (promela-indent-command))))

(defun promela-toggle-auto-match-delimiter ()
  "Toggle auto-insertion of parens and other delimiters.
See variable `promela-auto-insert-matching-delimiter'"
  (interactive)
  (setq promela-auto-match-delimiter
        (not promela-auto-match-delimiter))
  (message (concat "Promela auto-insert matching delimiters "
                   (if promela-auto-match-delimiter
                       "enabled" "disabled"))))


;; -------------------------------------------------------------------------
;; Compilation/Verification functions

;; all of this is in serious "beta" mode - don't trust it ;-)
(setq 
        promela-compile-command		"spin "
        promela-syntax-check-args	"-a -v "
)

;;(setq compilation-error-regexp-alist
;;      (append compilation-error-regexp-alist
;;              '(("spin: +line +\\([0-9]+\\) +\"\\([^\"]+\\)\"" 2 1))))

(defun promela-syntax-check ()
  (interactive)
  (compile (concat promela-compile-command
                   promela-syntax-check-args
                   (buffer-name))))


;; -------------------------------------------------------------------------
;; Indentation support functions

(defun promela-indent-around-label ()
  "Indent the current line as PROMELA code,
but make sure to consider the label at the beginning of the line."
  (beginning-of-line)
  (delete-horizontal-space)	; delete any leading whitespace
  (if (not (looking-at "\\sw+:\\([ \t]*\\)"))
      (error "promela-indent-around-label: no label on this line")
    (goto-char (match-beginning 1))
    (let* ((space  (length (match-string 1)))
           (indent (promela-calc-indent))
           (wanted (max 0 (- indent (current-column)))))
      (if (>= space wanted)
          (delete-region (point) (+ (point) (- space wanted)))
        (goto-char (+ (point) space))
        (indent-to-column indent)))))

;; Note that indentation is based ENTIRELY upon the indentation of the
;; previous line(s), esp. the previous non-blank line and the line
;; starting the current containgng block...
(defun promela-indent-line ()
  "Indent the current line as PROMELA code.
Return the amount the by which the indentation changed."
  (beginning-of-line)
  (if (looking-at "[ \t]*\\sw+:")
      (promela-indent-around-label)
    (let ((indent (promela-calc-indent))
          beg
          shift-amt
          (pos (- (point-max) (point))))
      (setq beg (point))
      (skip-chars-forward " \t")
      (setq shift-amt (- indent (current-column)))
      (if (zerop shift-amt)
          (if (> (- (point-max) pos) (point))
              (goto-char (- (point-max) pos)))
        (delete-region beg (point))
        (indent-to indent)
        (if (> (- (point-max) pos) (point))
            (goto-char (- (point-max) pos))))
      shift-amt)))

(defun promela-calc-indent ()
  "Return the appropriate indentation for this line as an int."
  (save-excursion
    (beginning-of-line)
    (let* ((orig-point  (point))
           (state       (promela-parse-partial-sexp))
           (paren-depth (nth 0 state))
           (paren-point (or (nth 1 state) 1))
           (paren-char  (char-after paren-point)))
      ;;(what-cursor-position)
      (cond
       ;; Indent not-at-all - inside a string
       ((nth 3 state)
        (current-indentation))
       ;; Indent inside a comment
       ((nth 4 state)
        (promela-calc-indent-within-comment))
       ;; looking at a pre-processor directive - indent=0
       ((looking-at "[ \t]*#\\(define\\|if\\(n?def\\)?\\|else\\|endif\\)")
        0)
       ;; If we're not inside a "true" block (i.e. "{}"), then indent=0
       ;; I think this is fair, since no (indentable) code in promela
       ;; exists outside of a proctype or similar "{ .. }" structure.
       ((zerop paren-depth)
        0)
       ;; Indent relative to non curly-brace "paren"
       ;; [ NOTE: I'm saving this, but don't use it any more.
       ;;         Now, we let parens be indented like curly braces
       ;;((and (>= paren-depth 1) (not (char-equal ?\{ paren-char)))
       ;; (goto-char paren-point)
       ;; (1+ (current-column)))
       ;; 
       ;; Last option: indent relative to contaning block(s)
       (t
        (goto-char orig-point)
        (promela-calc-indent-within-block paren-point))))))

(defun promela-calc-indent-within-block (&optional limit)
  "Return the appropriate indentation for this line, assume within block.
with optional arg, limit search back to `limit'"
  (save-excursion
    (let* ((stop 	 (or limit 1))
           (block-point  (promela-find-start-of-containing-block stop))
           (block-type   (promela-block-type-after block-point))
           (indent-point (point))
           (indent-type  (promela-block-type-after indent-point)))
      (if (not block-type) 0
        ;;(message "paren: %d (%d); block: %s (%d); indent: %s (%d); stop: %d"
        ;;         paren-depth paren-point block-type block-point
        ;;         indent-type indent-point stop)
        (goto-char block-point)
        (cond
         ;; Indent (options) inside "if" or "do"
         ((memq block-type '(selection iteration))
          (if (re-search-forward "\\(do\\|if\\)[ \t]*::" indent-point t)
              (- (current-column) 2)
            (+ (current-column) promela-selection-indent)))
         ;; indent (generic code) inside "::" option
         ((eq 'option block-type)
          (if (and (not indent-type)
                   (re-search-forward "::.*->[ \t]*\\sw"
                                      (save-excursion (end-of-line) (point))
                                      t))
              (1- (current-column))
            (+ (current-column) promela-selection-option-indent))
          )
         ;; indent code inside "{"
         ((eq 'block block-type)
          (cond
           ;; if we are indenting the end of a block,
           ;; use indentation of start-of-block
           ((equal 'block-end indent-type)
            (current-indentation))
           ;; if the start of the code inside the block is not at eol
           ;; then indent to the same column as the block start +some
           ;; [ but ignore comments after "{" ]
           ((and (not (promela-effective-eolp (1+ (point))))
                 (not (looking-at "{[ \t]*/\\*")))
            (forward-char)		; skip block-start
            (skip-chars-forward "{ \t") ; skip whitespace, if any
            (current-column))
           ;; anything else we indent +promela-block-indent from
           ;; the indentation of the start of block (where we are now)
           (t
            (+ (current-indentation)
               promela-block-indent))))
         ;; dunno what kind of block this is - sound an error
         (t
          (error "promela-calc-indent-within-block: unknown block type: %s" block-type)
          (current-indentation)))))))

(defun promela-calc-indent-within-comment ()
  "Return the indentation amount for line, assuming that the
current line is to be regarded as part of a block comment."
  (save-excursion
    (beginning-of-line)
    (skip-chars-forward " \t")
    (let ((indenting-end-of-comment (looking-at "\\*/"))
          (indenting-blank-line (eolp)))
      ;; if line is NOT blank and next char is NOT a "*'
      (if (not (or indenting-blank-line (= (following-char) ?\*)))
          ;; leave indent alone
          (current-column)
        ;; otherwise look back for _PREVIOUS_ possible nested comment start
        (let ((comment-start (save-excursion 
                               (re-search-backward comment-start-skip))))
          ;; and see if there is an appropriate middle-comment "*"
          (if (re-search-backward "^[ \t]+\\*" comment-start t)
              (current-indentation)
            ;; guess not, so indent relative to comment start
            (goto-char comment-start)
            (if indenting-end-of-comment
                (current-column)
              (1+ (current-column)))))))))


;; -------------------------------------------------------------------------
;; Misc other support functions

(defun promela-parse-partial-sexp (&optional start limit)
  "Return the partial parse state of current defun or from optional start
to end limit"
  (save-excursion
    (let ((end (or limit (point))))
      (if start
          (goto-char start)
        (promela-beginning-of-defun))
      (parse-partial-sexp (point) end))))

;;(defun promela-at-end-of-block-p ()
;;  "Return t if cursor is at the end of a promela block"
;;  (save-excursion
;;    (let ((eol (progn (end-of-line) (point))))
;;      (beginning-of-line)
;;      (skip-chars-forward " \t")
;;      ;;(re-search-forward "\\(}\\|\\b\\(od\\|fi\\)\\b\\)" eol t))))
;;      (looking-at "[ \t]*\\(od\\|fi\\)\\b"))))

(defun promela-inside-comment-p ()
  "Check if the point is inside a comment block."
  (save-excursion
    (let ((origpoint (point))
          state)
      (goto-char 1)
      (while (> origpoint (point))
	(setq state (parse-partial-sexp (point) origpoint 0)))
      (nth 4 state))))

(defun promela-inside-comment-or-string-p ()
  "Check if the point is inside a comment or a string."
  (save-excursion
    (let ((origpoint (point))
          state)
      (goto-char 1)
      (while (> origpoint (point))
	(setq state (parse-partial-sexp (point) origpoint 0)))
      (or (nth 3 state) (nth 4 state)))))


(defun promela-effective-eolp (&optional point)
  "Check if we are at the effective end-of-line, ignoring whitespace"
  (save-excursion
    (if point (goto-char point))
    (skip-chars-forward " \t")
    (eolp)))

(defun promela-check-expansion ()
  "If abbrev was made within a comment or a string, de-abbrev!"
  (if promela-inside-comment-or-string-p
	(unexpand-abbrev)))

(defun promela-block-type-after (&optional point)
  "Return the type of block after current point or parameter as a symbol.
Return one of 'iteration `do', 'selection `if', 'option `::',
'block `{' or `}' or nil if none of the above match."
  (save-excursion
    (goto-char (or point (point)))
    (skip-chars-forward " \t")
    (cond
     ((looking-at "do\\b") 'iteration)
     ;;((looking-at "od\\b") 'iteration-end)
     ((looking-at "if\\b") 'selection)
     ;;((looking-at "fi\\b") 'selection-end)
     ((looking-at "::") 'option)
     ((looking-at "[{(]") 'block)
     ((looking-at "[})]") 'block-end)
     (t nil))))

(defun promela-find-start-of-containing-comment (&optional limit)
  "Return the start point of the containing comment block.
Stop at `limit' or beginning of buffer."
  (let ((stop (or limit 1)))
    (save-excursion
      (while (and (>= (point) stop)
                  (nth 4 (promela-parse-partial-sexp)))
        (re-search-backward comment-start-skip stop t))
      (point))))

(defun promela-find-start-of-containing-block (&optional limit)
  "Return the start point of the containing `do', `if', `::' or
`{' block or containing comment.
Stop at `limit' or beginning of buffer."
  (save-excursion
    (skip-chars-forward " \t")
    (let* ((type  (promela-block-type-after))
           (stop  (or limit
                      (save-excursion (promela-beginning-of-defun) (point))))
           (state (promela-parse-partial-sexp stop))
           (level (if (looking-at "\\(od\\|fi\\)\\b")
                      2
                    (if (zerop (nth 0 state)) 0 1))))
      ;;(message "find-start-of-containing-block: type: %s; level %d; stop %d"
      ;;         type level stop)
      (while (and (> (point) stop) (not (zerop level)))
	(re-search-backward
             "\\({\\|}\\|::\\|\\b\\(do\\|od\\|if\\|fi\\)\\b\\)"
             stop 'move)
        ;;(message "looking from %d back-to %d" (point) stop)
	(setq state (promela-parse-partial-sexp stop))
	(setq level (+ level
                       (cond ((or (nth 3 state) (nth 4 state))    	 0)
                             ((and (= 1 level) (looking-at "::")
                                   (not (equal type 'option)))	  	-1)
                             ((looking-at "\\({\\|\\(do\\|if\\)\\b\\)") -1)
                             ((looking-at "\\(}\\|\\(od\\|fi\\)\\b\\)") +1)
                             (t 0)))))
      (point))))

(defun promela-find-start-of-containing-block-or-comment (&optional limit)
  "Return the start point of the containing comment or
the start of the containing `do', `if', `::' or `{' block.
Stop at limit or beginning of buffer."
  (if (promela-inside-comment-p)
      (promela-find-start-of-containing-comment limit)
    (promela-find-start-of-containing-block limit)))

;; -------------------------------------------------------------------------
;; Debugging/testing

;; (defun promela-mode-toggle-debug ()
;;   (interactive)
;;   (make-local-variable 'debug-on-error)
;;   (setq debug-on-error (not debug-on-error)))

;;(defun promela-mode-revert-buffer ()
;;  (interactive)
;;  (revert-buffer t t))

;; -------------------------------------------------------------------------
;;###autoload

(provide 'promela-mode)


;;----------------------------------------------------------------------
;; Change History:
;;
;; $Log: promela-mode.el,v $
;; Revision 1.11  2001/07/09 18:36:45  engstrom
;;  - added comments on use of font-lock-maximum-decoration
;;  - moved basic preprocess directive fontification to "level 2"
;;
;; Revision 1.10  2001/05/22 16:29:59  engstrom
;;  - fixed error introduced in fontification levels stuff (xemacs only)
;;
;; Revision 1.9  2001/05/22 16:21:29  engstrom
;;  - commented out the compilation / syntax check stuff for now
;;
;; Revision 1.8  2001/05/22 16:18:49  engstrom
;;  - Munched history in preparation for first non-Honeywell release
;;  - Added "levels" of fontification to be controlled by the std. variable:
;;      'font-lock-maximum-decoration'
;;
;; Revision 1.7  2001/04/20 01:41:46  engstrom
;; Revision 1.6  2001/04/06 23:57:18  engstrom
;; Revision 1.5  2001/04/04 20:04:15  engstrom
;; Revision 1.4  2001/03/15 02:22:18  engstrom
;; Revision 1.3  2001/03/09 19:39:51  engstrom
;; Revision 1.2  2001/03/01 18:07:47  engstrom
;; Revision 1.1  2001/02/01 xx:xx:xx  engstrom
;;     migrated to CVS versioning...
;; Pre-CVS-History:
;;   99-10-04 V0.4 EDE        Fixed bug in end-of-block indentation
;;                            Simplified indentation code significantly
;;   99-09-2x V0.3 EDE        Hacked on indentation more while at FM'99
;;   99-09-16 V0.2 EDE        Hacked, hacked, hacked on indentation
;;   99-04-01 V0.1 EDE        Introduced (less-than) half-baked indentation
;;   98-11-05 V0.0 EDE        Created - much code stolen from rexx-mode.el
;;                            Mostly just a fontification mode -
;;                            (indentation is HARD ;-)
;;
;; EOF promela-mode.el