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.
This commit is contained in:
shipmints 2025-03-06 12:04:57 +01:00 committed by Michael Albinus
parent bf027eb6ff
commit c1ddf612d5
4 changed files with 198 additions and 9 deletions

View file

@ -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

View file

@ -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 <up>') is like 'minibuffer-complete-history'
but completes on comint inputs.

View file

@ -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

View file

@ -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