summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lisp/whitespace.el255
-rw-r--r--test/lisp/whitespace-tests.el230
2 files changed, 385 insertions, 100 deletions
diff --git a/lisp/whitespace.el b/lisp/whitespace.el
index 8146eff9b0a..ae4d8ae3f06 100644
--- a/lisp/whitespace.el
+++ b/lisp/whitespace.el
@@ -1139,12 +1139,21 @@ Used by function `whitespace-trailing-regexp' (which see).")
"Region whose highlighting depends on `whitespace-point'.")
(defvar-local whitespace-bob-marker nil
- "Used to save locally the bob marker value.
-Used by function `whitespace-post-command-hook' (which see).")
+ "Position of the buffer's first non-empty line.
+This marker is positioned at the beginning of the first line in
+the buffer that contains a non-space character. If no such line
+exists, this is positioned at the end of the buffer (which could
+be after `whitespace-eob-marker' if the buffer contains nothing
+but empty lines).")
(defvar-local whitespace-eob-marker nil
- "Used to save locally the eob marker value.
-Used by function `whitespace-post-command-hook' (which see).")
+ "Position after the buffer's last non-empty line.
+This marker is positioned at the beginning of the first line
+immediately following the last line in the buffer that contains a
+non-space character. If no such line exists, this is positioned
+at the beginning of the buffer (which could be before
+`whitespace-bob-marker' if the buffer contains nothing but empty
+lines).")
(defvar-local whitespace-buffer-changed nil
"Used to indicate locally if buffer changed.
@@ -2059,9 +2068,14 @@ resultant list will be returned."
(delete-overlay ol) ol))
(setq-local whitespace-bob-marker (point-min-marker))
(setq-local whitespace-eob-marker (point-max-marker))
+ (whitespace--update-bob-eob)
(setq-local whitespace-buffer-changed nil)
(add-hook 'post-command-hook #'whitespace-post-command-hook nil t)
(add-hook 'before-change-functions #'whitespace-buffer-changed nil t)
+ (add-hook 'after-change-functions #'whitespace--update-bob-eob
+ ;; The -1 ensures that it runs before any
+ ;; `font-lock-mode' hook functions.
+ -1 t)
;; Add whitespace-mode color into font lock.
(setq
whitespace-font-lock-keywords
@@ -2114,11 +2128,11 @@ resultant list will be returned."
`((,whitespace-big-indent-regexp 1 'whitespace-big-indent t)))
,@(when (memq 'empty whitespace-active-style)
;; Show empty lines at beginning of buffer.
- `((,#'whitespace-empty-at-bob-regexp
- 1 whitespace-empty t)
+ `((,#'whitespace--empty-at-bob-matcher
+ 0 whitespace-empty t)
;; Show empty lines at end of buffer.
- (,#'whitespace-empty-at-eob-regexp
- 1 whitespace-empty t)))
+ (,#'whitespace--empty-at-eob-matcher
+ 0 whitespace-empty t)))
,@(when (or (memq 'space-after-tab whitespace-active-style)
(memq 'space-after-tab::tab whitespace-active-style)
(memq 'space-after-tab::space whitespace-active-style))
@@ -2153,6 +2167,8 @@ resultant list will be returned."
(when (whitespace-style-face-p)
(remove-hook 'post-command-hook #'whitespace-post-command-hook t)
(remove-hook 'before-change-functions #'whitespace-buffer-changed t)
+ (remove-hook 'after-change-functions #'whitespace--update-bob-eob
+ t)
(font-lock-remove-keywords nil whitespace-font-lock-keywords)
(font-lock-flush)))
@@ -2201,115 +2217,83 @@ resultant list will be returned."
(format ".\\{%d\\}" rem)))))
limit t))
-(defun whitespace-empty-at-bob-regexp (limit)
- "Match spaces at beginning of buffer (BOB) which do not contain point at BOB."
- (let ((b (point))
- r)
- (cond
- ;; at bob
- ((= b 1)
- (setq r (and (looking-at whitespace-empty-at-bob-regexp)
- (or (/= whitespace-point 1)
- (progn (whitespace-point--used (match-beginning 0)
- (match-end 0))
- nil))))
- (set-marker whitespace-bob-marker (if r (match-end 1) b)))
- ;; inside bob empty region
- ((<= limit whitespace-bob-marker)
- (setq r (looking-at whitespace-empty-at-bob-regexp))
- (if r
- (when (< (match-end 1) limit)
- (set-marker whitespace-bob-marker (match-end 1)))
- (set-marker whitespace-bob-marker b)))
- ;; intersection with end of bob empty region
- ((<= b whitespace-bob-marker)
- (setq r (looking-at whitespace-empty-at-bob-regexp))
- (set-marker whitespace-bob-marker (if r (match-end 1) b)))
- ;; it is not inside bob empty region
- (t
- (setq r nil)))
- ;; move to end of matching
- (and r (goto-char (match-end 1)))
- r))
-
-
-(defsubst whitespace-looking-back (regexp limit)
+(defun whitespace--empty-at-bob-matcher (limit)
+ "Match empty/space-only lines at beginning of buffer (BoB).
+Match does not extend past position LIMIT. For improved UX, the
+line containing `whitespace-point' and subsequent lines are
+excluded from the match. (The idea is that the user might be
+about to start typing, and if they do, that line and any
+following empty lines will no longer be BoB empty lines.
+Highlighting those lines can be distracting.)"
+ (let ((p (point))
+ (e (min whitespace-bob-marker limit
+ ;; EoB marker will be before BoB marker if the buffer
+ ;; has nothing but empty lines.
+ whitespace-eob-marker
+ (save-excursion (goto-char whitespace-point)
+ (line-beginning-position)))))
+ (when (= p 1)
+ ;; See the comment in `whitespace--update-bob-eob' for why this
+ ;; text property is added here.
+ (put-text-property 1 whitespace-bob-marker
+ 'font-lock-multiline t))
+ (when (< p e)
+ (set-match-data (list p e))
+ (goto-char e))))
+
+(defsubst whitespace--looking-back (regexp)
(save-excursion
- (when (/= 0 (skip-chars-backward " \t\n" limit))
+ (when (/= 0 (skip-chars-backward " \t\n"))
(unless (bolp)
(forward-line 1))
(looking-at regexp))))
-
-(defun whitespace-empty-at-eob-regexp (limit)
- "Match spaces at end of buffer which do not contain the point at end of \
-buffer."
- (let ((b (point))
- (e (1+ (buffer-size)))
- r)
- (cond
- ;; at eob
- ((= limit e)
- (goto-char limit)
- (setq r (whitespace-looking-back whitespace-empty-at-eob-regexp b))
- (when (and r (= whitespace-point e))
- (setq r nil)
- (whitespace-point--used (match-beginning 0) (match-end 0)))
- (if r
- (set-marker whitespace-eob-marker (match-beginning 1))
- (set-marker whitespace-eob-marker limit)
- (goto-char b))) ; return back to initial position
- ;; inside eob empty region
- ((>= b whitespace-eob-marker)
- (goto-char limit)
- (setq r (whitespace-looking-back whitespace-empty-at-eob-regexp b))
- (if r
- (when (> (match-beginning 1) b)
- (set-marker whitespace-eob-marker (match-beginning 1)))
- (set-marker whitespace-eob-marker limit)
- (goto-char b))) ; return back to initial position
- ;; intersection with beginning of eob empty region
- ((>= limit whitespace-eob-marker)
- (goto-char limit)
- (setq r (whitespace-looking-back whitespace-empty-at-eob-regexp b))
- (if r
- (set-marker whitespace-eob-marker (match-beginning 1))
- (set-marker whitespace-eob-marker limit)
- (goto-char b))) ; return back to initial position
- ;; it is not inside eob empty region
- (t
- (setq r nil)))
- r))
-
+(defun whitespace--empty-at-eob-matcher (limit)
+ "Match empty/space-only lines at end of buffer (EoB).
+Match does not extend past position LIMIT. For improved UX, the
+line containing `whitespace-point' and preceding lines are
+excluded from the match. (The idea is that the user might be
+about to start typing, and if they do, that line and previous
+empty lines will no longer be EoB empty lines. Highlighting
+those lines can be distracting.)"
+ (when (= limit (1+ (buffer-size)))
+ ;; See the comment in `whitespace--update-bob-eob' for why this
+ ;; text property is added here.
+ (put-text-property whitespace-eob-marker limit
+ 'font-lock-multiline t))
+ (let ((b (max (point) whitespace-eob-marker
+ whitespace-bob-marker ; See comment in the bob func.
+ (save-excursion (goto-char whitespace-point)
+ (forward-line 1)
+ (point)))))
+ (when (< b limit)
+ (set-match-data (list b limit))
+ (goto-char limit))))
(defun whitespace-buffer-changed (_beg _end)
"Set `whitespace-buffer-changed' variable to t."
(setq whitespace-buffer-changed t))
-
(defun whitespace-post-command-hook ()
"Save current point into `whitespace-point' variable.
Also refontify when necessary."
(unless (and (eq whitespace-point (point))
(not whitespace-buffer-changed))
+ (when (and (not whitespace-buffer-changed)
+ (memq 'empty whitespace-active-style))
+ ;; No need to handle the `whitespace-buffer-changed' case here
+ ;; because that is taken care of by the `font-lock-multiline'
+ ;; text property.
+ (when (<= (min (point) whitespace-point) whitespace-bob-marker)
+ (font-lock-flush 1 whitespace-bob-marker))
+ (when (>= (max (point) whitespace-point) whitespace-eob-marker)
+ (font-lock-flush whitespace-eob-marker (1+ (buffer-size)))))
(setq-local whitespace-buffer-changed nil)
(setq whitespace-point (point)) ; current point position
- (let ((refontify
- (cond
- ;; It is at end of buffer (eob).
- ((= whitespace-point (1+ (buffer-size)))
- (when (whitespace-looking-back whitespace-empty-at-eob-regexp
- nil)
- (match-beginning 0)))
- ;; It is at end of line ...
- ((and (eolp)
- ;; ... with trailing SPACE or TAB
- (or (memq (preceding-char) '(?\s ?\t))))
- (line-beginning-position))
- ;; It is at beginning of buffer (bob).
- ((and (= whitespace-point 1)
- (looking-at whitespace-empty-at-bob-regexp))
- (match-end 0))))
+ (let ((refontify (and (eolp) ; It is at end of line ...
+ ;; ... with trailing SPACE or TAB
+ (or (memq (preceding-char) '(?\s ?\t)))
+ (line-beginning-position)))
(ostart (overlay-start whitespace-point--used)))
(cond
((not refontify)
@@ -2363,6 +2347,77 @@ to `indent-tabs-mode' and `tab-width'."
(when whitespace-mode
(font-lock-flush)))))
+(defun whitespace--update-bob-eob (&optional beg end &rest _)
+ "Update `whitespace-bob-marker' and `whitespace-eob-marker'.
+Also apply `font-lock-multiline' text property. If BEG and END
+are non-nil, assume that only characters in that range have
+changed since the last call to this function (for optimization
+purposes)."
+ (when (memq 'empty whitespace-active-style)
+ ;; When a line is changed, `font-lock-mode' normally limits
+ ;; re-processing to only the changed line. That behavior is
+ ;; problematic for highlighting `empty' lines because adding or
+ ;; deleting a character might affect lines before or after the
+ ;; change. To address this, all `empty' lines are marked with a
+ ;; non-nil `font-lock-multiline' text property. This forces
+ ;; `font-lock-mode' to re-process all of the lines whenever
+ ;; there's an edit within any one of them.
+ ;;
+ ;; The text property must be set on `empty' lines twice per
+ ;; relevant change:
+ ;;
+ ;; 1. Before the change. This is necessary to ensure that
+ ;; previously highlighted lines become un-highlighted if
+ ;; necessary. The text property must be added after the
+ ;; previous `font-lock-mode' run (the run in reaction to the
+ ;; previous change) because `font-lock-mode' clears the text
+ ;; property when it runs.
+ ;;
+ ;; 2. After the change, but before `font-lock-mode' reacts to
+ ;; the change. This is necessary to ensure that new `empty'
+ ;; lines become highlighted.
+ ;;
+ ;; This hook function is responsible for #2, while the
+ ;; `whitespace--empty-at-bob-matcher' and
+ ;; `whitespace--empty-at-eob-matcher' functions are responsible
+ ;; for #1. (Those functions run after `font-lock-mode' clears the
+ ;; text property and before the next change.)
+ (save-excursion
+ (save-restriction
+ (widen)
+ (when (or (null beg)
+ (<= beg (save-excursion
+ (goto-char whitespace-bob-marker)
+ ;; Any change in the first non-`empty'
+ ;; line, even if it's not the first
+ ;; character in the line, can potentially
+ ;; cause subsequent lines to become
+ ;; classified as `empty' (e.g., delete the
+ ;; "x" from " x").
+ (forward-line 1)
+ (point))))
+ (goto-char 1)
+ (set-marker whitespace-bob-marker (point))
+ (save-match-data
+ (when (looking-at whitespace-empty-at-bob-regexp)
+ (set-marker whitespace-bob-marker (match-end 1))
+ (put-text-property (match-beginning 1) (match-end 1)
+ 'font-lock-multiline t))))
+ (when (or (null end)
+ (>= end (save-excursion
+ (goto-char whitespace-eob-marker)
+ ;; See above comment for the BoB case.
+ (forward-line -1)
+ (point))))
+ (goto-char (1+ (buffer-size)))
+ (set-marker whitespace-eob-marker (point))
+ (save-match-data
+ (when (whitespace--looking-back
+ whitespace-empty-at-eob-regexp)
+ (set-marker whitespace-eob-marker (match-beginning 1))
+ (put-text-property (match-beginning 1) (match-end 1)
+ 'font-lock-multiline t))))))))
+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Hacked from visws.el (Miles Bader <miles@gnu.org>)
diff --git a/test/lisp/whitespace-tests.el b/test/lisp/whitespace-tests.el
index 2a59bfe9d80..792e157ec08 100644
--- a/test/lisp/whitespace-tests.el
+++ b/test/lisp/whitespace-tests.el
@@ -20,8 +20,35 @@
;;; Code:
(require 'ert)
+(require 'ert-x)
+(require 'faceup)
(require 'whitespace)
+(defmacro whitespace-tests--with-test-buffer (style &rest body)
+ "Run BODY in a buffer with `whitespace-mode' style STYLE.
+The buffer is displayed in `selected-window', and
+`noninteractive' is set to nil even in batch mode."
+ (declare (debug ((style form) def-body))
+ (indent 1))
+ `(ert-with-test-buffer-selected ()
+ ;; In case global-*-mode is enabled.
+ (whitespace-mode -1)
+ (font-lock-mode -1)
+ (let ((noninteractive nil)
+ (whitespace-style ,style))
+ (font-lock-mode 1)
+ (whitespace-mode 1)
+ ,@body)))
+
+(defun whitespace-tests--faceup (&rest lines)
+ "Convenience wrapper around `faceup-test-font-lock-buffer'.
+Returns non-nil if the concatenated LINES match the current
+buffer's content."
+ (faceup-test-font-lock-buffer nil (apply #'concat lines)))
+(let ((x (get 'faceup-test-font-lock-buffer 'ert-explainer)))
+ (put 'whitespace-tests--faceup 'ert-explainer
+ (lambda (&rest lines) (funcall x nil (apply #'concat lines)))))
+
(defun whitespace-tests--cleanup-string (string)
(with-temp-buffer
(insert string)
@@ -80,6 +107,209 @@
(whitespace-turn-off)
buffer-display-table))))))
+(ert-deftest whitespace-tests--empty-bob ()
+ (whitespace-tests--with-test-buffer '(face empty)
+ (electric-indent-mode -1)
+
+ ;; Insert some empty lines. None of the lines should be
+ ;; highlighted even though point is on the last line because the
+ ;; entire buffer is empty lines.
+ (execute-kbd-macro (kbd "SPC RET C-q TAB RET RET SPC"))
+ (should (equal (buffer-string) " \n\t\n\n "))
+ (should (equal (line-number-at-pos) 4))
+ (should (whitespace-tests--faceup " \n"
+ "\t\n"
+ "\n"
+ " "))
+
+ ;; Adding content on the last line (and keeping point there)
+ ;; should cause the previous lines to be highlighted. Note that
+ ;; the `whitespace-empty' face applies to the newline just before
+ ;; the last line, which has the desired property of extending the
+ ;; highlight the full width of the window.
+ (execute-kbd-macro (kbd "x"))
+ (should (equal (buffer-string) " \n\t\n\n x"))
+ (should (equal (line-number-at-pos) 4))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "\t\n"
+ "\n"
+ "» x"))
+
+ ;; Lines should become un-highlighted as point moves up into the
+ ;; empty lines.
+ (execute-kbd-macro (kbd "<up>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "\t\n"
+ "»\n"
+ " x"))
+ (execute-kbd-macro (kbd "<up>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "»\t\n"
+ "\n"
+ " x"))
+ (execute-kbd-macro (kbd "<up> <home>"))
+ (should (equal (point) 1))
+ (should (whitespace-tests--faceup " \n"
+ "\t\n"
+ "\n"
+ " x"))
+
+ ;; Line 1 should be un-highlighted when point is in line 1 even if
+ ;; point is not bobp.
+ (execute-kbd-macro (kbd "<right>"))
+ (should (equal (line-number-at-pos) 1))
+ (should (> (point) 1))
+ (should (whitespace-tests--faceup " \n"
+ "\t\n"
+ "\n"
+ " x"))
+
+ ;; Make sure lines become re-highlighted as point moves down.
+ (execute-kbd-macro (kbd "<down>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "»\t\n"
+ "\n"
+ " x"))
+ (execute-kbd-macro (kbd "<down>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "\t\n"
+ "»\n"
+ " x"))
+ (execute-kbd-macro (kbd "<down>"))
+ (should (equal (line-number-at-pos) 4))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "\t\n"
+ "\n"
+ "» x"))
+
+ ;; Inserting content on line 2 should un-highlight lines 2 and 3.
+ (execute-kbd-macro (kbd "<up> <up> <end>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (equal (- (point) (line-beginning-position)) 1))
+ (execute-kbd-macro (kbd "y <down> <down>"))
+ (should (equal (line-number-at-pos) 4))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "»\ty\n"
+ "\n"
+ " x"))
+
+ ;; Removing the content on line 2 should re-highlight lines 2 and
+ ;; 3.
+ (execute-kbd-macro (kbd "<up> <up> <end>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (equal (- (point) (line-beginning-position)) 2))
+ (execute-kbd-macro (kbd "DEL <down> <down>"))
+ (should (equal (line-number-at-pos) 4))
+ (should (whitespace-tests--faceup "«:whitespace-empty: \n"
+ "\t\n"
+ "\n"
+ "» x"))))
+
+(ert-deftest whitespace-tests--empty-eob ()
+ (whitespace-tests--with-test-buffer '(face empty)
+ (electric-indent-mode -1)
+
+ ;; Insert some empty lines. None of the lines should be
+ ;; highlighted even though point is on line 1 because the entire
+ ;; buffer is empty lines.
+ (execute-kbd-macro (kbd "RET RET C-q TAB RET SPC C-<home>"))
+ (should (equal (buffer-string) "\n\n\t\n "))
+ (should (equal (line-number-at-pos) 1))
+ (should (whitespace-tests--faceup "\n"
+ "\n"
+ "\t\n"
+ " "))
+
+ ;; Adding content on the first line (and keeping point there)
+ ;; should cause the subsequent lines to be highlighted.
+ (execute-kbd-macro (kbd "x"))
+ (should (equal (buffer-string) "x\n\n\t\n "))
+ (should (equal (line-number-at-pos) 1))
+ (should (whitespace-tests--faceup "x\n"
+ "«:whitespace-empty:\n"
+ "\t\n"
+ " »"))
+
+ ;; Lines should become un-highlighted as point moves down into the
+ ;; empty lines.
+ (execute-kbd-macro (kbd "<down>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "«:whitespace-empty:\t\n"
+ " »"))
+ (execute-kbd-macro (kbd "<down>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "\t\n"
+ "«:whitespace-empty: »"))
+ (execute-kbd-macro (kbd "C-<end>"))
+ (should (equal (line-number-at-pos) 4))
+ (should (eobp))
+ (should (equal (- (point) (line-beginning-position)) 1))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "\t\n"
+ " "))
+
+ ;; The last line should be un-highlighted when point is in that
+ ;; line even if point is not eobp.
+ (execute-kbd-macro (kbd "<left>"))
+ (should (equal (line-number-at-pos) 4))
+ (should (not (eobp)))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "\t\n"
+ " "))
+
+ ;; Make sure lines become re-highlighted as point moves up.
+ (execute-kbd-macro (kbd "<up>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "\t\n"
+ "«:whitespace-empty: »"))
+ (execute-kbd-macro (kbd "<up>"))
+ (should (equal (line-number-at-pos) 2))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "«:whitespace-empty:\t\n"
+ " »"))
+ (execute-kbd-macro (kbd "<up>"))
+ (should (equal (line-number-at-pos) 1))
+ (should (whitespace-tests--faceup "x\n"
+ "«:whitespace-empty:\n"
+ "\t\n"
+ " »"))
+
+ ;; Inserting content on line 3 should un-highlight lines 2 and 3.
+ (execute-kbd-macro (kbd "<down> <down> <home>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (equal (- (point) (line-beginning-position)) 0))
+ (execute-kbd-macro (kbd "y <up> <up>"))
+ (should (equal (line-number-at-pos) 1))
+ (should (whitespace-tests--faceup "x\n"
+ "\n"
+ "y\t\n"
+ "«:whitespace-empty: »"))
+
+ ;; Removing the content on line 3 should re-highlight lines 2 and
+ ;; 3.
+ (execute-kbd-macro (kbd "<down> <down> <home>"))
+ (should (equal (line-number-at-pos) 3))
+ (should (equal (- (point) (line-beginning-position)) 0))
+ (execute-kbd-macro (kbd "<deletechar> <up> <up>"))
+ (should (equal (line-number-at-pos) 1))
+ (should (whitespace-tests--faceup "x\n"
+ "«:whitespace-empty:\n"
+ "\t\n"
+ " »"))))
+
(provide 'whitespace-tests)
;;; whitespace-tests.el ends here