+ "Return the relative name of the main file."
+ (let* ((file (or tex-main-file
+ ;; Compatibility with AUCTeX.
+ (with-no-warnings
+ (when (boundp 'TeX-master)
+ (cond ((stringp TeX-master)
+ (make-local-variable 'tex-main-file)
+ (setq tex-main-file TeX-master))
+ ((and (eq TeX-master t) buffer-file-name)
+ (file-relative-name buffer-file-name)))))
+ ;; Try to guess the main file.
+ (if (not buffer-file-name)
+ (error "Buffer is not associated with any file")
+ (file-relative-name
+ (if (save-excursion
+ (goto-char (point-min))
+ (re-search-forward tex-start-of-header
+ (+ (point) 10000) t))
+ ;; This is the main file.
+ buffer-file-name
+ ;; This isn't the main file, let's try to find better,
+ (or (tex-guess-main-file)
+ (tex-guess-main-file 'sub)
+ ;; (tex-guess-main-file t)
+ buffer-file-name)))))))
+ (if (or (file-exists-p file) (string-match "\\.tex\\'" file))
+ file (concat file ".tex"))))
+
+(defun tex-summarize-command (cmd)
+ (if (not (stringp cmd)) ""
+ (mapconcat 'identity
+ (mapcar (lambda (s) (car (split-string s)))
+ (split-string cmd "\\s-*\\(?:;\\|&&\\)\\s-*"))
+ "&")))
+
+(defun tex-uptodate-p (file)
+ "Return non-nil if FILE is not uptodate w.r.t the document source files.
+FILE is typically the output DVI or PDF file."
+ ;; We should check all the files included !!!
+ (and
+ ;; Clearly, the target must exist.
+ (file-exists-p file)
+ ;; And the last run must not have asked for a rerun.
+ ;; FIXME: this should check that the last run was done on the same file.
+ (let ((buf (condition-case nil (tex-shell-buf) (error nil))))
+ (when buf
+ (with-current-buffer buf
+ (save-excursion
+ (goto-char (point-max))
+ (and (re-search-backward
+ (concat
+ "(see the transcript file for additional information)"
+ "\\|^Output written on .*"
+ (regexp-quote (file-name-nondirectory file))
+ " (.*)\\.") nil t)
+ (> (save-excursion
+ (or (re-search-backward "\\[[0-9]+\\]" nil t)
+ (point-min)))
+ (save-excursion
+ (or (re-search-backward "Rerun" nil t)
+ (point-min)))))))))
+ ;; And the input files must not have been changed in the meantime.
+ (let ((files (if (and tex-use-reftex
+ (fboundp 'reftex-scanning-info-available-p)
+ (reftex-scanning-info-available-p))
+ (reftex-all-document-files)
+ (list (file-name-directory (expand-file-name file)))))
+ (ignored-dirs-re
+ (concat
+ (regexp-opt
+ (delq nil (mapcar (lambda (s) (if (eq (aref s (1- (length s))) ?/)
+ (substring s 0 (1- (length s)))))
+ completion-ignored-extensions))
+ t) "\\'"))
+ (uptodate t))
+ (while (and files uptodate)
+ (let ((f (pop files)))
+ (if (and (file-directory-p f)
+ ;; Avoid infinite loops.
+ (not (file-symlink-p f)))
+ (unless (string-match ignored-dirs-re f)
+ (setq files (nconc
+ (directory-files f t tex-input-files-re)
+ files)))
+ (when (file-newer-than-file-p f file)
+ (setq uptodate nil)))))
+ uptodate)))
+
+
+(autoload 'format-spec "format-spec")
+
+(defvar tex-executable-cache nil)
+(defun tex-executable-exists-p (name)
+ "Like `executable-find' but with a cache."
+ (let ((cache (assoc name tex-executable-cache)))
+ (if cache (cdr cache)
+ (let ((executable (executable-find name)))
+ (push (cons name executable) tex-executable-cache)
+ executable))))
+
+(defun tex-command-executable (cmd)
+ (let ((s (if (stringp cmd) cmd (eval (car cmd)))))
+ (substring s 0 (string-match "[ \t]\\|\\'" s))))
+
+(defun tex-command-active-p (cmd fspec)
+ "Return non-nil if the CMD spec might need to be run."
+ (let ((in (nth 1 cmd))
+ (out (nth 2 cmd)))
+ (if (stringp in)
+ (let ((file (format-spec in fspec)))
+ (when (file-exists-p file)
+ (or (not out)
+ (file-newer-than-file-p
+ file (format-spec out fspec)))))
+ (when (and (eq in t) (stringp out))
+ (not (tex-uptodate-p (format-spec out fspec)))))))
+
+(defun tex-compile-default (fspec)
+ "Guess a default command given the `format-spec' FSPEC."
+ ;; TODO: Learn to do latex+dvips!
+ (let ((cmds nil)
+ (unchanged-in nil))
+ ;; Only consider active commands.
+ (dolist (cmd tex-compile-commands)
+ (when (tex-executable-exists-p (tex-command-executable cmd))
+ (if (tex-command-active-p cmd fspec)
+ (push cmd cmds)
+ (push (nth 1 cmd) unchanged-in))))
+ ;; If no command seems to be applicable, arbitrarily pick the first one.
+ (setq cmds (if cmds (nreverse cmds) (list (car tex-compile-commands))))
+ ;; Remove those commands whose input was considered stable for
+ ;; some other command (typically if (t . "%.pdf") is inactive
+ ;; then we're using pdflatex and the fact that the dvi file
+ ;; is inexistent doesn't matter).
+ (let ((tmp nil))
+ (dolist (cmd cmds)
+ (unless (member (nth 1 cmd) unchanged-in)
+ (push cmd tmp)))
+ ;; Only remove if there's something left.
+ (if tmp (setq cmds (nreverse tmp))))
+ ;; Remove commands whose input is not uptodate either.
+ (let ((outs (delq nil (mapcar (lambda (x) (nth 2 x)) cmds)))
+ (tmp nil))
+ (dolist (cmd cmds)
+ (unless (member (nth 1 cmd) outs)
+ (push cmd tmp)))
+ ;; Only remove if there's something left.
+ (if tmp (setq cmds (nreverse tmp))))
+ ;; Select which file we're going to operate on (the latest).
+ (let ((latest (nth 1 (car cmds))))
+ (dolist (cmd (prog1 (cdr cmds) (setq cmds (list (car cmds)))))
+ (if (equal latest (nth 1 cmd))
+ (push cmd cmds)
+ (unless (eq latest t) ;Can't beat that!
+ (if (or (not (stringp latest))
+ (eq (nth 1 cmd) t)
+ (and (stringp (nth 1 cmd))
+ (file-newer-than-file-p
+ (format-spec (nth 1 cmd) fspec)
+ (format-spec latest fspec))))
+ (setq latest (nth 1 cmd) cmds (list cmd)))))))
+ ;; Expand the command spec into the actual text.
+ (dolist (cmd (prog1 cmds (setq cmds nil)))
+ (push (cons (eval (car cmd)) (cdr cmd)) cmds))
+ ;; Select the favorite command from the history.
+ (let ((hist tex-compile-history)
+ re hist-cmd)
+ (while hist
+ (setq hist-cmd (pop hist))
+ (setq re (concat "\\`"
+ (regexp-quote (tex-command-executable hist-cmd))
+ "\\([ \t]\\|\\'\\)"))
+ (dolist (cmd cmds)
+ ;; If the hist entry uses the same command and applies to a file
+ ;; of the same type (e.g. `gv %r.pdf' vs `gv %r.ps'), select cmd.
+ (and (string-match re (car cmd))
+ (or (not (string-match "%[fr]\\([-._[:alnum:]]+\\)" (car cmd)))
+ (string-match (regexp-quote (match-string 1 (car cmd)))
+ hist-cmd))
+ (setq hist nil cmds (list cmd)))))
+ ;; Substitute and return.
+ (if (and hist-cmd
+ (string-match (concat "[' \t\"]" (format-spec "%r" fspec)
+ "\\([;&' \t\"]\\|\\'\\)") hist-cmd))
+ ;; The history command was already applied to the same file,
+ ;; so just reuse it.
+ hist-cmd
+ (if cmds (format-spec (caar cmds) fspec))))))
+
+(defun tex-compile (dir cmd)
+ "Run a command CMD on current TeX buffer's file in DIR."
+ ;; FIXME: Use time-stamps on files to decide the next op.
+ (interactive
+ (let* ((file (tex-main-file))
+ (default-directory
+ (prog1 (file-name-directory (expand-file-name file))
+ (setq file (file-name-nondirectory file))))
+ (root (file-name-sans-extension file))
+ (fspec (list (cons ?r (shell-quote-argument root))
+ (cons ?f (shell-quote-argument file))))
+ (default (tex-compile-default fspec)))
+ (list default-directory
+ (completing-read
+ (format "Command [%s]: " (tex-summarize-command default))
+ (mapcar (lambda (x)
+ (list (format-spec (eval (car x)) fspec)))
+ tex-compile-commands)
+ nil nil nil 'tex-compile-history default))))
+ (save-some-buffers (not compilation-ask-about-save) nil)
+ (if (tex-shell-running)
+ (tex-kill-job)
+ (tex-start-shell))
+ (tex-send-tex-command cmd dir))