From 126557c6998950c522baab9d344bb62629b7f9c0 Mon Sep 17 00:00:00 2001 From: Philipp Stephani Date: Wed, 28 Jun 2017 23:47:57 +0200 Subject: [PATCH] Electric quotes: Improve support for Markdown mode (Bug#24709) Introduce a new user option 'electric-quote-context-sensitive'. If non-nil, have ' insert an opening quote if sensible. Also introduce a new variable 'electric-quote-code-faces'. Major modes such as 'markdown-mode' can add faces to this list to treat text as inline code and disable electric quoting. * lisp/electric.el (electric-quote-context-sensitive): New user option. (electric-quote-code-faces): New variable. (electric-quote-post-self-insert-function): Treat ' as ` if desired and applicable; disable electric quoting for given faces. * test/lisp/electric-tests.el (electric-quote-opening-single) (electric-quote-closing-single, electric-quote-opening-double) (electric-quote-closing-double) (electric-quote-context-sensitive-backtick) (electric-quote-context-sensitive-bob-single) (electric-quote-context-sensitive-bob-double) (electric-quote-context-sensitive-bol-single) (electric-quote-context-sensitive-bol-double) (electric-quote-context-sensitive-after-space-single) (electric-quote-context-sensitive-after-space-double) (electric-quote-context-sensitive-after-letter-single) (electric-quote-context-sensitive-after-letter-double) (electric-quote-context-sensitive-after-paren-single) (electric-quote-context-sensitive-after-paren-double) (electric-quote-markdown-in-text) (electric-quote-markdown-in-code): New unit tests. --- etc/NEWS | 16 ++++++ lisp/electric.el | 66 +++++++++++++++++-------- test/lisp/electric-tests.el | 116 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+), 19 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 5e10ca9cb6..b0bb5e1465 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -129,6 +129,22 @@ given file is on a case-insensitive filesystem. of curved quotes for 'electric-quote-mode', allowing user to choose the types of quotes to be used. +** The new user option 'electric-quote-context-sensitive' makes +'electric-quote-mode' context sensitive. If it is non-nil, you can +type an ASCII apostrophe to insert an opening or closing quote, +depending on context. Emacs will replace the apostrophe by an opening +quote character at the beginning of the buffer, the beginning of a +line, after a whitespace character, and after an opening parenthesis; +and it will replace the apostrophe by a closing quote character in all +other cases. + +** The new variable 'electric-quote-code-faces' controls when to +disable electric quoting in text modes. Major modes can add faces to +this list; Emacs will temporarily disable 'electric-quote-mode' +whenever point is before a character having such a face. This is +intended for major modes that derive from 'text-mode' but allow inline +code segments, such as 'markdown-mode'. + +++ ** The new user variable 'dired-omit-case-fold' allows the user to customize the case-sensitivity of dired-omit-mode. It defaults to diff --git a/lisp/electric.el b/lisp/electric.el index 4078ef8193..1564df5949 100644 --- a/lisp/electric.el +++ b/lisp/electric.el @@ -443,11 +443,24 @@ electric-quote-paragraph :version "25.1" :type 'boolean :safe 'booleanp :group 'electricity) +(defcustom electric-quote-context-sensitive nil + "Non-nil means to replace \\=' with an electric quote depending on context. +If `electric-quote-context-sensitive' is non-nil, Emacs replaces +\\=' and \\='\\=' with an opening quote after a line break, +whitespace, opening parenthesis, or quote and leaves \\=` alone." + :version "26.1" + :type 'boolean :safe #'booleanp :group 'electricity) + +(defvar electric-quote-code-faces () + "List of faces to treat as inline code in `text-mode'.") + (defun electric-quote-post-self-insert-function () "Function that `electric-quote-mode' adds to `post-self-insert-hook'. This requotes when a quoting key is typed." (when (and electric-quote-mode - (memq last-command-event '(?\' ?\`))) + (or (eq last-command-event ?\') + (and (not electric-quote-context-sensitive) + (eq last-command-event ?\`)))) (let ((start (if (and comment-start comment-use-syntax) (when (or electric-quote-comment electric-quote-string) @@ -462,30 +475,45 @@ electric-quote-post-self-insert-function (syntax-ppss (1- (point))))))))) (and electric-quote-paragraph (derived-mode-p 'text-mode) + ;; FIXME: There should be a ‘cl-disjoint’ function. + (null (cl-intersection (face-at-point nil 'multiple) + electric-quote-code-faces + :test #'eq)) + ;; FIXME: Why is the next form there? It’s never + ;; nil. (or (eq last-command-event ?\`) (save-excursion (backward-paragraph) (point))))))) (pcase electric-quote-chars (`(,q< ,q> ,q<< ,q>>) (when start (save-excursion - (if (eq last-command-event ?\`) - (cond ((search-backward (string q< ?`) (- (point) 2) t) - (replace-match (string q<<)) - (when (and electric-pair-mode - (eq (cdr-safe - (assq q< electric-pair-text-pairs)) - (char-after))) - (delete-char 1)) - (setq last-command-event q<<)) - ((search-backward "`" (1- (point)) t) - (replace-match (string q<)) - (setq last-command-event q<))) - (cond ((search-backward (string q> ?') (- (point) 2) t) - (replace-match (string q>>)) - (setq last-command-event q>>)) - ((search-backward "'" (1- (point)) t) - (replace-match (string q>)) - (setq last-command-event q>))))))))))) + (let ((backtick ?\`)) + (if (or (eq last-command-event ?\`) + (and electric-quote-context-sensitive + (save-excursion + (backward-char) + (or (bobp) (bolp) + (memq (char-before) (list q< q<<)) + (memq (char-syntax (char-before)) + '(?\s ?\()))) + (setq backtick ?\'))) + (cond ((search-backward (string q< backtick) (- (point) 2) t) + (replace-match (string q<<)) + (when (and electric-pair-mode + (eq (cdr-safe + (assq q< electric-pair-text-pairs)) + (char-after))) + (delete-char 1)) + (setq last-command-event q<<)) + ((search-backward (string backtick) (1- (point)) t) + (replace-match (string q<)) + (setq last-command-event q<))) + (cond ((search-backward (string q> ?') (- (point) 2) t) + (replace-match (string q>>)) + (setq last-command-event q>>)) + ((search-backward "'" (1- (point)) t) + (replace-match (string q>)) + (setq last-command-event q>)))))))))))) (put 'electric-quote-post-self-insert-function 'priority 10) diff --git a/test/lisp/electric-tests.el b/test/lisp/electric-tests.el index 78a3765061..6f63d30e75 100644 --- a/test/lisp/electric-tests.el +++ b/test/lisp/electric-tests.el @@ -593,5 +593,121 @@ electric-quote-string :bindings '((electric-quote-string . t)) :test-in-comments nil :test-in-strings nil) +(define-electric-pair-test electric-quote-opening-single + "" "`" :expected-string "‘" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-closing-single + "" "'" :expected-string "’" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-opening-double + "‘" "-`" :expected-string "“" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-closing-double + "’" "-'" :expected-string "”" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-backtick + "" "`" :expected-string "`" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-bob-single + "" "'" :expected-string "‘" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-bob-double + "‘" "-'" :expected-string "“" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-bol-single + "a\n" "--'" :expected-string "a\n‘" :expected-point 4 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-bol-double + "a\n‘" "---'" :expected-string "a\n“" :expected-point 4 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-space-single + " " "-'" :expected-string " ‘" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-space-double + " ‘" "--'" :expected-string " “" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-letter-single + "a" "-'" :expected-string "a’" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-letter-double + "a’" "--'" :expected-string "a”" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-paren-single + "(" "-'" :expected-string "(‘" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-context-sensitive-after-paren-double + "(‘" "--'" :expected-string "(“" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-context-sensitive . t)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-markdown-in-text + "" "'" :expected-string "’" :expected-point 2 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-code-faces font-lock-constant-face)) + :test-in-comments nil :test-in-strings nil) + +(define-electric-pair-test electric-quote-markdown-in-code + #("`a`" 1 2 (face font-lock-constant-face)) "-'" + :expected-string "`'a`" :expected-point 3 + :modes '(text-mode) + :fixture-fn #'electric-quote-local-mode + :bindings '((electric-quote-code-faces font-lock-constant-face)) + :test-in-comments nil :test-in-strings nil) + (provide 'electric-tests) ;;; electric-tests.el ends here -- 2.13.2