diff options
author | Sean Whitton <spwhitton@spwhitton.name> | 2021-06-21 17:14:18 -0700 |
---|---|---|
committer | Sean Whitton <spwhitton@spwhitton.name> | 2021-06-21 17:14:18 -0700 |
commit | feed41ba15c8dcd9616a56cf01c4cb104eb9bd59 (patch) | |
tree | f8b6c86e2d5a552ff45d54a5df4a818dccf66f26 /.emacs.d | |
parent | 065576ec1b521b90cf68a8531ddf162ec9f678cc (diff) | |
download | dotfiles-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.el | 224 |
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 |