;;; ess-trns.el --- Support for manipulating S transcript files  -*- lexical-binding: t; -*-

;; Copyright (C) 1989-2020 Free Software Foundation, Inc.
;; Author: David Smith <dsmith@stats.adelaide.edu.au>
;; Maintainer: ESS-core <ESS-core@r-project.org>

;; This file is part of GNU Emacs.

;;; License:
;;
;; This program 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 3 of the License, or
;; (at your option) any later version.
;;
;; This program 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 this program.  If not, see
;; <http://www.gnu.org/licenses/>

;;; Commentary:

;; Code for dealing with ESS transcripts.

;;; Code:

 ; Requires and autoloads

(require 'ess-mode)
(require 'ess-inf)
(require 'comint)

(declare-function ess-display-help-on-object "ess-help" (object &optional command))

 ; ess-transcript-mode
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; In this section:
;;;;
;;;; * The major mode ess-transcript-mode
;;;; * Commands for ess-transcript-mode
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defcustom ess-transcript-mode-hook nil
  "Hook for customizing ESS transcript mode."
  :group 'ess-hooks
  :type 'hook)

;;*;; Major mode definition
(defvar ess-transcript-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map "\C-c\C-s" #'ess-switch-process)
    (define-key map "\C-c\C-r" #'ess-eval-region)
    (define-key map "\C-c\M-r" #'ess-eval-region-and-go)
    (define-key map "\C-c\C-k" #'ess-force-buffer-current)
    (define-key map "\C-c\C-q" #'ess-quit)
    (define-key map "\C-c\C-j" #'ess-transcript-send-command)
    (define-key map "\C-c\M-j" #'ess-transcript-send-command-and-move)
    (define-key map "\M-\C-a"  #'ess-goto-end-of-function-or-para)
    (define-key map "\M-\C-e"  #'ess-goto-end-of-function-or-para)
    (define-key map "\C-c\C-y" #'ess-switch-to-ESS)
    (define-key map "\C-c\C-z" #'ess-switch-to-end-of-ESS)
    (define-key map "\C-c\C-v" #'ess-display-help-on-object)
    (define-key map "\C-c\C-d" #'ess-dump-object-into-edit-buffer)
    (define-key map "\C-a"     #'comint-bol)
    (define-key map "\M-\t"    #'comint-replace-by-expanded-filename)
    (define-key map "\M-?"     #'comint-dynamic-list-completions)
    (define-key map "\C-c\C-k" #'ess-request-a-process)
    (define-key map "{"        #'skeleton-pair-insert-maybe)
    (define-key map "}"        #'skeleton-pair-insert-maybe)
    (define-key map "\e\C-h"   #'ess-mark-function-or-para)
    (define-key map "\e\C-q"   #'ess-indent-exp)
    (define-key map "\t"       #'ess-indent-command)
    (define-key map "\C-c\C-p" #'comint-previous-prompt)
    (define-key map "\C-c\C-n" #'comint-next-prompt)
    (define-key map "\r"       #'ess-transcript-send-command-and-move)
    (define-key map "\M-\r"    #'ess-transcript-send-command)
    (define-key map "\C-c\r"   #'ess-transcript-copy-command)
    (define-key map "\C-c\C-w" #'ess-transcript-DO-clean-region)
    (define-key map "\C-c\M-c" #'ess-transcript-clean-buffer)
    map)
  "Keymap for `ess-transcript-mode'.")

(easy-menu-define
  ess-transcript-mode-menu ess-transcript-mode-map
  "Menu for use in S transcript mode."
  '("ESS-trans"
    ["Describe"         describe-mode                   t]
    ["About"           (ess-goto-info "Transcript Mode") t]
    ["Send bug report"  ess-submit-bug-report           t]
    "------"
    ["Mark cmd group"   mark-paragraph          t]
    ["Previous prompt"  comint-previous-prompt  t]
    ["Next prompt"      comint-next-prompt      t]
    "------"
    ["Send and move" ess-transcript-send-command-and-move t]
    ["Copy command"  ess-transcript-copy-command	t]
    ["Send command"  ess-transcript-send-command	t]
    ["Clean Region"  ess-transcript-DO-clean-region	t]
    ["Clean Whole Buffer" ess-transcript-clean-buffer	t]
    ["Switch S process" ess-switch-process		t]
    ))

;;;###autoload
(define-derived-mode ess-transcript-mode ess-mode "ESS Transcript"
  "Major mode for transcript files.

Type \\[ess-transcript-send-command] to send a command in the
transcript to the current inferior process. \\[ess-transcript-copy-command]
copies the command but does not execute it, allowing you to edit it in
the process buffer first.

Type \\[ess-transcript-clean-region] to delete all outputs and prompts
in the region, leaving only the commands."
  :group 'ess
  (setq buffer-read-only t
        ess-local-process-name nil
        mode-line-process '(" [" ess-local-process-name "]"))
  ;; TODO: Set this more normally in various major modes
  (unless inferior-ess-prompt ;; For S languages it is set in custom-alist
    (setq inferior-ess-prompt
          ;; Do not anchor to bol with `^'
          (concat "\\("
                  inferior-ess-primary-prompt
                  "\\|"
                  inferior-ess-secondary-prompt
                  "\\)")))
  (setq-local paragraph-start (concat "^" inferior-ess-prompt "\\|^\^L"))
  (setq-local paragraph-separate "^\^L")
  (setq-local comint-use-prompt-regexp t)
  (setq-local comint-prompt-regexp (concat "^" inferior-ess-prompt))
  (setq font-lock-defaults '(ess-build-font-lock-keywords
                             nil nil ((?\. . "w") (?\_ . "w") (?' . ".")))))

;;*;; Commands used in S transcript mode

(defun ess-transcript-send-command ()
  "Send the command at point in the transcript to the ESS process.
The line should begin with a prompt.  The ESS process buffer is displayed if it
is not already."
  (interactive)
  (let* ((proc (or ess-local-process-name
                   (ess-request-a-process "Evaluate into which process? " t)))
         (ess-buf (ess-get-process-buffer proc)))
    (setq ess-local-process-name proc)
    (if (get-buffer-window ess-buf) nil
      (display-buffer ess-buf t))
    (let ((input (inferior-ess-get-old-input)))
      (with-current-buffer ess-buf
        (goto-char (point-max))
        (ess-eval-linewise input)))))

(defun ess-transcript-send-command-and-move ()
  "Send the command on this line, and move point to the next command."
  (interactive)
  (let* ((proc (or ess-local-process-name
                   (ess-request-a-process "Evaluate into which process? " t)))
         (ess-buf (ess-get-process-buffer proc)))
    (setq ess-local-process-name proc)
    (if (get-buffer-window ess-buf) nil
      (display-buffer ess-buf t))
    (let ((input (inferior-ess-get-old-input)))
      (with-current-buffer ess-buf
        (goto-char (point-max))
        (ess-eval-linewise input nil nil nil 1))))
  (comint-next-prompt 1))

(defun ess-transcript-copy-command ()
  "Copy the command at point to the command line of the ESS process."
  (interactive)
  (let* ((proc (or ess-local-process-name
                   (ess-request-a-process "Evaluate into which process? " t)))
         (ess-buf (process-buffer (get-process proc)))
         (input (inferior-ess-get-old-input)))
    (setq ess-local-process-name proc)
    (if (get-buffer-window ess-buf) nil
      (display-buffer ess-buf t))
    (with-current-buffer ess-buf
      (goto-char (point-max))
      (insert input)))
  (ess-switch-to-end-of-ESS))

(defun ess-transcript-clean-region (beg end even-if-read-only)
  "Strip the transcript in the region, leaving only (R/S/Lsp/..) commands.
Deletes any lines not beginning with a prompt, and then removes the
prompt from those lines that remain.  Prefix argument means to
clean even if the buffer is \\[read-only]."
  (interactive "r\nP")
  (unless inferior-ess-prompt
    (error "Cannot clean ESS transcript region in this mode!
 That only works in ess-transcript-mode or inferior-ess-mode ('*R*' etc)."
           ;; Maybe call ess-clean-region-in-new-transcript ?"))
           ))
  (let ((do-toggle (and buffer-read-only even-if-read-only))
        (ess-prompt-rx (if inferior-ess-secondary-prompt
                           (concat "^\\(\\("
                                   inferior-ess-prompt
                                   "\\)\\|\\("
                                   inferior-ess-secondary-prompt
                                   "\\)\\)")
                         (concat "^" inferior-ess-prompt))))
    (save-excursion
      (if do-toggle (setq buffer-read-only nil))
      (save-restriction
        (deactivate-mark)
        (narrow-to-region beg end)
        (goto-char (point-min))
        (delete-non-matching-lines ess-prompt-rx)
        (goto-char (point-min))
        ;; (replace-regexp  *  * ) :
        (while (re-search-forward ess-prompt-rx nil t)
          (replace-match "" nil nil)))

      (if do-toggle (setq buffer-read-only t)))))

(defun ess-transcript-DO-clean-region (beg end)
  "Clean the current via \\[ess-transcript-clean-region] even if the buffer is read-only."
  (interactive "r")
  (ess-transcript-clean-region beg end 'In-ANY-case))

(defun ess-transcript-clean-buffer ()
  "Cleanup the whole buffer.
Use point-min/max to obey `narrow-to-region'."
  (interactive)
  (ess-transcript-clean-region (point-min) (point-max) 'In-ANY-case))

(provide 'ess-trns)

;;; ess-trns.el ends here
