;;; ecb-eshell.el --- eshell integration for the ECB.

;; Copyright (C) 2000 - 2005 Jesper Nordenberg,
;;                           Klaus Berndl,
;;                           Kevin A. Burton,
;;                           Free Software Foundation, Inc.

;; Author: Klaus Berndl <klaus.berndl@sdm.de>
;;         Kevin A. Burton <burton@openprivacy.org>
;; Maintainer: Klaus Berndl <klaus.berndl@sdm.de>
;; Keywords: browser, code, programming, tools
;; Created: 2001

;; 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 2, 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
;; GNU Emacs; see the file COPYING.  If not, write to the Free Software
;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

;; $Id$

;;; Commentary:

;; This package provides eshell integration for the ECB.  This basically allows
;; you to jump to the eshell in the compilation window, sync up the current
;; eshell with the current ECB buffer and run commands without getting in the
;; way.
;;
;; It provides the following features:
;;
;; - ability to jump to the eshell buffer within the compilation window ( C-.e )
;;   If the eshell isn't running it will be started
;;
;; - expands the compilation window when you run commands.  So for example it
;;   allows you to view the eshell in minimized mode and then when you run 'ls'
;;   the window automatically expands.
;;
;; - Synchronizes the current directory of the eshell with the current buffer
;;   of the either the edit-window or the ecb-windows.
;;
;; - Provides smart window layout of the eshell buffer.  This makes sure that
;;   the eshell is taking up the exact amount of space and that nothing is
;;   hidden.
;; 
;; The goal is to make it easy to jump to a command prompt to run OS level
;; commands.  
;; 
;; If you enjoy this software, please consider a donation to the EFF
;; (http://www.eff.org)

;;; History:

;; For the ChangeLog of this file see the CVS-repository. For a complete
;; history of the ECB-package see the file NEWS.

;;; Design:
;;
;; Syncing the current buffer with the eshell is done two ways.  If the buffer
;; is visible in a window, we always resync.  If it is not visible then
;; ecb-eshell-goto-eshell will sync up when the user goes to the eshell
;; buffer.
;;
;; Integrating of eshell is mostly done by advicing the command `eshell' which
;; uses the mechanism of the display-buffer (adviced version).


;;; Code:

(eval-when-compile
  (require 'silentcomp))

(require 'ecb-util)
(require 'ecb-compilation)
(require 'ecb-common-browser)

(silentcomp-defvar eshell-buffer-name)
(silentcomp-defun eshell)
(silentcomp-defun eshell/cd)
(silentcomp-defun eshell-send-input)
(silentcomp-defun eshell-bol)

(defgroup ecb-eshell nil
  "Settings for eshell integration within the ECB."
  :group 'ecb
  :prefix "ecb-eshell-")

(defcustom ecb-eshell-enlarge-when-eshell t
  "*Enlarge the compile-window if it is selected by `eshell'.
This takes only effect if the command `eshell' is called!"
  :group 'ecb-eshell
  :type 'boolean)

(defcustom ecb-eshell-fit-window-to-command-output t
  "*Fit the compile-window after an eshell-command to the output.
This is done by the function `ecb-eshell-fit-window-to-output' which is added
to `eshell-post-command-hook' ie. which is running autom. after each
eshell-command."
  :group 'ecb-eshell
  :type 'boolean)

(defcustom ecb-eshell-auto-activate nil
  "*Startup the eshell and display it in the compile-window.
If current layout does not display a compile-window \(see
`ecb-compile-window-height') then nothing is done."
  :group 'ecb-eshell
  :type 'boolean)

;; TODO: Klaus Berndl <klaus.berndl@sdm.de>: was ecb-eshell-synchronize -->
;; rename in texi and also to ecb-upgrade (also with value-upgrade!)
(defcustom ecb-eshell-buffer-sync 'basic
  "*Synchronize eshell with the default-directory of current source-buffer.

This option takes only effect if a permanant compile-window is used in the
current layout.

If 'always then the synchronization takes place always a buffer
changes in the edit window and if after this the
default-directory of the new edit-buffer is different from the
default-directory of the current eshell-buffer. If value is nil
then never a synchronization will take place. If a list of
major-modes then only if the `major-mode' of the new buffer
belongs NOT to this list.

If the special value 'basic is set then ECB uses the setting of the option
`ecb-basic-buffer-sync'."
  :group 'ecb-eshell
  :type '(radio :tag "Synchronize the eshell if in compile-window."
                (const :tag "Use basic value" :value basic)
                (const :tag "Always" :value always)
                (const :tag "Never" nil)
                (repeat :tag "Not with these modes"
                        (symbol :tag "mode"))))
    

