diff options
Diffstat (limited to 'lisp/eshell/em-dirs.el')
-rw-r--r-- | lisp/eshell/em-dirs.el | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/lisp/eshell/em-dirs.el b/lisp/eshell/em-dirs.el new file mode 100644 index 00000000000..642163cb1bd --- /dev/null +++ b/lisp/eshell/em-dirs.el @@ -0,0 +1,563 @@ +;;; em-dirs --- directory navigation commands + +;; Copyright (C) 1999, 2000 Free Sofware Foundation + +;; This file is part of GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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. + +(provide 'em-dirs) + +(eval-when-compile (require 'esh-maint)) + +(defgroup eshell-dirs nil + "Directory navigation involves changing directories, examining the +current directory, maintaining a directory stack, and also keeping +track of a history of the last directory locations the user was in. +Emacs does provide standard Lisp definitions of `pwd' and `cd', but +they lack somewhat in feel from the typical shell equivalents." + :tag "Directory navigation" + :group 'eshell-module) + +;;; Commentary: + +;; The only special feature that Eshell offers in the last-dir-ring. +;; To view the ring, enter: +;; +;; cd = +;; +;; Changing to an index within the ring is done using: +;; +;; cd - ; same as cd -0 +;; cd -4 +;; +;; Or, it is possible to change the first member in the ring which +;; matches a regexp: +;; +;; cd =bcc ; change to the last directory visited containing "bcc" +;; +;; This ring is maintained automatically, and is persisted across +;; Eshell sessions. It is a separate mechanism from `pushd' and +;; `popd', and the two may be used at the same time. + +(require 'ring) +(require 'esh-opt) + +;;; User Variables: + +(defcustom eshell-dirs-load-hook '(eshell-dirs-initialize) + "*A hook that gets run when `eshell-dirs' is loaded." + :type 'hook + :group 'eshell-dirs) + +(defcustom eshell-pwd-convert-function (if (eshell-under-windows-p) + 'expand-file-name + 'identity) + "*The function used to normalize the value of Eshell's `pwd'. +The value returned by `pwd' is also used when recording the +last-visited directory in the last-dir-ring, so it will affect the +form of the list used by 'cd ='." + :type '(radio (function-item file-truename) + (function-item expand-file-name) + (function-item identity) + (function :tag "Other")) + :group 'eshell-dirs) + +(defcustom eshell-ask-to-save-last-dir 'always + "*Determine if the last-dir-ring should be automatically saved. +The last-dir-ring is always preserved when exiting an Eshell buffer. +However, when Emacs is being shut down, this variable determines +whether to prompt the user, or just save the ring. +If set to nil, it means never ask whether to save the last-dir-ring. +If set to t, always ask if any Eshell buffers are open at exit time. +If set to `always', the list-dir-ring will always be saved, silently." + :type '(choice (const :tag "Never" nil) + (const :tag "Ask" t) + (const :tag "Always save" always)) + :group 'eshell-dirs) + +(defcustom eshell-cd-shows-directory nil + "*If non-nil, using `cd' will report the directory it changes to." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-cd-on-directory t + "*If non-nil, do a cd if a directory is in command position." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-directory-change-hook nil + "*A hook to run when the current directory changes." + :type 'hook + :group 'eshell-dirs) + +(defcustom eshell-list-files-after-cd nil + "*If non-nil, call \"ls\" with any remaining args after doing a cd. +This is provided for convenience, since the same effect is easily +achieved by adding a function to `eshell-directory-change-hook' that +calls \"ls\" and references `eshell-last-arguments'." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-pushd-tohome nil + "*If non-nil, make pushd with no arg behave as 'pushd ~' (like `cd'). +This mirrors the optional behavior of tcsh." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-pushd-dextract nil + "*If non-nil, make \"pushd +n\" pop the nth dir to the stack top. +This mirrors the optional behavior of tcsh." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-pushd-dunique nil + "*If non-nil, make pushd only add unique directories to the stack. +This mirrors the optional behavior of tcsh." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-dirtrack-verbose t + "*If non-nil, show the directory stack following directory change. +This is effective only if directory tracking is enabled." + :type 'boolean + :group 'eshell-dirs) + +(defcustom eshell-last-dir-ring-file-name + (concat eshell-directory-name "lastdir") + "*If non-nil, name of the file to read/write the last-dir-ring. +See also `eshell-read-last-dir-ring' and `eshell-write-last-dir-ring'. +If it is nil, the last-dir-ring will not be written to disk." + :type 'file + :group 'eshell-dirs) + +(defcustom eshell-last-dir-ring-size 32 + "*If non-nil, the size of the directory history ring. +This ring is added to every time `cd' or `pushd' is used. It simply +stores the most recent directory locations Eshell has been in. To +return to the most recent entry, use 'cd -' (equivalent to 'cd -0'). +To return to an older entry, use 'cd -N', where N is an integer less +than `eshell-last-dir-ring-size'. To return to the last directory +matching a particular regexp, use 'cd =REGEXP'. To display the +directory history list, use 'cd ='. + +This mechanism is very similar to that provided by `pushd', except +it's far more automatic. `pushd' allows the user to decide which +directories gets pushed, and its size is unlimited. + +`eshell-last-dir-ring' is meant for users who don't use `pushd' +explicity very much, but every once in a while would like to return to +a previously visited directory without having to type in the whole +thing again." + :type 'integer + :group 'eshell-dirs) + +(defcustom eshell-last-dir-unique t + "*If non-nil, `eshell-last-dir-ring' contains only unique entries." + :type 'boolean + :group 'eshell-dirs) + +;;; Internal Variables: + +(defvar eshell-dirstack nil + "List of directories saved by pushd in the Eshell buffer. +Thus, this does not include the current directory.") + +(defvar eshell-last-dir-ring nil + "The last directory that eshell was in.") + +;;; Functions: + +(defun eshell-dirs-initialize () + "Initialize the builtin functions for Eshell." + (make-local-variable 'eshell-variable-aliases-list) + (setq eshell-variable-aliases-list + (append + eshell-variable-aliases-list + '(("-" (lambda (indices) + (if (not indices) + (unless (ring-empty-p eshell-last-dir-ring) + (expand-file-name + (ring-ref eshell-last-dir-ring 0))) + (expand-file-name + (eshell-apply-indices eshell-last-dir-ring indices))))) + ("+" "PWD") + ("PWD" (lambda (indices) + (expand-file-name (eshell/pwd))) t) + ("OLDPWD" (lambda (indices) + (unless (ring-empty-p eshell-last-dir-ring) + (expand-file-name + (ring-ref eshell-last-dir-ring 0)))) t)))) + + (when eshell-cd-on-directory + (make-local-variable 'eshell-interpreter-alist) + (setq eshell-interpreter-alist + (cons (cons 'eshell-lone-directory-p + 'eshell-dirs-substitute-cd) + eshell-interpreter-alist))) + + (make-local-hook 'eshell-parse-argument-hook) + (add-hook 'eshell-parse-argument-hook + 'eshell-parse-user-reference nil t) + (if (eshell-under-windows-p) + (add-hook 'eshell-parse-argument-hook + 'eshell-parse-drive-letter nil t)) + + (when (eshell-using-module 'eshell-cmpl) + (make-local-hook 'pcomplete-try-first-hook) + (add-hook 'pcomplete-try-first-hook + 'eshell-complete-user-reference nil t)) + + (make-local-variable 'eshell-dirstack) + (make-local-variable 'eshell-last-dir-ring) + + (if eshell-last-dir-ring-file-name + (eshell-read-last-dir-ring)) + (unless eshell-last-dir-ring + (setq eshell-last-dir-ring (make-ring eshell-last-dir-ring-size))) + + (make-local-hook 'eshell-exit-hook) + (add-hook 'eshell-exit-hook 'eshell-write-last-dir-ring nil t) + + (add-hook 'kill-emacs-hook 'eshell-save-some-last-dir)) + +(defun eshell-save-some-last-dir () + "Save the list-dir-ring for any open Eshell buffers." + (eshell-for buf (buffer-list) + (if (buffer-live-p buf) + (with-current-buffer buf + (if (and eshell-mode + eshell-ask-to-save-last-dir + (or (eq eshell-ask-to-save-last-dir 'always) + (y-or-n-p + (format "Save last dir ring for Eshell buffer `%s'? " + (buffer-name buf))))) + (eshell-write-last-dir-ring)))))) + +(defun eshell-lone-directory-p (file) + "Test whether FILE is just a directory name, and not a command name." + (and (file-directory-p file) + (or (file-name-directory file) + (not (eshell-search-path file))))) + +(defun eshell-dirs-substitute-cd (&rest args) + "Substitute the given command for a call to `cd' on that name." + (if (> (length args) 1) + (error "%s: command not found" (car args)) + (throw 'eshell-replace-command + (eshell-parse-command "cd" args)))) + +(defun eshell-parse-user-reference () + "An argument beginning with ~ is a filename to be expanded." + (when (and (not eshell-current-argument) + (eq (char-after) ?~)) + (add-to-list 'eshell-current-modifiers 'expand-file-name) + (forward-char) + (char-to-string (char-before)))) + +(defun eshell-parse-drive-letter () + "An argument beginning X:[^/] is a drive letter reference." + (when (and (not eshell-current-argument) + (looking-at "\\([A-Za-z]:\\)\\([^/\\\\]\\|\\'\\)")) + (goto-char (match-end 1)) + (let* ((letter (match-string 1)) + (regexp (concat "\\`" letter)) + (path (eshell-find-previous-directory regexp))) + (concat (or path letter) + (char-to-string directory-sep-char))))) + +(defun eshell-complete-user-reference () + "If there is a user reference, complete it." + (let ((arg (pcomplete-actual-arg))) + (when (string-match "\\`~[a-z]*\\'" arg) + (setq pcomplete-stub (substring arg 1) + pcomplete-last-completion-raw t) + (throw 'pcomplete-completions + (progn + (eshell-read-user-names) + (pcomplete-uniqify-list + (mapcar + (function + (lambda (user) + (file-name-as-directory (cdr user)))) + eshell-user-names))))))) + +(defun eshell/pwd (&rest args) ; ignored + "Change output from `pwd` to be cleaner." + (let* ((path default-directory) + (len (length path))) + (if (and (> len 1) + (eq (aref path (1- len)) directory-sep-char) + (not (and (eshell-under-windows-p) + (string-match "\\`[A-Za-z]:[\\\\/]\\'" path)))) + (setq path (substring path 0 (1- (length path))))) + (if eshell-pwd-convert-function + (setq path (funcall eshell-pwd-convert-function path))) + path)) + +(defun eshell-expand-multiple-dots (path) + "Convert '...' to '../..', '....' to '../../..', etc.. + +With the following piece of advice, you can make this functionality +available in most of Emacs, with the exception of filename completion +in the minibuffer: + + (defadvice expand-file-name + (before translate-multiple-dots + (filename &optional directory) activate) + (setq filename (eshell-expand-multiple-dots filename)))" + (while (string-match "\\.\\.\\(\\.+\\)" path) + (let* ((extra-dots (match-string 1 path)) + (len (length extra-dots)) + replace-text) + (while (> len 0) + (setq replace-text + (concat replace-text + (char-to-string directory-sep-char) "..") + len (1- len))) + (setq path + (replace-match replace-text t t path 1)))) + path) + +(defun eshell-find-previous-directory (regexp) + "Find the most recent last-dir matching REGEXP." + (let ((index 0) + (len (ring-length eshell-last-dir-ring)) + oldpath) + (if (> (length regexp) 0) + (while (< index len) + (setq oldpath (ring-ref eshell-last-dir-ring index)) + (if (string-match regexp oldpath) + (setq index len) + (setq oldpath nil + index (1+ index))))) + oldpath)) + +(eval-when-compile + (defvar dired-directory)) + +(defun eshell/cd (&rest args) ; all but first ignored + "Alias to extend the behavior of `cd'." + (let ((path (car args)) + (subpath (car (cdr args))) + handled) + (if (numberp path) + (setq path (number-to-string path))) + (if (numberp subpath) + (setq subpath (number-to-string subpath))) + (cond + (subpath + (let ((curdir (eshell/pwd))) + (if (string-match path curdir) + (setq path (replace-match subpath nil nil curdir)) + (error "Path substring '%s' not found" path)))) + ((and path (string-match "^-\\([0-9]*\\)$" path)) + (let ((index (match-string 1 path))) + (setq path + (ring-remove eshell-last-dir-ring + (if index + (string-to-int index) + 0))))) + ((and path (string-match "^=\\(.*\\)$" path)) + (let ((oldpath (eshell-find-previous-directory + (match-string 1 path)))) + (if oldpath + (setq path oldpath) + (let ((len (ring-length eshell-last-dir-ring)) + (index 0)) + (if (= len 0) + (error "Directory ring empty")) + (while (< index len) + (eshell-printn + (concat (number-to-string index) ": " + (ring-ref eshell-last-dir-ring index))) + (setq index (1+ index))) + (setq handled t))))) + (path + (setq path (eshell-expand-multiple-dots path)))) + (unless handled + (setq dired-directory (or path "~")) + (let ((curdir (eshell/pwd))) + (unless (equal curdir dired-directory) + (eshell-add-to-dir-ring curdir)) + (let ((result (cd dired-directory))) + (and eshell-cd-shows-directory + (eshell-printn result))) + (run-hooks 'eshell-directory-change-hook) + (if eshell-list-files-after-cd + (throw 'eshell-replace-command + (eshell-parse-command "ls" (cdr args)))) + nil)))) + +(defun eshell-add-to-dir-ring (path) + "Add PATH to the last-dir-ring, if applicable." + (unless (and (not (ring-empty-p eshell-last-dir-ring)) + (equal path (ring-ref eshell-last-dir-ring 0))) + (if eshell-last-dir-unique + (let ((index 0) + (len (ring-length eshell-last-dir-ring))) + (while (< index len) + (if (equal (ring-ref eshell-last-dir-ring index) path) + (ring-remove eshell-last-dir-ring index) + (setq index (1+ index)))))) + (ring-insert eshell-last-dir-ring path))) + +;;; pushd [+n | dir] +(defun eshell/pushd (&rest args) ; all but first ignored + "Implementation of pushd in Lisp." + (let ((path (car args))) + (cond + ((null path) + ;; no arg -- swap pwd and car of stack unless eshell-pushd-tohome + (cond (eshell-pushd-tohome + (eshell/pushd "~")) + (eshell-dirstack + (let ((old (eshell/pwd))) + (eshell/cd (car eshell-dirstack)) + (setq eshell-dirstack (cons old (cdr eshell-dirstack))) + (eshell/dirs t))) + (t + (error "pushd: No other directory")))) + ((string-match "^\\+\\([0-9]\\)" path) + ;; pushd +n + (setq path (string-to-number (match-string 1 path))) + (cond ((> path (length eshell-dirstack)) + (error "Directory stack not that deep")) + ((= path 0) + (error "Couldn't cd")) + (eshell-pushd-dextract + (let ((dir (nth (1- path) eshell-dirstack))) + (eshell/popd path) + (eshell/pushd (eshell/pwd)) + (eshell/cd dir) + (eshell/dirs t))) + (t + (let* ((ds (cons (eshell/pwd) eshell-dirstack)) + (dslen (length ds)) + (front (nthcdr path ds)) + (back (nreverse (nthcdr (- dslen path) (reverse ds)))) + (new-ds (append front back))) + (eshell/cd (car new-ds)) + (setq eshell-dirstack (cdr new-ds)) + (eshell/dirs t))))) + (t + ;; pushd <dir> + (let ((old-wd (eshell/pwd))) + (eshell/cd path) + (if (or (null eshell-pushd-dunique) + (not (member old-wd eshell-dirstack))) + (setq eshell-dirstack (cons old-wd eshell-dirstack))) + (eshell/dirs t))))) + nil) + +;;; popd [+n] +(defun eshell/popd (&rest args) + "Implementation of popd in Lisp." + (let ((ref (or (car args) "+0"))) + (unless (and (stringp ref) + (string-match "\\`\\([+-][0-9]+\\)\\'" ref)) + (error "popd: bad arg `%s'" ref)) + (setq ref (string-to-number (match-string 1 ref))) + (cond ((= ref 0) + (unless eshell-dirstack + (error "popd: Directory stack empty")) + (eshell/cd (car eshell-dirstack)) + (setq eshell-dirstack (cdr eshell-dirstack)) + (eshell/dirs t)) + ((<= (abs ref) (length eshell-dirstack)) + (let* ((ds (cons nil eshell-dirstack)) + (cell (nthcdr (if (> ref 0) + (1- ref) + (+ (length eshell-dirstack) ref)) ds)) + (dir (cadr cell))) + (eshell/cd dir) + (setcdr cell (cdr (cdr cell))) + (setq eshell-dirstack (cdr ds)) + (eshell/dirs t))) + (t + (error "Couldn't popd")))) + nil) + +(defun eshell/dirs (&optional if-verbose) + "Implementation of dirs in Lisp." + (when (or (not if-verbose) eshell-dirtrack-verbose) + (let* ((msg "") + (ds (cons (eshell/pwd) eshell-dirstack)) + (home (expand-file-name "~/")) + (homelen (length home))) + (while ds + (let ((dir (car ds))) + (and (>= (length dir) homelen) + (string= home (substring dir 0 homelen)) + (setq dir (concat "~/" (substring dir homelen)))) + (setq msg (concat msg (directory-file-name dir) " ")) + (setq ds (cdr ds)))) + msg))) + +(defun eshell-read-last-dir-ring () + "Sets the buffer's `eshell-last-dir-ring' from a history file." + (let ((file eshell-last-dir-ring-file-name)) + (cond + ((or (null file) + (equal file "") + (not (file-readable-p file))) + nil) + (t + (let* ((count 0) + (size eshell-last-dir-ring-size) + (ring (make-ring size))) + (with-temp-buffer + (insert-file-contents file) + ;; Save restriction in case file is already visited... + ;; Watch for those date stamps in history files! + (goto-char (point-max)) + (while (and (< count size) + (re-search-backward "^\\([^\n].*\\)$" nil t)) + (ring-insert-at-beginning ring (match-string 1)) + (setq count (1+ count))) + ;; never allow the top element to equal the current + ;; directory + (while (and (not (ring-empty-p ring)) + (equal (ring-ref ring 0) (eshell/pwd))) + (ring-remove ring 0))) + (setq eshell-last-dir-ring ring)))))) + +(defun eshell-write-last-dir-ring () + "Writes the buffer's `eshell-last-dir-ring' to a history file." + (let ((file eshell-last-dir-ring-file-name)) + (cond + ((or (null file) + (equal file "") + (null eshell-last-dir-ring) + (ring-empty-p eshell-last-dir-ring)) + nil) + ((not (file-writable-p file)) + (message "Cannot write last-dir-ring file %s" file)) + (t + (let* ((ring eshell-last-dir-ring) + (index (ring-length ring))) + (with-temp-buffer + (while (> index 0) + (setq index (1- index)) + (insert (ring-ref ring index) ?\n)) + (insert (eshell/pwd) ?\n) + (eshell-with-private-file-modes + (write-region (point-min) (point-max) file nil + 'no-message)))))))) + +;;; Code: + +;;; em-dirs.el ends here |