Subject: [PATCH] Support curved quotes in doc strings

From: Paul Eggert <[email-masked]>
Date: Wed, 13 May 2015 00:30:52 -0700

Stefan's comment "How does it interact with electric-pair-mode?" in 
<http://bugs.gnu.org/20545#11> prompted me to go back to the patch proposed for 
Bug#20385 and to get it to work with electric-pair-mode as well.  The revised 
patch is attached.  Most of the new revisions are to 
lisp/progmodes/elisp-mode.el.  They also add support for C-M-i completion in the 
presence of curved quotes, and tweak some wording in the documentation.

I haven't installed this as I am still mulling over similar revisions to the 
patch proposed for Bug#20545 (this is to make it easier to type curved quotes in 
doc strings).
>From eb392195ffdb108228c981f92ca8b5766afdcff9 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Wed, 13 May 2015 00:10:00 -0700
Subject: [PATCH] Support curved quotes in doc strings
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Emacs's traditional doc string style has been to quote symbols
`like this'.  This worked well on now-obsolete terminals where
` and ' were symmetric quotes, but nowadays curved quotes
‘like this’ look better.  Support quoting the new way too.
(Bug#20385)
* doc/lispref/tips.texi (Documentation Tips): Symbols can be quoted
‘like-this’ as well as `like-this'.
* etc/NEWS: Mention this.
* lisp/cedet/srecode/texi.el (srecode-texi-texify-docstring):
* lisp/emacs-lisp/checkdoc.el (checkdoc-this-string-valid-engine):
* lisp/emacs-lisp/lisp-mode.el (lisp-el-font-lock-keywords-2)
(lisp-cl-font-lock-keywords-2):
* lisp/finder.el (finder-font-lock-keywords):
* lisp/gnus/gnus-art.el (gnus-button-alist):
* lisp/help-mode.el (help-xref-symbol-regexp)
(help-xref-info-regexp, help-xref-url-regexp):
* lisp/international/mule-cmds.el (help-xref-mule-regexp-template):
* lisp/progmodes/elisp-mode.el (emacs-lisp-mode):
Add "‘" and "’" to electric-pair-text-pairs.
(elisp--form-quoted-p): Also allow "‘" as a quoting char.
(elisp-completion-at-point, elisp--preceding-sexp):
Also treat "‘" and "’" as quoting chars.
* lisp/wid-edit.el (widget-documentation-link-regexp):
Parse symbols quoted ‘like-this’ as well as `like-this'.
---
 doc/lispref/tips.texi           | 33 ++++++++++++++++++---------------
 etc/NEWS                        |  4 ++++
 lisp/cedet/srecode/texi.el      |  2 +-
 lisp/emacs-lisp/checkdoc.el     |  5 +++--
 lisp/emacs-lisp/lisp-mode.el    |  8 ++++----
 lisp/finder.el                  |  2 +-
 lisp/gnus/gnus-art.el           |  8 ++++----
 lisp/help-mode.el               |  7 ++++---
 lisp/international/mule-cmds.el |  2 +-
 lisp/progmodes/elisp-mode.el    | 24 +++++++++++++-----------
 lisp/wid-edit.el                |  2 +-
 11 files changed, 54 insertions(+), 43 deletions(-)

diff --git a/doc/lispref/tips.texi b/doc/lispref/tips.texi
index cc1f0e4..912d746 100644
--- a/doc/lispref/tips.texi
+++ b/doc/lispref/tips.texi
@@ -658,17 +658,20 @@ starting double-quote is not part of the string!
 @anchor{Docstring hyperlinks}
 @item
 When a documentation string refers to a Lisp symbol, write it as it
-would be printed (which usually means in lower case), with a grave
-accent @samp{`} before and apostrophe @samp{'} after it.  There are
+would be printed (which usually means in lower case), with surrounding
+it with curved single quotes (@samp{‘} and @samp{’}).  There are
 two exceptions: write @code{t} and @code{nil} without surrounding
-punctuation.  For example: @samp{CODE can be `lambda', nil, or t.}
-(In this manual, we use a different convention, with single-quotes
-around symbols.)
+punctuation.  For example: @samp{CODE can be ‘lambda’, nil, or t.}
+
+Documentation strings can also use an older single-quoting convention,
+which quotes symbols with grave accent @samp{`} and apostrophe
+@samp{'}: @samp{`like-this'} rather than @samp{‘like-this’}.  This
+older convention was designed for now-obsolete displays in which grave
+accent and apostrophe were mirror images.
 
 @cindex hyperlinks in documentation strings
 Help mode automatically creates a hyperlink when a documentation string
-uses a symbol name between grave accent and apostrophe, if the symbol
-has either a
+uses a single-quoted symbol name, if the symbol has either a
 function or a variable definition.  You do not need to do anything
 special to make use of this feature.  However, when a symbol has both a
 function definition and a variable definition, and you want to refer to
@@ -678,7 +681,7 @@ immediately before the symbol name.  (Case makes no difference in
 recognizing these indicator words.)  For example, if you write
 
 @example
-This function sets the variable `buffer-file-name'.
+This function sets the variable ‘buffer-file-name’.
 @end example
 
 @noindent
@@ -691,7 +694,7 @@ you can write the words @samp{symbol} or @samp{program} before the
 symbol name to prevent making any hyperlink.  For example,
 
 @example
-If the argument KIND-OF-RESULT is the symbol `list',
+If the argument KIND-OF-RESULT is the symbol ‘list’,
 this function returns a list of all the objects
 that satisfy the criterion.
 @end example
@@ -710,21 +713,21 @@ followed by the word @samp{face}.  In that case, only the face
 documentation will be shown, even if the symbol is also defined as a
 variable or as a function.
 
-To make a hyperlink to Info documentation, write the name of the Info
-node (or anchor) between grave accent and apostrophe, preceded by
+To make a hyperlink to Info documentation, write the single-quoted
+name of the Info node (or anchor), preceded by
 @samp{info node}, @samp{Info node}, @samp{info anchor} or @samp{Info
 anchor}.  The Info file name defaults to @samp{emacs}.  For example,
 
 @smallexample
-See Info node `Font Lock' and Info node `(elisp)Font Lock Basics'.
+See Info node ‘Font Lock’ and Info node ‘(elisp)Font Lock Basics’.
 @end smallexample
 
-Finally, to create a hyperlink to URLs, write the URL between grave
-accent and apostrophe, preceded by @samp{URL}. For example,
+Finally, to create a hyperlink to URLs, write the single-quoted URL,
+preceded by @samp{URL}.  For example,
 
 @smallexample
 The home page for the GNU project has more information (see URL
-`http://www.gnu.org/').
+‘http://www.gnu.org/’).
 @end smallexample
 
 @item
diff --git a/etc/NEWS b/etc/NEWS
index 3f907db..817330f 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -858,6 +858,10 @@ directory at point.
 *** New macros `thread-first' and `thread-last' allow threading a form
     as the first or last argument of subsequent forms.
 
+** Documentation strings now support quoting with curved single quotes
+‘like-this’ in addition to the old style with grave accent and
+apostrophe `like-this'.  The new style looks better on today's displays.
+
 +++
 ** Time-related changes:
 
diff --git a/lisp/cedet/srecode/texi.el b/lisp/cedet/srecode/texi.el
index 5c3f21c..5068b2a 100644
--- a/lisp/cedet/srecode/texi.el
+++ b/lisp/cedet/srecode/texi.el
@@ -253,7 +253,7 @@ that class.
  [ stuff ]  => @code{[ stuff ]}
  Key        => @kbd{Key}     (key is C\\-h, M\\-h, SPC, RET, TAB and the like)
  ...        => @dots{}"
-  (while (string-match "`\\([-a-zA-Z0-9<>.]+\\)'" string)
+  (while (string-match "[`‘]\\([-a-zA-Z0-9<>.]+\\)['’]" string)
     (let* ((vs (substring string (match-beginning 1) (match-end 1)))
 	   (v (intern-soft vs)))
       (setq string
diff --git a/lisp/emacs-lisp/checkdoc.el b/lisp/emacs-lisp/checkdoc.el
index 777fed0..d6a4c7b 100644
--- a/lisp/emacs-lisp/checkdoc.el
+++ b/lisp/emacs-lisp/checkdoc.el
@@ -1554,7 +1554,8 @@ mouse-[0-3]\\)\\)\\>"))
      (save-excursion
        (let ((case-fold-search t)
 	     (ret nil) mb me)
-	 (while (and (re-search-forward "`\\(\\sw\\(\\sw\\|\\s_\\)+\\)'" e t)
+	 (while (and (re-search-forward
+                      "[`‘]\\(\\sw\\(\\sw\\|\\s_\\)+\\)['’]" e t)
 		     (not ret))
 	   (let* ((ms1 (match-string 1))
 		  (sym (intern-soft ms1)))
@@ -1824,7 +1825,7 @@ Replace with \"%s\"? " original replace)
 	   nil)))
      ;; t and nil case
      (save-excursion
-       (if (re-search-forward "\\(`\\(t\\|nil\\)'\\)" e t)
+       (if (re-search-forward "\\([`‘]\\(t\\|nil\\)['’]\\)" e t)
 	   (if (checkdoc-autofix-ask-replace
 		(match-beginning 1) (match-end 1)
 		(format "%s should not appear in quotes.  Remove? "
diff --git a/lisp/emacs-lisp/lisp-mode.el b/lisp/emacs-lisp/lisp-mode.el
index 108d5cc..476125d 100644
--- a/lisp/emacs-lisp/lisp-mode.el
+++ b/lisp/emacs-lisp/lisp-mode.el
@@ -405,8 +405,8 @@
        ;; Words inside \\[] tend to be for `substitute-command-keys'.
        ("\\\\\\\\\\[\\(\\(?:\\sw\\|\\s_\\)+\\)\\]"
         (1 font-lock-constant-face prepend))
-       ;; Words inside `' tend to be symbol names.
-       ("`\\(\\(?:\\sw\\|\\s_\\)\\(?:\\sw\\|\\s_\\)+\\)'"
+       ;; Words inside ‘’ and `' tend to be symbol names.
+       ("[`‘]\\(\\(?:\\sw\\|\\s_\\)\\(?:\\sw\\|\\s_\\)+\\)['’]"
         (1 font-lock-constant-face prepend))
        ;; Constant values.
        ("\\_<:\\(?:\\sw\\|\\s_\\)+\\_>" 0 font-lock-builtin-face)
@@ -454,8 +454,8 @@
        ;; Erroneous structures.
        (,(concat "(" cl-errs-re "\\_>")
         (1 font-lock-warning-face))
-       ;; Words inside `' tend to be symbol names.
-       ("`\\(\\(?:\\sw\\|\\s_\\)\\(?:\\sw\\|\\s_\\)+\\)'"
+       ;; Words inside ‘’ and `' tend to be symbol names.
+       ("[`‘]\\(\\(?:\\sw\\|\\s_\\)\\(?:\\sw\\|\\s_\\)+\\)['’]"
         (1 font-lock-constant-face prepend))
        ;; Constant values.
        ("\\_<:\\(?:\\sw\\|\\s_\\)+\\_>" 0 font-lock-builtin-face)
diff --git a/lisp/finder.el b/lisp/finder.el
index 47fab3c..306f2e2 100644
--- a/lisp/finder.el
+++ b/lisp/finder.el
@@ -115,7 +115,7 @@ Each element has the form (KEYWORD . DESCRIPTION).")
   "Syntax table used while in `finder-mode'.")
 
 (defvar finder-font-lock-keywords
-  '(("`\\([^'`]+\\)'" 1 font-lock-constant-face prepend))
+  '(("[`‘]\\([^'`‘’]+\\)['’]" 1 font-lock-constant-face prepend))
   "Font-lock keywords for Finder mode.")
 
 (defvar finder-headmark nil
diff --git a/lisp/gnus/gnus-art.el b/lisp/gnus/gnus-art.el
index 989a424..fea89a8 100644
--- a/lisp/gnus/gnus-art.el
+++ b/lisp/gnus/gnus-art.el
@@ -7827,11 +7827,11 @@ positives are possible."
     ("/\\([a-z][-a-z0-9]+\\.el\\)\\>[^.?]"
      ;; Exclude [.?] for URLs in gmane.emacs.cvs
      1 (>= gnus-button-emacs-level 8) gnus-button-handle-library 1)
-    ("`\\([a-z][-a-z0-9]+\\.el\\)'"
+    ("[`‘]\\([a-z][-a-z0-9]+\\.el\\)['’]"
      1 (>= gnus-button-emacs-level 8) gnus-button-handle-library 1)
-    ("`\\([a-z][a-z0-9]+-[a-z0-9]+-[-a-z0-9]*[a-z]\\|\\(gnus\\|message\\)-[-a-z]+\\)'"
+    ("[`‘]\\([a-z][a-z0-9]+-[a-z0-9]+-[-a-z0-9]*[a-z]\\|\\(gnus\\|message\\)-[-a-z]+\\)['’]"
      0 (>= gnus-button-emacs-level 8) gnus-button-handle-symbol 1)
-    ("`\\([a-z][a-z0-9]+-[a-z]+\\)'"
+    ("[`‘]\\([a-z][a-z0-9]+-[a-z]+\\)['’]"
      0 (>= gnus-button-emacs-level 9) gnus-button-handle-symbol 1)
     ("(setq[ \t\n]+\\([a-z][a-z0-9]+-[-a-z0-9]+\\)[ \t\n]+.+)"
      1 (>= gnus-button-emacs-level 7) gnus-button-handle-describe-variable 1)
@@ -7841,7 +7841,7 @@ positives are possible."
      0 (>= gnus-button-emacs-level 1) gnus-button-handle-describe-function 2)
     ("\\b\\(C-h\\|<?[Ff]1>?\\)[ \t\n]+v[ \t\n]+\\([^ \t\n]+\\)[ \t\n]+RET\\>"
      0 (>= gnus-button-emacs-level 1) gnus-button-handle-describe-variable 2)
-    ("`\\(\\(C-h\\|<?[Ff]1>?\\)[ \t\n]+k[ \t\n]+\\([^']+\\)\\)'"
+    ("[`‘]\\(\\(C-h\\|<?[Ff]1>?\\)[ \t\n]+k[ \t\n]+\\([^'’]+\\)\\)['’]"
      ;; Unlike the other regexps we really have to require quoting
      ;; here to determine where it ends.
      1 (>= gnus-button-emacs-level 1) gnus-button-handle-describe-key 3)
diff --git a/lisp/help-mode.el b/lisp/help-mode.el
index d6679e9..cf8ec0c 100644
--- a/lisp/help-mode.el
+++ b/lisp/help-mode.el
@@ -322,7 +322,7 @@ Commands:
 		    "\\(source \\(?:code \\)?\\(?:of\\|for\\)\\)\\)"
 		    "[ \t\n]+\\)?"
 		    ;; Note starting with word-syntax character:
-		    "`\\(\\sw\\(\\sw\\|\\s_\\)+\\)'"))
+		    "[`‘]\\(\\sw\\(\\sw\\|\\s_\\)+\\)['’]"))
   "Regexp matching doc string references to symbols.
 
 The words preceding the quoted symbol can be used in doc strings to
@@ -337,11 +337,12 @@ when help commands related to multilingual environment (e.g.,

 (defconst help-xref-info-regexp
-  (purecopy "\\<[Ii]nfo[ \t\n]+\\(node\\|anchor\\)[ \t\n]+`\\([^']+\\)'")
+  (purecopy
+   "\\<[Ii]nfo[ \t\n]+\\(node\\|anchor\\)[ \t\n]+[`‘]\\([^'’]+\\)['’]")
   "Regexp matching doc string references to an Info node.")
 
 (defconst help-xref-url-regexp
-  (purecopy "\\<[Uu][Rr][Ll][ \t\n]+`\\([^']+\\)'")
+  (purecopy "\\<[Uu][Rr][Ll][ \t\n]+[`‘]\\([^'’]+\\)['’]")
   "Regexp matching doc string references to a URL.")
 
 ;;;###autoload
diff --git a/lisp/international/mule-cmds.el b/lisp/international/mule-cmds.el
index fd892aa..a49e293 100644
--- a/lisp/international/mule-cmds.el
+++ b/lisp/international/mule-cmds.el
@@ -177,7 +177,7 @@
 		    "\\(charset\\)"
 		    "\\)\\s-+\\)?"
 		    ;; Note starting with word-syntax character:
-		    "`\\(\\sw\\(\\sw\\|\\s_\\)+\\)'")))
+		    "[`‘]\\(\\sw\\(\\sw\\|\\s_\\)+\\)['’]")))
 
 (defun coding-system-change-eol-conversion (coding-system eol-type)
   "Return a coding system which differs from CODING-SYSTEM in EOL conversion.
diff --git a/lisp/progmodes/elisp-mode.el b/lisp/progmodes/elisp-mode.el
index e06b920..b070029 100644
--- a/lisp/progmodes/elisp-mode.el
+++ b/lisp/progmodes/elisp-mode.el
@@ -231,7 +231,7 @@ Blank lines separate paragraphs.  Semicolons start comments.
   (lisp-mode-variables nil nil 'elisp)
   (add-hook 'after-load-functions #'elisp--font-lock-flush-elisp-buffers)
   (setq-local electric-pair-text-pairs
-              (cons '(?\` . ?\') electric-pair-text-pairs))
+              (append '((?\` . ?\') (?‘ . ?’)) electric-pair-text-pairs))
   (setq imenu-case-fold-search nil)
   (add-function :before-until (local 'eldoc-documentation-function)
                 #'elisp-eldoc-documentation-function)
@@ -394,7 +394,7 @@ It can be quoted, or be inside a quoted form."
                ((or (eq (char-after) ?\[)
                     (progn
                       (skip-chars-backward " ")
-                      (memq (char-before) '(?' ?`))))
+                      (memq (char-before) '(?' ?` ?‘))))
                 (setq res t))
                ((eq (char-before) ?,)
                 (setq nesting nil))))
@@ -459,7 +459,7 @@ It can be quoted, or be inside a quoted form."
 	   (beg (condition-case nil
 		    (save-excursion
 		      (backward-sexp 1)
-		      (skip-syntax-forward "'")
+		      (skip-chars-forward "`',‘")
 		      (point))
 		  (scan-error pos)))
 	   (end
@@ -470,7 +470,7 @@ It can be quoted, or be inside a quoted form."
 		  (save-excursion
 		    (goto-char beg)
 		    (forward-sexp 1)
-                    (skip-chars-backward "'")
+                    (skip-chars-backward "'’")
 		    (when (>= (point) pos)
 		      (point)))
 		(scan-error pos))))
@@ -478,7 +478,7 @@ It can be quoted, or be inside a quoted form."
            (funpos (eq (char-before beg) ?\())
            (quoted (elisp--form-quoted-p beg)))
       (when (and end (or (not (nth 8 (syntax-ppss)))
-                         (eq (char-before beg) ?`)))
+                         (memq (char-before beg) '(?` ?‘))))
         (let ((table-etc
                (if (or (not funpos) quoted)
                    ;; FIXME: We could look at the first element of the list and
@@ -901,15 +901,17 @@ If CHAR is not a character, return nil."
 (defun elisp--preceding-sexp ()
   "Return sexp before the point."
   (let ((opoint (point))
-	ignore-quotes
+	(left-quote ?‘)
 	expr)
     (save-excursion
       (with-syntax-table emacs-lisp-mode-syntax-table
-	;; If this sexp appears to be enclosed in `...'
+	;; If this sexp appears to be enclosed in `...' or ‘...’
 	;; then ignore the surrounding quotes.
-	(setq ignore-quotes
-	      (or (eq (following-char) ?\')
-		  (eq (preceding-char) ?\')))
+	(cond ((eq (preceding-char) ?’)
+	       (progn (forward-char -1) (setq opoint (point))))
+	      ((or (eq (following-char) ?\')
+		   (eq (preceding-char) ?\'))
+	       (setq left-quote ?\`)))
 	(forward-sexp -1)
 	;; If we were after `?\e' (or similar case),
 	;; use the whole thing, not just the `e'.
@@ -933,7 +935,7 @@ If CHAR is not a character, return nil."
 	      (forward-sexp -1))))
 
 	(save-restriction
-	  (if (and ignore-quotes (eq (following-char) ?`))
+	  (if (eq (following-char) left-quote)
               ;; [email-masked] 30-Jul-1997: Skip ` in `variable' so
               ;; that the value is returned, not the name.
 	      (forward-char))
diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el
index 0a95783..dac3b1e 100644
--- a/lisp/wid-edit.el
+++ b/lisp/wid-edit.el
@@ -2863,7 +2863,7 @@ The following properties have special meanings for this widget:
   :type 'boolean
   :group 'widget-documentation)
 
-(defcustom widget-documentation-link-regexp "`\\([^\n`' ]+\\)'"
+(defcustom widget-documentation-link-regexp "[`‘]\\([^\n `'‘’]+\\)['’]"
   "Regexp for matching potential links in documentation strings.
 The first group should be the link itself."
   :type 'regexp
-- 
2.1.0