From c1ddf612d592fcaa3fae66b24933e2aab78406f1 Mon Sep 17 00:00:00 2001 From: shipmints Date: Thu, 6 Mar 2025 12:04:57 +0100 Subject: [PATCH] Add shell-mode bookmark support for local and remote shells (bug#65039) * doc/emacs/misc.texi (Shell): Add "Shell Bookmarks" menu item. (Shell Mode): Fix typo. (Shell Bookmarks): New node. * etc/NEWS: Announce shell-mode bookmark capability. * lisp/bookmark.el: (bookmark-insert): Refuse to insert bookmarks whose handlers have the property 'bookmark-inhibit eq 'insert. * lisp/shell.el (shell-mode): Set bookmark-make-record-function. (shell-bookmark-name-function): New defcustom. (shell-bookmark-name-from-default-directory): New defun. (shell-bookmark-name-from-buffer-name): New defun. (shell-bookmark-defaults-function): New defvar. (shell-bookmark-defaults): New defun. (shell-bookmark-make-record): New defun. (shell-bookmark-jump-non-essential): New defvar. (shell-bookmark-jump): New defun with properties: 'bookmark-handler-type "Shell", 'bookmark-inhibit 'insert. --- doc/emacs/misc.texi | 48 ++++++++++++++++- etc/NEWS | 10 ++++ lisp/bookmark.el | 20 ++++--- lisp/shell.el | 129 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 9 deletions(-) diff --git a/doc/emacs/misc.texi b/doc/emacs/misc.texi index 22af77b62c6..6ffa185cd49 100644 --- a/doc/emacs/misc.texi +++ b/doc/emacs/misc.texi @@ -773,6 +773,7 @@ See the Eshell Info manual, which is distributed with Emacs. * Shell Prompts:: Two ways to recognize shell prompts. * History: Shell History. Repeating previous commands in a shell buffer. * Directory Tracking:: Keeping track when the subshell changes directory. +* Shell Bookmarks:: Save and restore local and remote shell buffers. * Options: Shell Options. Options for customizing Shell mode. * Terminal emulator:: An Emacs window as a terminal emulator. * Term Mode:: Special Emacs commands used in Term mode. @@ -1199,7 +1200,7 @@ subshell: By default, Shell mode handles common @acronym{ANSI} escape codes (for instance, for changing the color of text). Emacs also optionally -supports some extend escape codes, like some of the @acronym{OSC} +supports some extended escape codes, like some of the @acronym{OSC} (Operating System Codes) if you put the following in your init file: @lisp @@ -1503,6 +1504,51 @@ working directory; see the documentation of the variable dirtrack-mode} in the Shell buffer, or add @code{dirtrack-mode} to @code{shell-mode-hook} (@pxref{Hooks}). +@node Shell Bookmarks +@subsection Shell Bookmarks +@cindex shell bookmarks + +Shell mode buffers can be bookmarked, and both local and remote +(@pxref{Remote Files}) shell buffers are supported. @xref{Bookmarks}. + + Opening, or ``jumping'' to, a bookmarked shell restores its buffer +name, its current directory, and will create a remote connection, as +necessary, using the shell command you used to create the remote buffer. + +@vindex shell-bookmark-name-function +@findex shell-bookmark-name-from-default-directory +@findex shell-bookmark-name-from-buffer-name + The option @code{shell-bookmark-name-function} can be customized to +suit your preferences. It defaults to the function +@code{shell-bookmark-name-from-default-directory} which uses the final +component of the buffer's @code{default-directory}. An alternate +function, @code{shell-bookmark-name-from-buffer-name}, uses the buffer's +name with its @code{rename-uniquely} suffix brackets "<>" stripped. You +can bind this option to your own function. + + You can inhibit remote connections when you open a remote shell +bookmark. This is useful when you restore sessions with +@code{desktop-load}, or via another session-management package, to avoid +time delays establishing connections. You can establish a connection on +an unconnected remote buffer using the command @kbd{C-x C-v} +(@code{find-alternate-file}). To inhibit a connection interactively, +give a prefix argument before invoking the open/jump bookmark menu item, +or before invoking the command @code{bookmark-jump}. @footnote{To +inhibit a connection programmatically, refer to the documentation for +the variable @code{shell-bookmark-jump-non-essential}.} @footnote{To +properly handle multi-hop remote connections, refer to the documentation +for the function @code{shell-bookmark-jump}.} + +Note: Before creating ad-hoc multi-hop remote connections, customize +either or both: +@code{tramp-save-ad-hoc-proxies} to non-@code{nil} to persist proxy +routes. +@code{tramp-show-ad-hoc-proxies} to non-@code{nil} to ensure connections +are fully qualified. This is helpful if you use the same persisted +bookmarks file on multiple hosts. + +@xref{Top, The Tramp Manual,, tramp, The Tramp Manual}. + @node Shell Options @subsection Shell Mode Options diff --git a/etc/NEWS b/etc/NEWS index 78a241abfd3..864bfbf595b 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -690,6 +690,16 @@ It removes all the buttons in the specified region. ** Shell +--- +*** Shell buffers now support bookmarks. + +You can now bookmark local and remote shell buffers using the bookmark +menu 'bookmark-bmenu-list', or by using the command 'bookmark-set'. +Shell bookmarks can be loaded via the menu and by using the command +'bookmark-jump', which open a bookmarked shell, restore its buffer name, +its current directory, and create a remote connection, if necessary. +You can customize 'shell-bookmark-name-function'. + *** New command to complete the shell history. 'comint-complete-input-ring' ('C-x ') is like 'minibuffer-complete-history' but completes on comint inputs. diff --git a/lisp/bookmark.el b/lisp/bookmark.el index 8495f33cb5f..54de574ab76 100644 --- a/lisp/bookmark.el +++ b/lisp/bookmark.el @@ -1530,14 +1530,18 @@ this." (interactive (list (bookmark-completing-read "Insert bookmark contents"))) (bookmark-maybe-historicize-string bookmark-name) (bookmark-maybe-load-default-file) - (let ((orig-point (point)) - (str-to-insert - (save-current-buffer - (bookmark-handle-bookmark bookmark-name) - (buffer-string)))) - (insert str-to-insert) - (push-mark) - (goto-char orig-point))) + (if (eq 'insert (get (or (bookmark-get-handler bookmark-name) + #'bookmark-default-handler) + 'bookmark-inhibit)) + (error "Insert not supported for bookmark %s" bookmark-name) + (let ((orig-point (point)) + (str-to-insert + (save-current-buffer + (bookmark-handle-bookmark bookmark-name) + (buffer-string)))) + (insert str-to-insert) + (push-mark) + (goto-char orig-point)))) ;;;###autoload diff --git a/lisp/shell.el b/lisp/shell.el index a3834634df7..25109a50b43 100644 --- a/lisp/shell.el +++ b/lisp/shell.el @@ -700,6 +700,7 @@ command." (setq-local paragraph-separate "\\'") (setq-local paragraph-start comint-prompt-regexp) (setq-local font-lock-defaults '(shell-font-lock-keywords t)) + (setq-local bookmark-make-record-function #'shell-bookmark-make-record) (setq-local shell-dirstack nil) (setq-local shell-last-dir nil) (setq-local comint-get-old-input #'shell-get-old-input) @@ -1862,6 +1863,134 @@ to make `shell-highlight-undef-mode' redo its setup." (when shell-highlight-undef-mode (shell-highlight-undef-mode 1))) +;;; Bookmark support: + +(declare-function bookmark-prop-get "bookmark" (bookmark prop)) + +(defcustom shell-bookmark-name-function #'shell-bookmark-name-from-default-directory + "Function to generate a shell bookmark name. +The default is `shell-bookmark-name', which see." + :group 'shell + :type `(choice (function-item ,#'shell-bookmark-name-from-default-directory) + (function-item ,#'shell-bookmark-name-from-buffer-name) + function) + :version "31.1") + +(defun shell-bookmark-name-from-default-directory () + "Return a `shell-mode' bookmark name based on `default-directory'. +Return \"shell-\" appended with the final path component of the buffer's +`default-directory'." + (format "shell-%s" + (file-name-nondirectory + (directory-file-name + (file-name-directory default-directory))))) + +(defun shell-bookmark-name-from-buffer-name () + "Return a `shell-mode' bookmark name based on buffer name'. +Return `buffer-name' stripped of its count suffix; e.g., \"*shell*<2>\", +if adorned by `rename-uniquely', which see." + (replace-regexp-in-string "<[[:digit:]]+>\\'" "" (buffer-name))) + +(defvar shell-bookmark-defaults-function #'shell-bookmark-defaults + "Function to generate a list of default shell bookmark names. +This list is used by `bookmark-set' and prompted by +`read-from-minibuffer'.") + +(defun shell-bookmark-defaults () + "Return bookmark name options for the current `shell-mode' buffer." + (list + (funcall shell-bookmark-name-function) + (buffer-name) + default-directory)) + +(defun shell-bookmark-make-record () + "Create a bookmark record for the current `shell-mode' buffer. +Handle both local and remote shell buffers. +Before creating ad-hoc multi-hop remote connections, customize either or +both: +`tramp-save-ad-hoc-proxies' to non-nil to persist proxy routes. +`tramp-show-ad-hoc-proxies' to non-nil to ensure connections are fully + qualified. This is helpful if you use the same persisted bookmarks + file on multiple hosts." + (let ((bookmark-shell-file-name + (or (connection-local-value shell-file-name) sh-shell-file))) + `((defaults . ,(funcall shell-bookmark-defaults-function)) + (location . ,default-directory) + (shell-file-name . ,bookmark-shell-file-name) + (handler . shell-bookmark-jump)))) + +(defvar shell-bookmark-jump-non-essential nil + "If non-nil, new remote connections are inhibited in shell-bookmark-jump. +This is useful when loading a session via `desktop-read' or another +session-management package.") + +;;;###autoload +(defun shell-bookmark-jump (bookmark) + "Default BOOKMARK handler for shell buffers. +Create a shell buffer with its `default-directory', shell process, and +buffer name from the bookmark. If there is an existing shell buffer of +the same name, default `shell-mode' behavior is to reuse that buffer. + +For a remote shell `default-directory' will be the remote file name. +Remote shell buffers reuse existing connections that match the remote +file name, or may prompt you to create a new connection. Bind +`tramp-show-ad-hoc-proxies' to non-nil to ensure multi-hop remote +connections are fully qualified. + +If called with a single \\[universal-argument] prefix, a new shell +buffer will be created if there is an existing buffer with the same +name. The new buffer name is made unique using `rename-uniquely', which +see. + +If called with a double \\[universal-argument] prefix, new remote +connections are inhibited, though an existing connection will be reused. +You can make a remote connection manually by reloading the buffer using +\\[find-alternate-file] or create a new shell using \\[shell]. + +If called with a triple \\[universal-argument] prefix, a new buffer will +be created if necessary, and new remote connections are inhibited." + (let* ((bookmark-default-directory (bookmark-prop-get bookmark 'location)) + (default-directory bookmark-default-directory) + (explicit-shell-file-name (bookmark-prop-get bookmark 'shell-file-name)) + (prefix-arg (prefix-numeric-value current-prefix-arg)) + (maybe-new-shell (or (= 4 prefix-arg) (= 64 prefix-arg))) + (non-essential (or shell-bookmark-jump-non-essential + (= 16 prefix-arg) (= 64 prefix-arg))) + (shell-buffer-name (car bookmark)) + (shell-buffer-name (if (and maybe-new-shell + (comint-check-proc shell-buffer-name)) + (generate-new-buffer-name shell-buffer-name) + shell-buffer-name))) + ;; Handle a local shell, a remote shell with an existing + ;; connection, or a remote shell needing a connection and new + ;; connections not inhibited. + (if (or (not (file-remote-p default-directory)) + (file-remote-p default-directory nil 'connected) + (and (not non-essential) + (not (file-remote-p default-directory nil 'connected)))) + (shell shell-buffer-name) + ;; Handle a remote shell with no matching active connection and if + ;; new connections are inhibited. + (let* ((file-name-handler-alist nil) + ;; Ignore file-name-handler-alist to guard + ;; abbreviate-file-name, et.al., which are remote aware. + ;; The macro without-remote-files is insufficient for this + ;; case. + (shell-buffer + (shell shell-buffer-name))) + (with-current-buffer shell-buffer + ;; Allow reloading or M-x shell to attempt a remote connection. + (setq default-directory bookmark-default-directory) + (setq list-buffers-directory bookmark-default-directory) + ;; Inhibit features that may cause remote connection attempts. + ;; These settings revert when the user reloads the buffer. + (dirtrack-mode -1) + (shell-dirtrack-mode -1) + (delq (assoc "7" ansi-osc-handlers) ; ansi-osc-directory-tracker + ansi-osc-handlers)))))) +(put #'shell-bookmark-jump 'bookmark-handler-type "Shell") +(put #'shell-bookmark-jump 'bookmark-inhibit 'insert) + (provide 'shell) ;;; shell.el ends here