(defcustom ecb-eshell-buffer-sync-delay 'basic
  "*Time Emacs must be idle before the eshell-buffer of ECB is synchronized.
Synchronizing is done with the current source displayed in the edit window. If
nil then there is no delay, means synchronization takes place immediately. A
small value of about 0.25 seconds saves CPU resources and you get even though
almost the same effect as if you set no delay.

If the special value 'basic is set then ECB uses the setting of the option
`ecb-basic-buffer-sync-delay'."
  :group 'ecb-eshell
  :type '(radio (const :tag "Use basic value" :value basic)
                (const :tag "No synchronizing delay" :value nil)
                (number :tag "Idle time before synchronizing" :value 2))
  :set (function (lambda (symbol value)
                   (set symbol value)
                   (if (and (boundp 'ecb-minor-mode)
                            ecb-minor-mode)
                       (ecb-activate-ecb-autocontrol-function
                        value 'ecb-analyse-buffer-sync))))
  :initialize 'custom-initialize-default)
  


(defvar ecb-eshell-pre-command-point nil
  "Point in the buffer we are at before we executed a command.")

(defvar ecb-eshell-buffer-list nil
  "List of eshell-buffers created until now.
Background: `eshell' creates new eshell-buffers with `generate-new-buffer' if
called with an prefix arg!")


(defecb-advice-set ecb-eshell-adviced-functions
  "These functions of eshell are adviced if ehsell is active during ECB is
active.")

(defecb-advice eshell around ecb-eshell-adviced-functions
  "Ensure that ehsell is running in the ECB-compile-window if any."
  ;; we tell ECB to handle the eshell-buffers as compilation-buffers so they
  ;; will be displayed in the compile-window (if any). We must add this as
  ;; regexp because ehsell can open new eshell-buffers with a name created by
  ;; generate-new-buffer-name! This approach is not completely save because if
  ;; a users changes `eshell-buffer-name' during acivated ECB we get not
  ;; informed about this and maybe we can handle the new buffer-name of eshell
  ;; not as compilation-buffer. But we have no other chance: Adding the return
  ;; value of `eshell' in the advice to `ecb-compilation-buffer-names-internal'
  ;; does not help because `eshell' uses the new buffer-name already for
  ;; `pop-to-buffer'. So an after advice would add the new buffer-name to late
  ;; and a before-advice does not know the new-buffer name. The only way would
  ;; be to reimplement the whole `eshell'-code in an around advice but this is
  ;; not related to the benefit. IMO is it very improbably that a user changes
  ;; `eshell-buffer-name' at all...
  (let ((new-elem (cons (concat ".*"
                                (regexp-quote eshell-buffer-name)
                                ".*")
                        t)))
    (if ecb-compile-window-height
        (progn
          (add-to-list 'ecb-compilation-buffer-names-internal new-elem)
          (add-to-list 'ecb-compilation-major-modes-internal 'eshell-mode))
      ;; if we have no persistent compile-window we do not handle eshell autom.
      ;; as compilation-buffer. If the user wants this then he has to modify
      ;; `ecb-compilation-buffer-names' and/or `ecb-compilation-major-modes'.
      ;; Therefore we remove the new-elem here from the internal lists.
      (setq ecb-compilation-buffer-names-internal
            (delete new-elem ecb-compilation-buffer-names-internal))
      (setq ecb-compilation-major-modes-internal
            (delete 'eshell-mode ecb-compilation-major-modes-internal))))
  
  ;; maybe we have to auto toggle our compile-window if temporally hidden
  (when (equal 'hidden (ecb-compile-window-state))
    (ecb-layout-debug-error "eshell around-advice: comp-win will be toggled.")
    (ecb-toggle-compile-window 1))

  (ecb-activate-ecb-autocontrol-function ecb-eshell-buffer-sync-delay
                                         'ecb-eshell-buffer-sync)
  ;; some hooks
  (add-hook 'eshell-post-command-hook 'ecb-eshell-recenter)
  (add-hook 'eshell-post-command-hook 'ecb-eshell-fit-window-to-output)
  (add-hook 'eshell-pre-command-hook 'ecb-eshell-precommand-hook)
  (add-hook 'window-size-change-functions 'ecb-eshell-window-size-change)

  ;; run `eshell' --------------------------------------------
  (ecb-eshell-save-buffer-history
   ad-do-it)
  ;; ---------------------------------------------------------

  ;; some post processing

  ;; add the buffer of the buffer used/created by `eshell' to
  ;; `ecb-eshell-buffer-list'
  (add-to-list 'ecb-eshell-buffer-list ad-return-value)

  (when ecb-eshell-enlarge-when-eshell
    (ecb-toggle-compile-window-height 1))

  ;;always recenter because if the point is at the top of the eshell buffer
  ;;and we switch to it the user is not going to be able to type a command
  ;;right away.
  (ecb-eshell-recenter)  
  
  ;;sync to the current buffer
  (ecb-eshell-buffer-sync))
  
  

(defun ecb-eshell-activate-integration ()
  "Does all necessary to activate the eshell-integration. But this doesn not
load or activate eshell - it just prepares ECB to work perfectly with eshell."
  (ecb-enable-advices 'ecb-eshell-adviced-functions))

(defun ecb-eshell-deactivate-integration ()
  (ecb-disable-advices 'ecb-eshell-adviced-functions)
  (ecb-stop-autocontrol/sync-function 'ecb-eshell-buffer-sync)
  (remove-hook 'eshell-post-command-hook 'ecb-eshell-recenter)
  (remove-hook 'eshell-post-command-hook 'ecb-eshell-fit-window-to-output)
  (remove-hook 'eshell-pre-command-hook 'ecb-eshell-precommand-hook)
  (remove-hook 'window-size-change-functions 'ecb-eshell-window-size-change))

(defecb-autocontrol/sync-function ecb-eshell-buffer-sync nil ecb-eshell-buffer-sync t
  "Synchronize the eshell with the directory of current source-buffer.
This is only done if the eshell is currently visible in the compile-window of
ECB and if either this function is called interactively or
`ecb-eshell-buffer-sync' is not nil."
  (when (and (equal (selected-frame) ecb-frame)
             (ecb-compile-window-live-p)
             (ecb-point-in-edit-window-number))
    (let* ((my-eshell-buffer
            ;; nil or a living eshell-buffer in the ecb-compile-window
            (car (member (window-buffer ecb-compile-window)
                         ecb-eshell-buffer-list)))
           (my-reference-directory default-directory)
           (my-eshell-directory (and (bufferp my-eshell-buffer)
                                     (with-current-buffer my-eshell-buffer
                                       default-directory))))
      (when (and (bufferp my-eshell-buffer)
                 (stringp my-reference-directory)
                 (stringp my-eshell-directory)
                 (not (ecb-string= (ecb-fix-filename my-reference-directory)
                                   (ecb-fix-filename my-eshell-directory))))
        (ecb-eshell-save-buffer-history
         (with-current-buffer my-eshell-buffer
           ;; make sure we have a clean eshell-command-line
           (goto-char (point-max))
           (eshell-bol)
           (delete-region (point) (point-at-eol))
           ;;change the directory without showing the cd command
           (eshell/cd my-reference-directory))
           
         ;;execute the command
         (save-selected-window
           (select-window ecb-compile-window)
           (eshell-send-input)))
        
        (ecb-eshell-recenter)
        
        ;; we need to make sure that that the eshell buffer isn't at the
        ;; top of the buffer history list just because we implicitly
        ;; changed its directory and switched to it. It might not be a
        ;; good idea in the long term to put it all the way at the end of
        ;; the history list but it is better than leaving it at the top.
        (bury-buffer eshell-buffer-name)))))

(defmacro ecb-eshell-save-buffer-history (&rest body)
  "Protect the buffer-list so that the eshell buffer name is not placed early
in the buffer list or at all if it currently doesn't exist."
  (let ((eshell-buffer-list (make-symbol "my-buffer-list")))
    `(let ((,eshell-buffer-list (ecb-frame-parameter (selected-frame)
                                                     'buffer-list)))
       (unwind-protect
           (progn
             ,@body)
         (modify-frame-parameters nil (list (cons 'buffer-list
                                                  ,eshell-buffer-list)))))))

(defun ecb-eshell-recenter(&optional display-errors)
  "Recenter the eshell window so that the prompt is at the buffer-end."
  (interactive (list t))

  (if (and (equal (selected-frame) ecb-frame)
           (ecb-compile-window-live-p)
           ;; the buffer in the ecb-compile-window is a living eshell-buffer
           (member (window-buffer ecb-compile-window)
                   ecb-eshell-buffer-list))
      (save-selected-window
        (select-window ecb-compile-window)
        (goto-char (point-max))
        (recenter -2))
    (when display-errors
      (ecb-error "Eshell not running or compile-window not visible!"))))

(defun ecb-eshell-precommand-hook ()
  ;;use the eshell-pre-command-hook to set the point.
  (setq ecb-eshell-pre-command-point (point)))


(defun ecb-eshell-fit-window-to-output()
  "Fit window of eshell to the output of last command. This function is added
to `eshell-post-command-hook' and only called there. This function tries to
fit the height of the compile-window best to the last command-output. The
algorithm fit the window to the height of the last command-output but do not
enlarge the compile-window over half of the frame-height and also not below
`ecb-compile-window-height' (in lines)."
  (when (and (equal (selected-frame) ecb-frame)
             (ecb-compile-window-live-p)
             ;; the buffer in the ecb-compile-window is a living eshell-buffer
             (member (window-buffer ecb-compile-window)
                     ecb-eshell-buffer-list))

    ;; fit the window to the height of the last command-output but do not
    ;; enlarge the compile-window over half of the frame-height and also not
    ;; below `ecb-compile-window-height' (in lines).
    (when (and ecb-eshell-fit-window-to-command-output
               (integer-or-marker-p ecb-eshell-pre-command-point))
      (let* ((compile-window-height-lines
              (ecb-normalize-number ecb-compile-window-height
                                    (1- (frame-height))))
             (ecb-enlarged-compilation-window-max-height
              (max (min (with-current-buffer (window-buffer ecb-compile-window)
                          ;; we want to see the old command line too and 2
                          ;; must be added because we have a modeline and one
                          ;; empty line cause of the (recenter -2) in
                          ;; `ecb-eshell-recenter'. For XEmacs it would be
                          ;; better to check if a horiz. scrollbar is used.
                          ;; This causes the one line more we need for XEmacs
                          (+ (if ecb-running-xemacs 4 3)
                             (count-lines ecb-eshell-pre-command-point
                                          (point))))
                        (/ (1- (frame-height)) 2))
                   compile-window-height-lines)))
        (ecb-toggle-compile-window-height 1)
        (ecb-eshell-recenter))
        
      ;;reset
      (setq ecb-eshell-pre-command-point nil))))


(defun ecb-eshell-auto-activate-hook()
  "Activate the eshell when ECB is activated. See `ecb-eshell-auto-activate'."
  (when ecb-eshell-auto-activate
    (ignore-errors (eshell))))

(defun ecb-eshell-window-size-change(frame)
  "Called when we change window sizes so that the eshell can resize."
  (when (and ecb-minor-mode
             (equal frame ecb-frame))
    (ignore-errors (ecb-eshell-recenter))))

(add-hook 'ecb-activate-hook 'ecb-eshell-auto-activate-hook)

(silentcomp-provide 'ecb-eshell)

;;; ecb-eshell.el ends here
