Subject: Re: bug#50804: Command substitutions for keys without keymaps

From: Stefan Kangas <[email-masked]>
Date: Sun, 21 Nov 2021 14:35:53 +0100

tags 50804 + patch
thanks

Lars Ingebrigtsen <[email-masked]> writes:

> I'd be OK to go with the new syntax here.

Maybe something like the attached?
From 9ed0baf9f889821f37ef6f661a440dab4761978f Mon Sep 17 00:00:00 2001
From: Stefan Kangas <[email-masked]>
Date: Sun, 21 Nov 2021 14:12:26 +0100
Subject: [PATCH] Add new format for literal key sequences to
 substitute-command-keys

* lisp/help.el (substitute-command-keys): Add new format "\\`f'" for
literal key sequences.
* doc/lispref/help.texi (Keys in Documentation): Document the above
new substitution.

* test/lisp/help-tests.el
(help-tests-substitute-command-keys/literal-key-sequence):
(help-tests-substitute-command-keys/literal-key-sequence-errors): New
tests.
(help-tests-substitute-key-bindings/face-help-key-binding): Extend test.
---
 doc/lispref/help.texi   |  7 +++++++
 etc/NEWS                |  9 +++++++++
 lisp/help.el            | 20 ++++++++++++++++++++
 test/lisp/help-tests.el | 19 +++++++++++++++++++
 4 files changed, 55 insertions(+)

diff --git a/doc/lispref/help.texi b/doc/lispref/help.texi
index a788852de7..1a9eb30fde 100644
--- a/doc/lispref/help.texi
+++ b/doc/lispref/help.texi
@@ -333,6 +333,13 @@ Keys in Documentation
 specifies @var{mapvar}'s value as the keymap for any following
 @samp{\[@var{command}]} sequences in this documentation string.
 
+@item \`@var{KEYSEQ}'
+stands for a key sequence @var{KEYSEQ}, which will use the same face
+as a command substitution.  This should be used only when a key
+sequence has no corresponding command, for example when it is read
+directly with @code{read-key-sequence}.  It must be a valid key
+sequence according to @code{key-valid-p}.
+
 @item `
 (grave accent) stands for a left quote.
 This generates a left single quotation mark, an apostrophe, or a grave
diff --git a/etc/NEWS b/etc/NEWS
index bfea4da8b9..afcc438001 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -716,6 +716,15 @@ syntax.
 This is like 'kbd', but only returns vectors instead of a mix of
 vectors and strings.
 
++++
+** New substitution in docstrings and 'substitute-command-keys'.  Use
+Use "\\`KEYSEQ'" to insert a literal key sequence "KEYSEQ"
+(e.g. "C-k") in a docstring or when calling 'substitute-command-keys',
+which will use the same face as a command substitution.  This should
+be used only when a key sequence has no corresponding command, for
+example when it is read directly with 'read-key-sequence'.  It must be
+a valid key sequence according to 'key-valid-p'.
+
 +++
 ** New function 'file-name-split'.
 This returns a list of all the components of a file name.
diff --git a/lisp/help.el b/lisp/help.el
index bc3d4773da..9122d96271 100644
--- a/lisp/help.el
+++ b/lisp/help.el
@@ -1078,6 +1078,9 @@ substitute-command-keys
 keystroke sequence that invokes COMMAND, or \"M-x COMMAND\" if COMMAND
 is not on any keys.  Keybindings will use the face `help-key-binding'.
 
+Each substring of the form \\\\=`KEYBINDING' will be replaced by
+KEYBINDING and use the `help-key-binding' face.
+
 Each substring of the form \\\\={MAPVAR} is replaced by a summary of
 the value of MAPVAR as a keymap.  This summary is similar to the one
 produced by ‘describe-bindings’.  The summary ends in two newlines
@@ -1130,6 +1133,23 @@ substitute-command-keys
                 (delete-char 2)
                 (ignore-errors
                   (forward-char 1)))
+               ((and (= (following-char) ?`)
+                     (save-excursion
+                       (prog1 (search-forward "'" nil t)
+                         (setq end-point (- (point) 2)))))
+                (goto-char orig-point)
+                (delete-char 2)
+                (goto-char (1- end-point))
+                (delete-char 1)
+                ;; (backward-char 1)
+                (let ((k (buffer-substring-no-properties orig-point (point))))
+                  (cond ((= (length k) 0)
+                         (error "Empty key sequence in substitution"))
+                        ((not (key-valid-p k))
+                         (error "Invalid key sequence in substitution: `%s'" k))))
+                (add-text-properties orig-point (point)
+                                     '( face help-key-binding
+                                        font-lock-face help-key-binding)))
                ;; 1C. \[foo] is replaced with the keybinding.
                ((and (= (following-char) ?\[)
                      (save-excursion
diff --git a/test/lisp/help-tests.el b/test/lisp/help-tests.el
index 982750f479..281d97ee92 100644
--- a/test/lisp/help-tests.el
+++ b/test/lisp/help-tests.el
@@ -88,6 +88,25 @@ help-tests-substitute-command-keys/commands
    (test "\\[emacs-version]\\[next-line]" "M-x emacs-versionC-n")
    (test-re "\\[emacs-version]`foo'" "M-x emacs-version[`'‘]foo['’]")))
 
+(ert-deftest help-tests-substitute-command-keys/literal-key-sequence ()
+  "Literal replacement."
+  (with-substitute-command-keys-test
+   (test "\\`C-m'" "C-m")
+   (test "\\`C-m'\\`C-j'" "C-mC-j")
+   (test "foo\\`C-m'bar\\`C-j'baz" "fooC-mbarC-jbaz")))
+
+(ert-deftest help-tests-substitute-command-keys/literal-key-sequence-errors ()
+  (should-error (substitute-command-keys "\\`'"))
+  (should-error (substitute-command-keys "\\`c-c'"))
+  (should-error (substitute-command-keys "\\`<foo bar baz>'")))
+
+(ert-deftest help-tests-substitute-key-bindings/face-help-key-binding ()
+  (should (eq (get-text-property 0 'face (substitute-command-keys "\\[next-line]"))
+              'help-key-binding))
+  (should (eq (get-text-property 0 'face (substitute-command-keys "\\`f'"))
+              'help-key-binding)))
+
+
 (ert-deftest help-tests-substitute-command-keys/keymaps ()
   (with-substitute-command-keys-test
    (test-re "\\{minibuffer-local-must-match-map}"
-- 
2.30.2