branch: elpa/flycheck
commit 2a0d9b2e6281e2dccd7e611000dfeccd52b4ae5b
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Pick the error indication side automatically
flycheck-indication-mode now defaults to auto, which resolves to the
left fringe on graphical displays and the left margin on text
terminals. The old left-fringe default silently displayed nothing in
terminal Emacs, one of the oldest out-of-the-box paper cuts.
When indicators end up in a margin that isn't visible, widen it to one
column and restore it when the mode is disabled, leaving margins and
fringes configured by the user or other packages untouched. For the
same reason flycheck-set-indication-mode no longer force-overrides the
fringe and margin widths.
---
CHANGES.rst | 7 ++
doc/user/error-reports.rst | 24 +++---
flycheck-buttercup.el | 15 ++--
flycheck.el | 148 ++++++++++++++++++++++++++++-------
test/specs/test-customization.el | 4 +-
test/specs/test-overlays.el | 165 ++++++++++++++++++++++++++++++++++-----
6 files changed, 293 insertions(+), 70 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 7bb1c8155c..0e10dccdac 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -18,6 +18,13 @@
kept errors and behave as they would in any buffer with that many real
errors; previously they ran unconditionally because the whole result
was discarded.
+- ``flycheck-indication-mode`` defaults to the new value ``auto``, which
+ picks the left fringe on graphical displays and the left margin on text
+ terminals. Previously the default was ``left-fringe``, which silently
+ displayed nothing in terminal Emacs. When the chosen margin isn't
+ visible, Flycheck now widens it automatically and restores it when the
+ mode is disabled. ``flycheck-set-indication-mode`` no longer overrides
+ fringe and margin widths configured by you or other packages.
- [#2161]: Fix the ``org-lint`` checker erroring out on Emacs 31, where
``org-lint`` reports line numbers as strings.
- [#2174]: Fix the ``haskell-ghc`` and ``haskell-stack-ghc`` checkers
diff --git a/doc/user/error-reports.rst b/doc/user/error-reports.rst
index 7f16676222..67ace38325 100644
--- a/doc/user/error-reports.rst
+++ b/doc/user/error-reports.rst
@@ -169,7 +169,11 @@ of the fringes:
.. defcustom:: flycheck-indication-mode
- How Flycheck indicates errors and warnings in the buffer fringes:
+ How Flycheck indicates errors and warnings in the buffer:
+
+ ``auto``
+ Use the left fringe on graphical displays and the left margin on text
+ terminals, where fringes are not available. This is the default.
``left-fringe`` or ``right-fringe``
Use the left or right fringe respectively. Fringes can only contain
@@ -183,18 +187,14 @@ of the fringes:
``nil``
Do not indicate errors and warnings in the fringe or in the margin.
-By default, Emacs displays fringes, but not margins. With ``left-margin`` and
-``right-margin`` indication modes, you will need to enable margins in your
-``.emacs``. For example:
-
-.. code-block:: elisp
-
- (setq-default left-fringe-width 1 right-fringe-width 8
- left-margin-width 1 right-margin-width 0)
+By default, Emacs displays fringes, but not margins. When indicators end up
+in a margin and that margin is not visible, Flycheck widens it to one column
+automatically, and restores it when you disable ``flycheck-mode``. Margins
+that you or other packages configured are never touched, and neither are the
+fringes.
-If you intend to use margins only with Flycheck, consider using
-``flycheck-set-indication-mode`` in a hook instead; this function adjusts
-margins and fringes for the current buffer.
+To switch the indication mode interactively in the current buffer, use
+``M-x flycheck-set-indication-mode``.
.. code-block:: elisp
diff --git a/flycheck-buttercup.el b/flycheck-buttercup.el
index 01f5710bdf..47f8708439 100644
--- a/flycheck-buttercup.el
+++ b/flycheck-buttercup.el
@@ -371,17 +371,20 @@ ERROR is a Flycheck error object."
(level (flycheck-error-level error))
(category (flycheck-error-level-overlay-category level))
(face (get category 'face))
- (fringe-bitmap (flycheck-error-level-fringe-bitmap level))
- (fringe-face (flycheck-error-level-fringe-face level))
- (fringe-icon (list 'left-fringe fringe-bitmap fringe-face)))
+ ;; With indication disabled there is no icon at all
+ (indicator-icon (when-let* ((side
(flycheck--resolve-indication-mode)))
+ (get-char-property
+ 0 'display
+ (flycheck-error-level-make-indicator level side))))
+ (before-string (and overlay (overlay-get overlay 'before-string))))
(expect overlay :to-be-truthy)
(expect (overlay-get overlay 'flycheck-overlay) :to-be-truthy)
(expect (overlay-start overlay) :to-equal (car region))
(expect (overlay-end overlay) :to-equal (cdr region))
(expect (overlay-get overlay 'face) :to-equal face)
- (expect (get-char-property 0 'display
- (overlay-get overlay 'before-string))
- :to-equal fringe-icon)
+ (expect (and before-string
+ (get-char-property 0 'display before-string))
+ :to-equal indicator-icon)
(expect (overlay-get overlay 'category) :to-equal category)
(expect (flycheck-buttercup-error-without-group
(overlay-get overlay 'flycheck-error))
diff --git a/flycheck.el b/flycheck.el
index 80630a4258..7cd9c23915 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -531,13 +531,17 @@ path and tries invoking `executable-find' again."
(when (file-name-directory executable)
(executable-find (expand-file-name executable)))))
-(defcustom flycheck-indication-mode 'left-fringe
+(defcustom flycheck-indication-mode 'auto
"The indication mode for Flycheck errors.
This variable controls how Flycheck indicates errors in buffers.
-May be `left-fringe', `right-fringe', `left-margin',
+May be `auto', `left-fringe', `right-fringe', `left-margin',
`right-margin', or nil.
+If set to `auto', indicate errors in the left fringe on graphical
+displays, and in the left margin on text terminals, where fringes
+are not available. This is the default.
+
If set to `left-fringe' or `right-fringe', indicate errors via
icons in the left and right fringe respectively. If set to
`left-margin' or `right-margin', use the margins instead.
@@ -545,12 +549,14 @@ icons in the left and right fringe respectively. If set
to
If set to nil, do not indicate errors and warnings, but just
highlight them according to `flycheck-highlighting-mode'."
:group 'flycheck
- :type '(choice (const :tag "Indicate in the left fringe" left-fringe)
+ :type '(choice (const :tag "Automatically choose fringe or margin" auto)
+ (const :tag "Indicate in the left fringe" left-fringe)
(const :tag "Indicate in the right fringe" right-fringe)
(const :tag "Indicate in the left margin" left-margin)
(const :tag "Indicate in the right margin" right-margin)
(const :tag "Do not indicate" nil))
- :safe #'symbolp)
+ :safe #'symbolp
+ :package-version '(flycheck . "37"))
(defcustom flycheck-highlighting-mode 'symbols
"The highlighting mode for Flycheck errors and warnings.
@@ -599,33 +605,114 @@ to a potential new indication mode."
(when flycheck-current-errors
(flycheck-buffer)))
+(defun flycheck--resolve-indication-mode ()
+ "Resolve `flycheck-indication-mode' to a concrete side, or nil.
+
+The value `auto' resolves to `left-fringe' when the current
+buffer is displayed on a graphical frame with a visible left
+fringe, and to `left-margin' otherwise; fringes are not available
+on text terminals.
+
+The resolution considers the frame of the first window displaying
+the buffer, falling back to the selected frame when the buffer is
+not displayed anywhere."
+ (if (not (eq flycheck-indication-mode 'auto))
+ flycheck-indication-mode
+ (let* ((window (get-buffer-window (current-buffer) 'visible))
+ (frame (if window (window-frame window) (selected-frame))))
+ (if (and (display-graphic-p frame)
+ ;; The buffer-local fringe width takes precedence over
+ ;; the frame's fringe; nil means inherit from the frame
+ (> (or left-fringe-width
+ (frame-parameter frame 'left-fringe)
+ 0)
+ 0))
+ 'left-fringe
+ 'left-margin))))
+
+(defvar-local flycheck--provisioned-margin nil
+ "The margin side that Flycheck widened in this buffer, if any.")
+
+(defun flycheck--margin-width-var (side)
+ "Return the margin width variable for margin SIDE."
+ (if (eq side 'left-margin) 'left-margin-width 'right-margin-width))
+
+(defun flycheck--update-window-margins ()
+ "Apply the buffer's margin widths to all windows displaying it.
+
+Unlike `flycheck-refresh-fringes-and-margins' this doesn't launch
+a new syntax check, so it is safe to call while reporting errors."
+ (dolist (win (get-buffer-window-list nil nil t))
+ (set-window-margins win left-margin-width right-margin-width)))
+
+(defun flycheck--sync-margin ()
+ "Reconcile the widened margin with the resolved indication mode.
+
+When `flycheck-indication-mode' resolves to a margin that isn't
+visible, widen it by one column, and remember doing so in
+`flycheck--provisioned-margin'. Undo a previous widening when
+indicators no longer resolve to that margin, e.g. after the
+buffer moved to a graphical frame. Margins configured by the
+user or other packages are left alone."
+ (let ((side (flycheck--resolve-indication-mode)))
+ (unless (eq side flycheck--provisioned-margin)
+ (flycheck--release-margin))
+ (when (memq side '(left-margin right-margin))
+ (let ((width-var (flycheck--margin-width-var side)))
+ ;; A nil margin width also means no margin
+ (when (zerop (or (symbol-value width-var) 0))
+ (set width-var 1)
+ (setq flycheck--provisioned-margin side)
+ (flycheck--update-window-margins))))))
+
+(defun flycheck--release-margin ()
+ "Undo the margin widening done by `flycheck--sync-margin'."
+ (when flycheck--provisioned-margin
+ (let ((width-var (flycheck--margin-width-var
+ flycheck--provisioned-margin)))
+ ;; Leave the margin alone if something else widened it meanwhile.
+ ;; Another package could also render into the one-column margin we
+ ;; widened without changing its width; that cannot be detected, so
+ ;; the column is reclaimed regardless.
+ (when (eql (symbol-value width-var) 1)
+ (set width-var 0))
+ (setq flycheck--provisioned-margin nil)
+ (flycheck--update-window-margins))))
+
(defun flycheck-set-indication-mode (&optional mode)
- "Set `flycheck-indication-mode' to MODE and adjust margins and fringes.
+ "Set `flycheck-indication-mode' to MODE in the current buffer.
-When MODE is nil, adjust window parameters without changing the
-mode. This function can be useful as a `flycheck-mode-hook',
-especially if you use margins only in Flycheck buffers.
+Widen the margin of the current buffer if MODE requires one that
+is not visible, as by `flycheck--sync-margin'. When MODE is nil,
+only adjust the margins for the current value of
+`flycheck-indication-mode'.
-When MODE is `left-margin', the left fringe is reduced to 1 pixel
-to save space."
+This function no longer shrinks fringes or margins configured by
+you or other packages; set the fringe and margin width variables
+directly to reclaim the space of unused indication areas."
(interactive (list (intern (completing-read
- "Mode: " '("left-fringe" "right-fringe"
+ "Mode: " '("auto" "left-fringe" "right-fringe"
"left-margin" "right-margin")
nil t nil nil
(prin1-to-string flycheck-indication-mode)))))
- (setq mode (or mode flycheck-indication-mode))
- (pcase mode
- ((or `left-fringe `right-fringe)
- (setq left-fringe-width 8 right-fringe-width 8
- left-margin-width 0 right-margin-width 0))
- (`left-margin
- (setq left-fringe-width 1 right-fringe-width 8
- left-margin-width 1 right-margin-width 0))
- (`right-margin
- (setq left-fringe-width 8 right-fringe-width 8
- left-margin-width 0 right-margin-width 1))
- (_ (user-error "Invalid indication mode")))
- (setq-local flycheck-indication-mode mode)
+ (when mode
+ (unless (memq mode '(auto left-fringe right-fringe
+ left-margin right-margin))
+ (user-error "Invalid indication mode: %S" mode))
+ (setq-local flycheck-indication-mode mode))
+ (flycheck--sync-margin)
+ (pcase (flycheck--resolve-indication-mode)
+ ((and (or `left-fringe `right-fringe) side)
+ ;; Unlike margins, fringes configured away are not widened back;
+ ;; at least tell the user why nothing shows up
+ (let ((width-var (if (eq side 'left-fringe)
+ 'left-fringe-width
+ 'right-fringe-width)))
+ (when (zerop (or (symbol-value width-var)
+ (frame-parameter nil side)
+ 0))
+ (message "The %s is disabled in this buffer; customize `%s' \
+to make Flycheck's indicators visible" side width-var)))))
(flycheck-refresh-fringes-and-margins))
(define-widget 'flycheck-highlighting-style 'lazy
@@ -3037,6 +3124,7 @@ ARG is ‘toggle’; disable the mode otherwise."
(cond
(flycheck-mode
(flycheck-clear)
+ (flycheck--sync-margin)
(pcase-dolist (`(,hook . ,fn) (reverse flycheck-hooks-alist))
(add-hook hook fn nil 'local))
@@ -3462,6 +3550,7 @@ buffer), and if so then clean up global hooks."
(flycheck-clean-deferred-check)
(flycheck-clear)
(setq flycheck--excessive-checkers nil)
+ (flycheck--release-margin)
(flycheck-cancel-error-display-error-at-point-timer)
(flycheck--clear-idle-trigger-timer)
(flycheck--empty-variables)
@@ -4092,6 +4181,9 @@ again."
Add ERRORS to `flycheck-current-errors' and process each error
with `flycheck-process-error-functions'."
+ ;; The frame type may have changed since the mode was enabled, e.g. a
+ ;; buffer redisplayed on a TTY frame of the same daemon
+ (flycheck--sync-margin)
(setq flycheck-current-errors (append errors flycheck-current-errors))
(overlay-recenter (point-max))
(seq-do (lambda (err)
@@ -4871,13 +4963,11 @@ function resolves `conditional' style specifications."
(unless flycheck-highlighting-mode
;; Erase the highlighting from the overlay if requested by the user
(setf (overlay-get overlay 'face) nil))
- (when flycheck-indication-mode
+ (when-let* ((side (flycheck--resolve-indication-mode)))
(setf (overlay-get overlay 'before-string)
- (flycheck-error-level-make-indicator
- level flycheck-indication-mode))
+ (flycheck-error-level-make-indicator level side))
(setf (overlay-get overlay 'wrap-prefix)
- (flycheck-error-level-make-indicator
- level flycheck-indication-mode t))
+ (flycheck-error-level-make-indicator level side t))
;; Preserve existing text-property prefixes so the overlay doesn't
;; clobber indentation set by other modes.
;;
diff --git a/test/specs/test-customization.el b/test/specs/test-customization.el
index 83ffdd4c26..9fea51bd1e 100644
--- a/test/specs/test-customization.el
+++ b/test/specs/test-customization.el
@@ -84,8 +84,8 @@
(describe "flycheck-indication-mode"
- (it "defaults to left-fringe"
- (expect flycheck-indication-mode :to-be 'left-fringe)))
+ (it "defaults to auto"
+ (expect flycheck-indication-mode :to-be 'auto)))
(describe "flycheck-highlighting-mode"
diff --git a/test/specs/test-overlays.el b/test/specs/test-overlays.el
index b955f2b35f..afa533df95 100644
--- a/test/specs/test-overlays.el
+++ b/test/specs/test-overlays.el
@@ -137,35 +137,38 @@
(it "has an info fringe icon"
(flycheck-buttercup-with-temp-buffer
(insert "Hello\n World")
- (pcase-let* ((overlay (flycheck-add-overlay
- (flycheck-error-new-at 1 1 'info)))
- (before-string (overlay-get overlay 'before-string))
- (`(_ ,bitmap ,face)
- (get-text-property 0 'display before-string)))
- (expect face :to-be 'flycheck-fringe-info)
- (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow))))
+ (let ((flycheck-indication-mode 'left-fringe))
+ (pcase-let* ((overlay (flycheck-add-overlay
+ (flycheck-error-new-at 1 1 'info)))
+ (before-string (overlay-get overlay 'before-string))
+ (`(_ ,bitmap ,face)
+ (get-text-property 0 'display before-string)))
+ (expect face :to-be 'flycheck-fringe-info)
+ (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow)))))
(it "has a warning fringe icon"
(flycheck-buttercup-with-temp-buffer
(insert "Hello\n World")
- (pcase-let* ((overlay (flycheck-add-overlay
- (flycheck-error-new-at 1 1 'warning)))
- (before-string (overlay-get overlay 'before-string))
- (`(_ ,bitmap ,face)
- (get-text-property 0 'display before-string)))
- (expect face :to-be 'flycheck-fringe-warning)
- (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow))))
+ (let ((flycheck-indication-mode 'left-fringe))
+ (pcase-let* ((overlay (flycheck-add-overlay
+ (flycheck-error-new-at 1 1 'warning)))
+ (before-string (overlay-get overlay 'before-string))
+ (`(_ ,bitmap ,face)
+ (get-text-property 0 'display before-string)))
+ (expect face :to-be 'flycheck-fringe-warning)
+ (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow)))))
(it "has an error fringe icon"
(flycheck-buttercup-with-temp-buffer
(insert "Hello\n World")
- (pcase-let* ((overlay (flycheck-add-overlay
- (flycheck-error-new-at 1 1 'error)))
- (before-string (overlay-get overlay 'before-string))
- (`(_ ,bitmap ,face)
- (get-text-property 0 'display before-string)))
- (expect face :to-be 'flycheck-fringe-error)
- (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow))))
+ (let ((flycheck-indication-mode 'left-fringe))
+ (pcase-let* ((overlay (flycheck-add-overlay
+ (flycheck-error-new-at 1 1 'error)))
+ (before-string (overlay-get overlay 'before-string))
+ (`(_ ,bitmap ,face)
+ (get-text-property 0 'display before-string)))
+ (expect face :to-be 'flycheck-fringe-error)
+ (expect bitmap :to-be 'flycheck-fringe-bitmap-double-arrow)))))
(it "has a left fringe icon"
(flycheck-buttercup-with-temp-buffer
@@ -259,4 +262,124 @@
:to-equal
"`\u2068int main() {}\u2069': info\n`\u2068main\u2069':
warning\n`\u2068main()\u2069': error"))))))
+(describe "Indication mode"
+
+ (it "resolves auto to the left margin on text terminals"
+ ;; Batch Emacs runs on a dumb terminal where `display-graphic-p' is nil
+ (let ((flycheck-indication-mode 'auto))
+ (expect (flycheck--resolve-indication-mode) :to-be 'left-margin)))
+
+ (it "resolves auto to the left fringe on graphical displays"
+ (let ((flycheck-indication-mode 'auto)
+ (orig (symbol-function 'frame-parameter)))
+ (cl-letf (((symbol-function 'display-graphic-p)
+ (lambda (&optional _) t))
+ ((symbol-function 'frame-parameter)
+ (lambda (frame param)
+ (if (eq param 'left-fringe) 8
+ (funcall orig frame param)))))
+ (expect (flycheck--resolve-indication-mode) :to-be 'left-fringe))))
+
+ (it "resolves auto to the left margin when fringes are disabled"
+ (let ((flycheck-indication-mode 'auto)
+ (orig (symbol-function 'frame-parameter)))
+ (cl-letf (((symbol-function 'display-graphic-p)
+ (lambda (&optional _) t))
+ ((symbol-function 'frame-parameter)
+ (lambda (frame param)
+ (if (eq param 'left-fringe) 0
+ (funcall orig frame param)))))
+ (expect (flycheck--resolve-indication-mode) :to-be 'left-margin))))
+
+ (it "passes explicit modes through unchanged"
+ (let ((flycheck-indication-mode 'right-fringe))
+ (expect (flycheck--resolve-indication-mode) :to-be 'right-fringe))
+ (let ((flycheck-indication-mode nil))
+ (expect (flycheck--resolve-indication-mode) :to-be nil)))
+
+ (it "puts indicators in the margin under auto on text terminals"
+ (flycheck-buttercup-with-temp-buffer
+ (insert "Hello")
+ (let* ((flycheck-indication-mode 'auto)
+ (overlay (flycheck-add-overlay
+ (flycheck-error-new-at 1 1 'error)))
+ (display (get-text-property
+ 0 'display (overlay-get overlay 'before-string))))
+ (expect (car display) :to-equal '(margin left-margin)))))
+
+ (it "widens a zero-width margin when flycheck-mode is enabled"
+ (flycheck-buttercup-with-temp-buffer
+ (let ((flycheck-indication-mode 'auto))
+ (expect left-margin-width :to-equal 0)
+ (flycheck-mode 1)
+ (expect left-margin-width :to-equal 1)
+ (expect flycheck--provisioned-margin :to-be 'left-margin)
+ (flycheck-mode -1)
+ (expect left-margin-width :to-equal 0)
+ (expect flycheck--provisioned-margin :not :to-be-truthy))))
+
+ (it "leaves margins configured by others alone"
+ (flycheck-buttercup-with-temp-buffer
+ (setq left-margin-width 2)
+ (let ((flycheck-indication-mode 'auto))
+ (flycheck-mode 1)
+ (expect left-margin-width :to-equal 2)
+ (expect flycheck--provisioned-margin :not :to-be-truthy)
+ (flycheck-mode -1)
+ (expect left-margin-width :to-equal 2))))
+
+ (it "treats a nil margin width as no margin"
+ (flycheck-buttercup-with-temp-buffer
+ (setq left-margin-width nil)
+ (let ((flycheck-indication-mode 'auto))
+ (flycheck-mode 1)
+ (expect left-margin-width :to-equal 1)
+ (expect flycheck--provisioned-margin :to-be 'left-margin)
+ (flycheck-mode -1))))
+
+ (it "resolves auto to the margin when the buffer disables its fringe"
+ (flycheck-buttercup-with-temp-buffer
+ (setq-local left-fringe-width 0)
+ (let ((flycheck-indication-mode 'auto)
+ (orig (symbol-function 'frame-parameter)))
+ (cl-letf (((symbol-function 'display-graphic-p)
+ (lambda (&optional _) t))
+ ((symbol-function 'frame-parameter)
+ (lambda (frame param)
+ (if (eq param 'left-fringe) 8
+ (funcall orig frame param)))))
+ (expect (flycheck--resolve-indication-mode) :to-be 'left-margin)))))
+
+ (it "releases the margin when indicators move back to the fringe"
+ (flycheck-buttercup-with-temp-buffer
+ (let ((flycheck-indication-mode 'auto)
+ (orig (symbol-function 'frame-parameter)))
+ ;; Provision the margin on a text terminal first
+ (flycheck--sync-margin)
+ (expect flycheck--provisioned-margin :to-be 'left-margin)
+ (expect left-margin-width :to-equal 1)
+ ;; ...then move the buffer to a graphical frame
+ (cl-letf (((symbol-function 'display-graphic-p)
+ (lambda (&optional _) t))
+ ((symbol-function 'frame-parameter)
+ (lambda (frame param)
+ (if (eq param 'left-fringe) 8
+ (funcall orig frame param)))))
+ (flycheck--sync-margin))
+ (expect flycheck--provisioned-margin :not :to-be-truthy)
+ (expect left-margin-width :to-equal 0))))
+
+ (it "moves the provisioned margin when the side changes"
+ (flycheck-buttercup-with-temp-buffer
+ (setq-local flycheck-indication-mode 'left-margin)
+ (flycheck--sync-margin)
+ (expect left-margin-width :to-equal 1)
+ (setq-local flycheck-indication-mode 'right-margin)
+ (flycheck--sync-margin)
+ (expect left-margin-width :to-equal 0)
+ (expect right-margin-width :to-equal 1)
+ (expect flycheck--provisioned-margin :to-be 'right-margin)
+ (flycheck--release-margin)
+ (expect right-margin-width :to-equal 0))))
+
;;; test-overlays.el ends here