summaryrefslogtreecommitdiff
path: root/.emacs.d
diff options
context:
space:
mode:
authorSean Whitton <spwhitton@spwhitton.name>2021-06-21 17:14:18 -0700
committerSean Whitton <spwhitton@spwhitton.name>2021-06-21 17:14:18 -0700
commitfeed41ba15c8dcd9616a56cf01c4cb104eb9bd59 (patch)
treef8b6c86e2d5a552ff45d54a5df4a818dccf66f26 /.emacs.d
parent065576ec1b521b90cf68a8531ddf162ec9f678cc (diff)
downloaddotfiles-feed41ba15c8dcd9616a56cf01c4cb104eb9bd59.tar.gz
rework getting to Eshell buffers
Main change is not paying attention to the current project.
Diffstat (limited to '.emacs.d')
-rw-r--r--.emacs.d/init-spw.el224
1 files changed, 130 insertions, 94 deletions
diff --git a/.emacs.d/init-spw.el b/.emacs.d/init-spw.el
index 193a8508..9692e9fd 100644
--- a/.emacs.d/init-spw.el
+++ b/.emacs.d/init-spw.el
@@ -1235,102 +1235,138 @@ remote hosts, to avoid having to roundtrip all the data."
(localname-start (string-match localname whole)))
(concat "cd " (substring whole 0 localname-start) "/")))
-;;; commands to get to shell buffers -- sometimes useful to prefix with C-x 4 1
+;;; getting to Eshell buffers
+
+(defun spw/eshell-jump (arg &optional chdir)
+ "Pop to *eshell*, and offer cycling among other Eshells, unless
+one of the following special circumstances applies:
+
+- If a command is running in *eshell*, rename that buffer out of
+ the way and start a new one.
+
+- If CHDIR, and there is no Eshell in `default-directory', also
+ change the directory of *eshell* to `default-directory'.
+
+- If CHDIR and there is an Eshell in `default-directory',
+ including one generated by `project-eshell', switch to that
+ Eshell instead of *eshell*.
+
+- If both ARG and CHDIR, or if CHDIR and the current buffer is an
+ Eshell buffer which is not running a command, unconditionally
+ start a new Eshell in `default-directory'.
+
+- If not CHDIR and the current buffer is *eshell*, activate
+ transient cycling to make it easy to get back to another
+ Eshell. (This is the only case in which we do not use
+ `pop-to-buffer' or equivalent, so C-x 4 4 must be used to cycle
+ in another window.)
+
+For the purpose of cycling, Eshells generated by `project-eshell'
+are sorted below Eshells generated by this function.
+
+The ideas behind this behaviour are as follows.
+
+- Just like Lisp REPLs, we do not normally need a lot of
+ different Eshells; it is fine for shell history associated with
+ different tasks to become mixed together. But we do need to
+ start a new Eshell when other Eshells are already busy running
+ commands.
+
+- Rename *eshell* to *eshell*<N>, but don't ever rename
+ *eshell*<N> back to *eshell*, because that is a conventional
+ workflow -- M-&, C-h i, M-x ielm, M-x compile etc. always take
+ you to the unnumbered buffer, possibly renaming the numbered one
+ out of the way.
+
+ This is why we don't try to reuse Eshells especially
+ aggressively; for example, we could find an *eshell*<N> not
+ running a command a rename it to *eshell*, but we don't.
+
+- Don't pay attention to the current project, as an old version
+ of this code did, because if we're using C-c e e and/or C-c e h
+ rather than C-x p e, we are probably working in a
+ project-agnostic way.
+
+ Thus, among the `project-eshell' Eshells available to cycle
+ through, don't prioritise those of the current project (for
+ example by moving them to the front) -- if we explicitly want
+ those, can use C-x p e.
+
+- Treat C-x p e as the primary way to get to Eshells in project
+ roots, and avoid changing the directories of those Eshells, as
+ it is surprising when C-x p e doesn't take us to an Eshell
+ which is ready to run commands in the project root (another
+ possibility would be to change the dir back to the project root
+ in this case).
+
+- It is assumed we'll sometimes use C-x 4 1 in front of this
+ command, and if we're already in Eshell, we might use C-x 4 4
+ to start the cycling in another window."
+ (interactive "P")
+ (cl-flet ((project-eshell-eshell-p ()
+ (string-match "\\`\\*.+-eshell\\*[><0-9]*\\'" (buffer-name)))
+ (fresh-eshell ()
+ (when-let ((buffer (get-buffer eshell-buffer-name)))
+ (with-current-buffer buffer (rename-uniquely)))
+ ;; This will pick up `default-directory'.
+ (eshell)))
+ (let ((current-eshell (and (eq major-mode 'eshell-mode)
+ (not (project-eshell-eshell-p))
+ (not (get-buffer-process (current-buffer)))
+ (current-buffer)))
+ (initial-default-directory default-directory)
+ default-directory-eshell
+ project-eshells
+ other-eshells)
+ ;; Populate our two lists of all Eshells.
+ (dolist (buffer (buffer-list))
+ (with-current-buffer buffer
+ (when (eq major-mode 'eshell-mode)
+ (if (and chdir (not default-directory-eshell)
+ (string= default-directory initial-default-directory)
+ (not (get-buffer-process buffer)))
+ (setq default-directory-eshell buffer)
+ (push buffer
+ (if (project-eshell-eshell-p)
+ project-eshells other-eshells))))))
+ ;; Now `pop-to-buffer' if we're going to do that.
+ (cond ((and chdir (or arg current-eshell))
+ (fresh-eshell))
+ ((and chdir default-directory-eshell)
+ (pop-to-buffer default-directory-eshell))
+ ((or chdir (not (string= (buffer-name) eshell-buffer-name)))
+ (if-let ((buffer (get-buffer eshell-buffer-name)))
+ (if (get-buffer-process buffer)
+ (fresh-eshell)
+ (pop-to-buffer buffer)
+ (goto-char (point-max))
+ (when chdir
+ (eshell-interrupt-process) ; to clear input
+ (insert "cd" " " ?\" initial-default-directory ?\")
+ (eshell-send-input)))
+ (fresh-eshell)))
+ ;; If `display-buffer-overriding-action' is not its default value,
+ ;; pop to ourselves, to allow subsequent cycling to a different
+ ;; Eshell in another window. E.g. C-x e e C-x 4 4 C-x e e
+ ((not (equal display-buffer-overriding-action '(nil . nil)))
+ (pop-to-buffer (current-buffer))))
+ ;; Finally, generate and return a ring for cycling purposes.
+ (let* ((all (delete (current-buffer)
+ (nconc project-eshells other-eshells)))
+ (ring (make-ring (1+ (length all)))))
+ (dolist (buffer all)
+ (ring-insert ring buffer))
+ (ring-insert ring (current-buffer))
+ ring))))
+
+(defun spw/eshell-jump-from-here (arg)
+ (interactive "P")
+ (spw/eshell-jump arg t))
-(defun spw/eshell-here ()
- "Switch to an Eshell in `default-directory', creating one if necessary."
- (interactive)
- (if (eq major-mode 'eshell-mode)
- ;; switch to another Eshell in the current directory
- (eshell t)
- (let ((dir default-directory)
- (buffers (buffer-list)))
- (while (and buffers
- (not (with-current-buffer (car buffers)
- (and (eq major-mode 'eshell-mode)
- (equal default-directory dir)))))
- (setq buffers (cdr buffers)))
- (if (car buffers)
- (pop-to-buffer (car buffers))
- (eshell t)))))
-
-(defconst spw/project-eshells-regexp
- "\\`\\*\\(.+\\)-eshell\\*[><0-9]*\\'")
-
-(defun spw/cycle-eshells ()
- "Cycle through all Eshell buffers."
- (interactive)
- (let ((project (project-current))
- (current-eshell (and (eq major-mode 'eshell-mode)
- (current-buffer)))
- project-eshells
- other-eshells)
- (dolist (buffer (buffer-list))
- (with-current-buffer buffer
- (when (and (eq major-mode 'eshell-mode)
- (not (eq buffer current-eshell)))
- (cond
- ;; First check if we're generated by `project-eshell'; if so,
- ;; include in the buffers to be cycled through only if we're for
- ;; the current project. An Eshell started by `project-eshell' may
- ;; lose its association with the project if I cd to somewhere else,
- ;; (e.g. to ~/.emacs.d from ~/src/dotfiles/.emacs.d) but at least
- ;; for the purposes of the present command, it is more useful to
- ;; consider it to remain one of the project's Eshells (as
- ;; `project-eshell' effectively does)
- ((string-match spw/project-eshells-regexp (buffer-name))
- (when (and project
- (string= (match-string 1 (buffer-name))
- (file-name-nondirectory
- (directory-file-name
- (project-root project)))))
- (push buffer project-eshells)))
- ;; Now look at the current project for this non-`project-eshell'
- ;; Eshell -- Eshells generated using C-c e h within the current
- ;; project should also be included (if we want to avoid cycling
- ;; through these can use C-x p e which too has arrow key cycling).
- ;;
- ;; Don't try to look up the current project for TRAMP Eshells as it
- ;; requires SSHing to the host. Even if an active connection
- ;; exists it would be too slow for the present function
- ((and (not (file-remote-p default-directory))
- (equal project (project-current)))
- (push buffer project-eshells))
- ;; If this Eshell which is not for the current project is the most
- ;; recently used Eshell, consider it to be an Eshell for the
- ;; current project -- idea is that we show the most recently used
- ;; Eshell first unless it's a `project-eshell' Eshell for another
- ;; project
- ((not (or other-eshells project-eshells))
- (push buffer project-eshells))
- ;; All remaining Eshells
- (t
- (push buffer other-eshells))))))
- (if-let ((eshell-buffers
- ;; reverse of the order we want to cycle through them in
- (append (and current-eshell (list current-eshell))
- other-eshells
- project-eshells)))
- (let ((ring (make-ring (length eshell-buffers))))
- (dolist (buffer eshell-buffers)
- (ring-insert ring buffer))
- (pop-to-buffer (ring-ref ring 0))
- ;; return the ring, to be used by transient cycling functions
- ring)
- ;; if no Eshells except possibly generated by `project-eshell' for other
- ;; projects, start a new Eshell in HOME
- (let ((default-directory (expand-file-name "~")))
- (eshell t)))))
-
-;; C-c e e is the main way to get back to the most recent shell session; C-c e
-;; h and C-x p e are for when we don't have one yet for the current task (or
-;; when we don't know whether we have one -- when we haven't run any shell
-;; commands yet in our attempts to accomplish this task)
-(global-set-key "\C-ceh" #'spw/eshell-here)
(spw/bind-command-with-ret-val-cycling
- ("\C-cee" . spw/cycle-eshells)
- (spw/buffer-ring-cycle-lambda
- (and (ring-p ret-val) ret-val)))
+ (("\C-cee" . spw/eshell-jump)
+ ("\C-ceh" . spw/eshell-jump-from-here))
+ (spw/buffer-ring-cycle-lambda ret-val))
;;; my commands -- like defining functions in .bashrc where simple aliases are
;;; not enough