;;; postscript.el --- major mode for editing PostScript programs

;; Keywords: langauges

;; This file is part of XEmacs.

;; XEmacs is free software; you can redistribute it and/or modify it
;; under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; XEmacs is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with XEmacs; see the file COPYING.  If not, write to the 
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Synched up with: Not in FSF.

;;
;; Author:	Chris Maio
;; Last edit:	4 Sep 1988
;; Includes patches from relph@presto.ig.com (John M. Relph) posted to
;; gnu.emacs.sources on 22 Nov 90 04:53:43 GMT.
;;
;; The following two statements, placed in your .emacs file or site-init.el,
;; will cause this file to be autoloaded, and postscript-mode invoked, when
;; visiting .ps or .cps files:
;;
;;	(autoload 'postscript-mode "postscript.el" "" t)
;;	(setq auto-mode-alist
;;	      (cons '("\\.c?ps$".postscript-mode) auto-mode-alist))
;;

(provide 'postscript)

(defconst ps-indent-level 2
  "*Indentation to be used inside of PostScript blocks or arrays")

(defconst ps-tab-width 8
  "*Tab stop width for PostScript mode")

(defun ps-make-tabs (stop)
  (and (< stop 132) (cons stop (ps-make-tabs (+ stop ps-tab-width)))))

(defconst ps-tab-stop-list (ps-make-tabs ps-tab-width)
  "*Tab stop list for PostScript mode")

(defconst ps-postscript-command '("gs" "-")
  "*Command used to invoke with a printer spooler or NeWS server.")

(defvar ps-mode-map nil
  "Keymap used in PostScript mode buffers")

(defvar ps-mode-syntax-table nil
  "PostScript mode syntax table")

(defvar ps-balanced-string-syntax-p
  (let ((b (current-buffer))
        (loser (generate-new-buffer "x")))
    (unwind-protect
         (progn
           (set-buffer loser)
           (set-syntax-table (copy-syntax-table))
           (modify-syntax-entry ?\(  "\"\)")
           (insert "((")
           (let ((v (parse-partial-sexp (point-min) (point-max))))
             (if (elt v 3)
                 ;; New syntax code think's we're still inside a string
                 t
                 nil)))
      (set-buffer b)
      (kill-buffer loser))))

(defconst postscript-font-lock-keywords (purecopy
   (list
    ;; Proper rule for Postscript strings
    '("(\\([^)]*\\))" . font-lock-string-face)
    ;; Make any line beginning with a / be a ``keyword''
    '("^/[^\n%]*" . font-lock-keyword-face)
    ;; Make brackets of all forms be keywords
    '("[][<>{}]+" . font-lock-keyword-face)
    ;; Keywords
    (list (concat 
	   "[][ \t\f\n\r()<>{}/%]"	;delimiter
	   "\\("
	   (mapconcat 'identity
		      '("begin" "end" 
			"save" "restore" "gsave" "grestore"
			;; Any delimited name ending in 'def'
			"[a-zA-Z0-9-._]*def"
			"[Dd]efine[a-zA-Z0-9-._]*")
		      "\\|")
	   "\\)"
	   "\\([][ \t\f\n\r()<>{}/%]\\|$\\)" ;delimiter
	   )
	  1 'font-lock-keyword-face)))
   "Expressions to highlight in Postscript buffers.")
(put 'postscript-mode 'font-lock-defaults '(postscript-font-lock-keywords))

(if ps-mode-syntax-table
    nil
  (let ((i 0))
    (setq ps-mode-syntax-table (copy-syntax-table nil))
    (while (< i 256)
      (or (= (char-syntax i ps-mode-syntax-table) ?w)
          (modify-syntax-entry i  "_"     ps-mode-syntax-table))
      (setq i (1+ i)))
    (modify-syntax-entry ?\   " "     ps-mode-syntax-table)
    (modify-syntax-entry ?\t  " "     ps-mode-syntax-table)
    (modify-syntax-entry ?\f  " "     ps-mode-syntax-table)
    (modify-syntax-entry ?\r  " "     ps-mode-syntax-table)
    (modify-syntax-entry ?\%  "<"     ps-mode-syntax-table)
    (modify-syntax-entry ?\n  ">"     ps-mode-syntax-table)
    (modify-syntax-entry ?\\  "\\"    ps-mode-syntax-table)
    (modify-syntax-entry ??   "_"     ps-mode-syntax-table)
    (modify-syntax-entry ?_   "_"     ps-mode-syntax-table)
    (modify-syntax-entry ?.   "_"     ps-mode-syntax-table)
    (modify-syntax-entry ?/   "'"     ps-mode-syntax-table)
    (if ps-balanced-string-syntax-p
        (progn
          (modify-syntax-entry ?\(  "\"\)"  ps-mode-syntax-table)
          (modify-syntax-entry ?\)  "\"\(" ps-mode-syntax-table))
        (progn
          ;; This isn't correct, but Emacs syntax stuff
          ;;  has no way to deal with string syntax which uses
          ;;  different open and close characters.  Sigh.
          (modify-syntax-entry ?\(  "("     ps-mode-syntax-table)
          (modify-syntax-entry ?\)  ")"     ps-mode-syntax-table)))
    (modify-syntax-entry ?\[  "(\]"   ps-mode-syntax-table)
    (modify-syntax-entry ?\]  ")\["   ps-mode-syntax-table)
    (modify-syntax-entry ?\{  "\(\}"  ps-mode-syntax-table)
    (modify-syntax-entry ?\}  "\)\{"  ps-mode-syntax-table)
    (modify-syntax-entry ?/   "' p"   ps-mode-syntax-table)
    ))


;;;###autoload
(defun postscript-mode ()
  "Major mode for editing PostScript files.

\\[ps-execute-buffer] will send the contents of the buffer to the NeWS
server using psh(1).  \\[ps-execute-region] sends the current region.
\\[ps-shell] starts an interactive psh(1) window which will be used for
subsequent \\[ps-execute-buffer] or \\[ps-execute-region] commands.

In this mode, TAB and \\[indent-region] attempt to indent code
based on the position of {}, [], and begin/end pairs.  The variable
ps-indent-level controls the amount of indentation used inside
arrays and begin/end pairs.  

\\{ps-mode-map}

\\[postscript-mode] calls the value of the variable postscript-mode-hook 
with no args, if that value is non-nil."
  (interactive)
  (kill-all-local-variables)
  (use-local-map ps-mode-map)
  (set-syntax-table ps-mode-syntax-table)
  (make-local-variable 'comment-start)
  (setq comment-start "% ")
  (make-local-variable 'comment-start-skip)
  (setq comment-start-skip "%+ *")
  (make-local-variable 'comment-column)
  (setq comment-column 40)
  (make-local-variable 'indent-line-function)
  (setq indent-line-function 'ps-indent-line)
  (make-local-variable 'tab-stop-list)
  (setq tab-stop-list ps-tab-stop-list)
  (make-local-variable 'page-delimiter)
  (setq page-delimiter "^showpage")
  (make-local-variable 'parse-sexp-ignore-comments)
  (setq parse-sexp-ignore-comments t)
  (setq mode-name "PostScript")
  (setq major-mode 'postscript-mode)
  (run-hooks 'ps-mode-hook) ; bad name!  Kept for compatibility.
  (run-hooks 'postscript-mode-hook)
  )

(defun ps-tab ()
  "Command assigned to the TAB key in PostScript mode."
  (interactive)
  (if (save-excursion (skip-chars-backward " \t") (bolp))
      (ps-indent-line)
    (save-excursion
      (ps-indent-line))))

(defun ps-indent-line ()
  "Indents a line of PostScript code."
  (interactive)
  (beginning-of-line)
  (delete-horizontal-space)
  (if (not (or (looking-at "%%")	; "%%" comments stay at left margin
	       (ps-top-level-p)))
      (if (and (< (point) (point-max))
	       (eq ?\) (char-syntax (char-after (point)))))
	  (ps-indent-close)		; indent close-delimiter
	(if (looking-at "\\(dict\\|class\\)?end\\|cdef\\|grestore\\|>>")
	    (ps-indent-end)		; indent end token
	  (ps-indent-in-block)))))	; indent line after open delimiter
  
;(defun ps-open ()
;  (interactive)
;  (insert last-command-char))

(defun ps-insert-d-char (arg)
  "Awful hack to make \"end\" and \"cdef\" keywords indent themselves."
  (interactive "p")
  (insert-char last-command-char arg)
  (save-excursion
    (beginning-of-line)
    (if (looking-at "^[ \t]*\\(\\(dict\\|class\\)?end\\|cdef\\|grestore\\)")
	(progn
	  (delete-horizontal-space)
	  (ps-indent-end)))))

(defun ps-close ()
  "Inserts and indents a close delimiter."
  (interactive)
  (insert last-command-char)
  (backward-char 1)
  (ps-indent-close)
  (forward-char 1)
  (blink-matching-open))

(defun ps-indent-close ()
  "Internal function to indent a line containing a an array close delimiter."
  (if (save-excursion (skip-chars-backward " \t") (bolp))
      (let (x (oldpoint (point)))
	(forward-char) (backward-sexp)	;XXX
	(if (and (eq 1 (count-lines (point) oldpoint))
		 (> 1 (- oldpoint (point))))
	    (goto-char oldpoint)
	  (beginning-of-line)
	  (skip-chars-forward " \t")
	  (setq x (current-column))
	  (goto-char oldpoint)
	  (delete-horizontal-space)
	  (indent-to x)))))

(defun ps-indent-end ()
  "Indent an \"end\" token or array close delimiter."
  (let ((goal (ps-block-start)))
    (if (not goal)
	(indent-relative)
      (setq goal (save-excursion
		   (goto-char goal) (back-to-indentation) (current-column)))
      (indent-to goal))))

(defun ps-indent-in-block ()
  "Indent a line which does not open or close a block."
  (let ((goal (ps-block-start)))
    (setq goal (save-excursion
		 (goto-char goal)
		 (back-to-indentation)
		 (if (bolp)
		     ps-indent-level
		   (back-to-indentation)
		   (+ (current-column) ps-indent-level))))
    (indent-to goal)))

;;; returns nil if at top-level, or char pos of beginning of current block
(defun ps-block-start ()
  "Returns the character position of the character following the nearest
enclosing `[' `{' or `begin' keyword."
  (save-excursion
    (let ((open (condition-case nil
                    (save-excursion
                      (backward-up-list 1)
                      (1+ (point)))
                  (error nil))))
      (ps-begin-end-hack open))))

(defun ps-begin-end-hack (start)
  "Search backwards from point to START for enclosing `begin' and returns the
character number of the character following `begin' or START if not found."
  (save-excursion
    (let ((depth 1))
      (while (and (> depth 0)
		  (or (re-search-backward "^[ \t]*\\(dict\\|class\\)?\\(end\\|grestore\\|>>\\)\\|\\(begin\\|gsave\\|<<\\)[ \t]*\\(%.*\\)*$"
                                          start t)
		      (re-search-backward "^[ \t]*cdef.*$" start t)))
 	(setq depth (if (looking-at "[ \t]*\\(dict\\|class\\)?\\(end\\|grestore\\|>>\\)")
			(1+ depth) (1- depth))))
      (if (not (eq 0 depth))
	  start
	(forward-word 1)
	(point)))))

(defun ps-top-level-p ()
  "Awful test to see whether we are inside some sort of PostScript block."
  (and (condition-case nil
	   (not (scan-lists (point) -1 1))
	 (error t))
       (not (ps-begin-end-hack nil))))

;;; initialize the keymap if it doesn't already exist
(if (null ps-mode-map)
    (progn
      (setq ps-mode-map (make-sparse-keymap))
      (set-keymap-name ps-mode-map 'ps-mode-map)
      ;;(define-key ps-mode-map "d" 'ps-insert-d-char)
      ;;(define-key ps-mode-map "f" 'ps-insert-d-char)
      ;;(define-key ps-mode-map "{" 'ps-open)
      ;;(define-key ps-mode-map "}" 'ps-close)
      ;;(define-key ps-mode-map "[" 'ps-open)
      ;;(define-key ps-mode-map "]" 'ps-close)
      (define-key ps-mode-map "\t" 'ps-tab)
      (define-key ps-mode-map "\C-c\C-c" 'ps-execute-buffer)
      (define-key ps-mode-map "\C-c|" 'ps-execute-region)
      ;; make up yout mind! -- the below or the above?
      (define-key ps-mode-map "\C-c!" 'ps-shell)
      ))

(defun ps-execute-buffer ()
  "Send the contents of the buffer to a printer or NeWS server."
  (interactive)
  (save-excursion
    (mark-whole-buffer)
    (ps-execute-region (point-min) (point-max))))

(defun ps-execute-region (start end)
  "Send the region between START and END to a printer or NeWS server.
You should kill any existing *PostScript* buffer unless you want the
PostScript text to be executed in that process."
  (interactive "r")
  (let ((start (min (point) (mark)))
	(end (max (point) (mark))))
    (condition-case nil
	(process-send-string "PostScript" (buffer-substring start end))
      (error (shell-command-on-region 
              start end
              (mapconcat 'identity ps-postscript-command " ")
              nil)))))

(defun ps-shell ()
  "Start a shell communicating with a PostScript printer or NeWS server."
  (interactive)
  (require 'shell)
  (switch-to-buffer-other-window
    (apply 'make-comint
           "PostScript"
           (car ps-postscript-command)
           nil
           (cdr ps-postscript-command)))
  (make-local-variable 'shell-prompt-pattern)
; (setq shell-prompt-pattern "PS>")
  (setq shell-prompt-pattern "GS>")
; (process-send-string "PostScript" "executive\n")
  )

;; XEmacs addition
;;;###autoload
(add-to-list 'auto-mode-alist '("\\.c?ps\\'" . postscript-mode))
