branch: elpa/flycheck
commit 7b45e3d97cb9a3a231b32bf70df70b1d51b3d267
Author: Bozhidar Batsov <[email protected]>
Commit: Bozhidar Batsov <[email protected]>
Modernize the error list: side window, filters, dynamic columns
The error list now pops up in a bottom side window by default (a
display-buffer action users can override or disable), can be filtered
by syntax checker and by message/ID regexp on top of the existing
minimum-level filter, and sizes its File and ID columns to fit the
errors on display instead of truncating at fixed widths.
---
CHANGES.rst | 10 +++
doc/user/error-list.rst | 44 ++++++----
flycheck.el | 190 +++++++++++++++++++++++++++++++++++++-----
test/specs/test-error-list.el | 122 ++++++++++++++++++++++++++-
4 files changed, 326 insertions(+), 40 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 51292f41e8..78e99f202f 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,16 @@
=======================
- Drop support for Emacs 27; Flycheck now requires Emacs 28.1 or newer.
+- The error list pops up in a bottom side window by default, a quarter of
+ the frame tall. Customize the new option
+ ``flycheck-error-list-display-buffer-action`` to change that;
+ ``display-buffer-alist`` entries still take precedence. Note that side
+ windows are unaffected by ``C-x 1``; dismiss the list with ``q``.
+- The error list can now also be filtered by syntax checker (``c``) and
+ by a regexp matching the error message or ID (``/``), in addition to
+ the existing minimum-level filter (``f``); ``F`` resets all filters.
+- The File and ID columns of the error list adapt their width to the
+ errors on display instead of truncating at fixed widths.
- [#1787]: Add the ``:handle-suspicious`` property to command checkers.
It lets a checker translate a "suspicious state" (a non-zero exit
status with no parsable errors, e.g. a missing dependency) into
diff --git a/doc/user/error-list.rst b/doc/user/error-list.rst
index 2c6eff9e15..833b7c1fb3 100644
--- a/doc/user/error-list.rst
+++ b/doc/user/error-list.rst
@@ -55,12 +55,18 @@ post-jump actions like recentering:
Filter the list
===============
-By default the error list shows all errors but sometimes you'd like to hide
-warnings to focus only on real errors. The error list lets you hide all errors
-below a certain level with :kbd:`f`. This key prompts for an error level and
-will remove all errors of lower levels from the list. The filter is permanent
-as long as the error list buffer stays alive or the filter is reset with
-:kbd:`F`.
+By default the error list shows all errors but sometimes you'd like to narrow
+it down. The error list supports three filters, which combine and stay in
+effect as long as the error list buffer stays alive, until you reset them all
+with :kbd:`F`:
+
+* :kbd:`f` prompts for an error level and hides all errors of lower levels.
+* :kbd:`c` prompts for a syntax checker and shows only its errors, e.g. only
+ the type errors from your type checker while ignoring stylistic complaints.
+* :kbd:`/` prompts for a regular expression and shows only errors whose
+ message or ID matches it.
+
+The active filters are shown in the error list's mode line.
Sort the list
=============
@@ -79,15 +85,22 @@ to descending or vice versa.
Tune error list display
=======================
-By default the error list buffer pops up like any other buffer. Flycheck does
-not enforce special rules on how it's displayed and where it's located in the
-frame so essentially the error list pops up at arbitrary places wherever Emacs
-can find a window for it.
+By default the error list pops up in a side window at the bottom of the frame,
+a quarter of the frame tall, like similar lists in contemporary IDEs. Side
+windows are not affected by :kbd:`C-x 1` (`delete-other-windows`); dismiss the
+error list with :kbd:`q` in its window instead. You can change or disable
+this behavior:
+
+.. defcustom:: flycheck-error-list-display-buffer-action
-However you can tell Emacs to obey certain rules when displaying buffers by
-customizing the built-in option `display-buffer-alist`. You can use this
option
-to make the error list display like similar lists in contemporary IDEs like
-VisualStudio, Eclipse, etc. with the following code in your :term:`init file`:
+ The `display-buffer` action used to display the error list. Set to ``nil``
+ to fall back to the default behavior of `display-buffer`, where the error
+ list pops up at an arbitrary place wherever Emacs finds a window for it.
+
+Entries in the built-in option `display-buffer-alist` matching the error list
+buffer take precedence over this action, so any existing window-management
+configuration keeps working. For example, to display the error list at the
+bottom with a third of the frame height instead:
.. code-block:: elisp
@@ -99,9 +112,6 @@ VisualStudio, Eclipse, etc. with the following code in your
:term:`init file`:
(reusable-frames . visible)
(window-height . 0.33)))
-This display rule tells Emacs to always display the error list at the bottom
-side of the frame, occupying a third of the entire height of the frame.
-
.. seealso::
Shackle_
diff --git a/flycheck.el b/flycheck.el
index 7cd9c23915..6dfd9e4f55 100644
--- a/flycheck.el
+++ b/flycheck.el
@@ -1294,6 +1294,25 @@ currently listed."
:risky t
:package-version '(flycheck . "0.20"))
+(defcustom flycheck-error-list-display-buffer-action
+ '((display-buffer-reuse-window display-buffer-in-side-window)
+ (side . bottom)
+ (window-height . 0.25)
+ (preserve-size . (nil . t)))
+ "The `display-buffer' action for the error list buffer.
+
+By default the error list pops up in a side window at the bottom
+of the frame, a quarter of the frame tall. Set to nil to fall
+back to the default behavior of `display-buffer'.
+
+Entries in `display-buffer-alist' matching the error list buffer
+take precedence over this action, so this option composes with
+window-management configurations."
+ :group 'flycheck
+ :type 'sexp
+ :risky t
+ :package-version '(flycheck . "37"))
+
(defcustom flycheck-global-modes t
"Modes for which option `flycheck-mode' is turned on.
@@ -5242,6 +5261,15 @@ the beginning of the buffer."
(defconst flycheck-error-list-buffer "*Flycheck errors*"
"The name of the buffer to show error lists.")
+(defvar-local flycheck-error-list--checker-filter nil
+ "When non-nil, show only errors from this syntax checker.")
+
+(defvar-local flycheck-error-list--message-filter nil
+ "When non-nil, show only errors matching this regexp.
+
+The regexp is matched against the error message and the error
+ID.")
+
(defmacro flycheck-error-list-with-buffer (&rest body)
"Evaluate BODY in flycheck-error-list-buffer, if it exists."
(declare (indent 0) (debug t))
@@ -5252,6 +5280,8 @@ the beginning of the buffer."
(defvar flycheck-error-list-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "f") #'flycheck-error-list-set-filter)
+ (define-key map (kbd "c") #'flycheck-error-list-set-checker-filter)
+ (define-key map (kbd "/") #'flycheck-error-list-set-message-filter)
(define-key map (kbd "F") #'flycheck-error-list-reset-filter)
(define-key map (kbd "n") #'flycheck-error-list-next-error)
(define-key map (kbd "p") #'flycheck-error-list-previous-error)
@@ -5279,26 +5309,76 @@ message to stretch arbitrarily far."
("Level" 8 flycheck-error-list-entry-level-<)
("ID" 6 t)
(,(flycheck-error-list-make-last-column "Message" 'Checker) 0 t)]
- "Table format for the error list.")
+ "Default table format for the error list.
+
+The File and ID columns are resized to fit the errors being
+displayed; see `flycheck-error-list--update-format'.")
(defconst flycheck-error-list-padding 1
"Padding used in error list.")
-(defconst flycheck--error-list-msg-offset
+(defun flycheck--error-list-compute-msg-offset (format)
+ "Compute the space before the message column in FORMAT."
(seq-reduce
(lambda (offset fmt)
(pcase-let* ((`(,_ ,width ,_ . ,props) fmt)
(padding (or (plist-get props :pad-right) 1)))
(+ offset width padding)))
- (seq-subseq flycheck-error-list-format 0 -1)
- flycheck-error-list-padding)
+ (seq-subseq format 0 -1)
+ flycheck-error-list-padding))
+
+(defvar-local flycheck--error-list-msg-offset
+ (flycheck--error-list-compute-msg-offset flycheck-error-list-format)
"Amount of space to use in `flycheck-flush-multiline-message'.")
+(defun flycheck-error-list--column-widths (errors)
+ "Compute the File and ID column widths for ERRORS.
+
+Return a cons cell (FILE-WIDTH . ID-WIDTH)."
+ (let ((file-width 4) (id-width 2))
+ (dolist (err errors)
+ (when-let* ((file (flycheck-error-filename err)))
+ (setq file-width (max file-width
+ (length (file-name-nondirectory file)))))
+ (when-let* ((id (flycheck-error-id err)))
+ (setq id-width (max id-width (length (format "%s" id))))))
+ (cons (min file-width 40) (min id-width 24))))
+
+(defun flycheck-error-list--set-column-width (format name width)
+ "Set the width of the column NAME in FORMAT to WIDTH.
+
+FORMAT is a tabulated list format vector; the other column
+properties are left unchanged. Unknown column names are
+ignored."
+ (when-let* ((index (seq-position format name
+ (lambda (column name)
+ (equal (car column) name)))))
+ (setf (aref format index)
+ (cons name (cons width (cddr (aref format index)))))))
+
+(defun flycheck-error-list--update-format ()
+ "Fit the File and ID column widths to the displayed errors."
+ (pcase-let ((`(,file-width . ,id-width)
+ (flycheck-error-list--column-widths
+ (flycheck-error-list-apply-filter
+ (flycheck-error-list-current-errors)))))
+ (let ((format (copy-sequence flycheck-error-list-format)))
+ (flycheck-error-list--set-column-width format "File" file-width)
+ (flycheck-error-list--set-column-width format "ID" id-width)
+ (setq tabulated-list-format format))
+ (setq flycheck--error-list-msg-offset
+ (flycheck--error-list-compute-msg-offset tabulated-list-format))
+ (tabulated-list-init-header)))
+
(define-derived-mode flycheck-error-list-mode tabulated-list-mode
"Flycheck errors"
"Major mode for listing Flycheck errors.
\\{flycheck-error-list-mode-map}"
+ ;; Fit the column widths on every revert, including a manual
+ ;; `revert-buffer', so that they never go stale
+ (add-hook 'tabulated-list-revert-hook
+ #'flycheck-error-list--update-format nil t)
(setq tabulated-list-format flycheck-error-list-format
;; Sort by location initially
tabulated-list-sort-key (cons "Line" nil)
@@ -5528,10 +5608,14 @@ list."
(flycheck-error-list-highlight-errors preserve-pos))))
(defun flycheck-error-list-mode-line-filter-indicator ()
- "Create a string representing the current error list filter."
- (if flycheck-error-list-minimum-level
- (format " [>= %s]" flycheck-error-list-minimum-level)
- ""))
+ "Create a string representing the current error list filters."
+ (concat
+ (when flycheck-error-list-minimum-level
+ (format " [>= %s]" flycheck-error-list-minimum-level))
+ (when flycheck-error-list--checker-filter
+ (format " [%s]" flycheck-error-list--checker-filter))
+ (when flycheck-error-list--message-filter
+ (format " [/%s/]" flycheck-error-list--message-filter))))
(defun flycheck-error-list-mode-line-suppressed-indicator ()
"Create a string for the mode line about suppressed errors.
@@ -5545,6 +5629,14 @@ suppressed over `flycheck-checker-error-threshold', if
any."
(format " (+%d suppressed)" count)
"")))
+(defun flycheck-error-list--apply-filter-change (thunk)
+ "Call THUNK in the error list buffer and refresh the list."
+ (flycheck-error-list-with-buffer
+ (funcall thunk)
+ (force-mode-line-update)
+ (flycheck-error-list-refresh)
+ (flycheck-error-list-recenter-at (point-min))))
+
(defun flycheck-error-list-set-filter (level)
"Restrict the error list to errors at level LEVEL or higher.
@@ -5554,11 +5646,45 @@ LEVEL is either an error level symbol, or nil, to
remove the filter."
"Minimum error level (errors at lower levels will be hidden): ")))
(when (and level (not (flycheck-error-level-p level)))
(user-error "Invalid level: %s" level))
- (flycheck-error-list-with-buffer
- (setq-local flycheck-error-list-minimum-level level)
- (force-mode-line-update)
- (flycheck-error-list-refresh)
- (flycheck-error-list-recenter-at (point-min))))
+ (flycheck-error-list--apply-filter-change
+ (lambda () (setq-local flycheck-error-list-minimum-level level))))
+
+(defun flycheck-error-list-set-checker-filter (checker)
+ "Restrict the error list to errors from CHECKER.
+
+CHECKER is a syntax checker symbol, or nil to remove the filter."
+ (interactive
+ (list (flycheck-error-list-with-buffer
+ (let ((checkers (seq-uniq
+ (seq-map
+ (lambda (err)
+ (symbol-name (flycheck-error-checker err)))
+ (flycheck-error-list-current-errors)))))
+ (unless checkers
+ (user-error "The error list contains no errors"))
+ (let ((name (completing-read "Show only errors from checker: "
+ checkers nil t)))
+ ;; Empty input removes the filter
+ (and (not (string-empty-p name)) (intern name)))))))
+ (flycheck-error-list--apply-filter-change
+ (lambda () (setq flycheck-error-list--checker-filter checker))))
+
+(defun flycheck-error-list-set-message-filter (regexp)
+ "Restrict the error list to errors matching REGEXP.
+
+REGEXP is matched against the error messages and IDs. An empty
+or nil REGEXP removes the filter."
+ (interactive (list (read-regexp "Show only errors matching regexp")))
+ (setq regexp (and regexp (not (string-empty-p regexp)) regexp))
+ ;; Reject invalid regexps here; storing one would break every
+ ;; subsequent refresh of the error list
+ (when regexp
+ (condition-case err
+ (string-match-p regexp "")
+ (invalid-regexp
+ (user-error "Invalid regexp: %s" (cadr err)))))
+ (flycheck-error-list--apply-filter-change
+ (lambda () (setq flycheck-error-list--message-filter regexp))))
(defun flycheck-error-list-reset-filter (&optional refresh)
"Remove local error filters and reset to the default filter.
@@ -5567,20 +5693,39 @@ Interactively, or with non-nil REFRESH, refresh the
error list."
(interactive '(t))
(flycheck-error-list-with-buffer
(kill-local-variable 'flycheck-error-list-minimum-level)
+ (setq flycheck-error-list--checker-filter nil
+ flycheck-error-list--message-filter nil)
(when refresh
(flycheck-error-list-refresh)
(flycheck-error-list-recenter-at (point-min))
(force-mode-line-update))))
(defun flycheck-error-list-apply-filter (errors)
- "Filter ERRORS according to `flycheck-error-list-minimum-level'."
- (if-let* ((min-level flycheck-error-list-minimum-level)
- (min-severity (flycheck-error-level-severity min-level)))
- (seq-filter (lambda (err) (>= (flycheck-error-level-severity
- (flycheck-error-level err))
- min-severity))
- errors)
- errors))
+ "Filter ERRORS according to the error list filters.
+
+Combines `flycheck-error-list-minimum-level',
+`flycheck-error-list--checker-filter' and
+`flycheck-error-list--message-filter'."
+ (when-let* ((min-level flycheck-error-list-minimum-level)
+ (min-severity (flycheck-error-level-severity min-level)))
+ (setq errors
+ (seq-filter (lambda (err) (>= (flycheck-error-level-severity
+ (flycheck-error-level err))
+ min-severity))
+ errors)))
+ (when-let* ((checker flycheck-error-list--checker-filter))
+ (setq errors
+ (seq-filter (lambda (err) (eq (flycheck-error-checker err) checker))
+ errors)))
+ (when-let* ((regexp flycheck-error-list--message-filter))
+ (setq errors
+ (seq-filter
+ (lambda (err)
+ (or (string-match-p regexp (or (flycheck-error-message err) ""))
+ (when-let* ((id (flycheck-error-id err)))
+ (string-match-p regexp (format "%s" id)))))
+ errors)))
+ errors)
(defcustom flycheck-error-list-after-jump-hook nil
"Functions to run after jumping to an error from the error list.
@@ -5766,7 +5911,8 @@ avoid slowing down editing when the error list is hidden."
;; Show the error list in a side window. Under some configurations of
;; `display-buffer', this may select `flycheck-error-list-buffer' (see URL
;; `https://github.com/flycheck/flycheck/issues/1776').
- (display-buffer flycheck-error-list-buffer)
+ (display-buffer flycheck-error-list-buffer
+ flycheck-error-list-display-buffer-action)
;; Adjust the source, causing a refresh
(flycheck-error-list-set-source source)))
diff --git a/test/specs/test-error-list.el b/test/specs/test-error-list.el
index e793aaa2bc..07f544c41d 100644
--- a/test/specs/test-error-list.el
+++ b/test/specs/test-error-list.el
@@ -195,6 +195,126 @@
(it "shows the level filter in the mode line if set"
(let ((flycheck-error-list-minimum-level 'warning))
(expect (flycheck-error-list-mode-line-filter-indicator)
- :to-equal " [>= warning]"))))))
+ :to-equal " [>= warning]")))
+
+ (it "shows the checker and message filters in the mode line"
+ (let ((flycheck-error-list-minimum-level nil)
+ (flycheck-error-list--checker-filter 'python-ruff)
+ (flycheck-error-list--message-filter "unused"))
+ (expect (flycheck-error-list-mode-line-filter-indicator)
+ :to-equal " [python-ruff] [/unused/]"))))
+
+ (describe "Checker and message filters"
+ (it "filters errors by checker"
+ (let ((flycheck-error-list--checker-filter 'foo)
+ (errors (list (flycheck-error-new-at 1 1 'error "a" :checker
'foo)
+ (flycheck-error-new-at 2 1 'error "b"
+ :checker 'bar))))
+ (expect (flycheck-error-list-apply-filter errors)
+ :to-be-equal-flycheck-errors
+ (list (flycheck-error-new-at 1 1 'error "a"
+ :checker 'foo)))))
+
+ (it "filters errors by message and id regexp"
+ (let ((flycheck-error-list--message-filter "unused\\|W10")
+ (errors (list (flycheck-error-new-at 1 1 'error "unused var")
+ (flycheck-error-new-at 2 1 'error "bad" :id "W101")
+ (flycheck-error-new-at 3 1 'error "other"))))
+ (expect (flycheck-error-list-apply-filter errors)
+ :to-be-equal-flycheck-errors
+ (list (flycheck-error-new-at 1 1 'error "unused var")
+ (flycheck-error-new-at 2 1 'error "bad" :id "W101")))))
+
+ (it "combines all filters"
+ (let ((flycheck-error-list-minimum-level 'warning)
+ (flycheck-error-list--checker-filter 'foo)
+ (flycheck-error-list--message-filter "spam")
+ (errors (list (flycheck-error-new-at 1 1 'error "spam"
+ :checker 'foo)
+ (flycheck-error-new-at 2 1 'info "spam"
+ :checker 'foo)
+ (flycheck-error-new-at 3 1 'error "spam"
+ :checker 'bar)
+ (flycheck-error-new-at 4 1 'error "eggs"
+ :checker 'foo))))
+ (expect (flycheck-error-list-apply-filter errors)
+ :to-be-equal-flycheck-errors
+ (list (flycheck-error-new-at 1 1 'error "spam"
+ :checker 'foo)))))
+
+ (it "resets all filters"
+ (flycheck/with-error-list-buffer
+ (setq-local flycheck-error-list-minimum-level 'error)
+ (setq flycheck-error-list--checker-filter 'foo
+ flycheck-error-list--message-filter "spam")
+ (flycheck-error-list-reset-filter)
+ (expect flycheck-error-list-minimum-level :not :to-be 'error)
+ (expect flycheck-error-list--checker-filter :not :to-be-truthy)
+ (expect flycheck-error-list--message-filter :not :to-be-truthy))))
+
+ (describe "Dynamic columns"
+ (it "fits the File and ID columns to the errors"
+ (expect (flycheck-error-list--column-widths
+ (list (flycheck-error-new-at
+ 1 1 'error "a" :filename "path/to/some-file.el")
+ (flycheck-error-new-at
+ 2 1 'error "b" :id "reportGeneralTypeIssues")))
+ :to-equal '(12 . 23)))
+
+ (it "caps the column widths"
+ (expect (flycheck-error-list--column-widths
+ (list (flycheck-error-new-at
+ 1 1 'error "a"
+ :filename (make-string 100 ?x)
+ :id (make-string 100 ?y))))
+ :to-equal '(40 . 24)))
+
+ (it "uses minimum widths without errors"
+ (expect (flycheck-error-list--column-widths nil)
+ :to-equal '(4 . 2)))
+
+ (it "fits the format to the displayed errors"
+ (flycheck-buttercup-with-temp-buffer
+ (setq flycheck-current-errors
+ (list (flycheck-error-new-at
+ 1 1 'error "a" :checker 'foo
+ :filename "a-rather-long-file-name.el")
+ (flycheck-error-new-at
+ 2 1 'error "b" :checker 'bar :filename "s.el")))
+ (let ((source (current-buffer)))
+ (flycheck/with-error-list-buffer
+ (setq flycheck-error-list-source-buffer source)
+ (flycheck-error-list--update-format)
+ (expect (cadr (aref tabulated-list-format 0))
+ :to-equal (length "a-rather-long-file-name.el"))
+ ;; Errors hidden by a filter don't inflate the columns
+ (setq flycheck-error-list--checker-filter 'bar)
+ (flycheck-error-list--update-format)
+ (expect (cadr (aref tabulated-list-format 0))
+ :to-equal (length "s.el"))
+ (expect flycheck--error-list-msg-offset
+ :to-equal (+ (length "s.el") 1 ; file + padding
+ 5 1 ; line
+ 3 1 ; col
+ 8 1 ; level
+ 2 1 ; id
+ 1)))))) ; list padding
+
+ (it "rejects an invalid message filter regexp"
+ (expect (flycheck-error-list-set-message-filter "[")
+ :to-throw 'user-error)))
+
+ (describe "Display"
+ (it "displays the error list according to the display action"
+ (let (received-action)
+ (cl-letf (((symbol-function 'display-buffer)
+ (lambda (_buffer &optional action &rest _)
+ (setq received-action action)
+ nil)))
+ (flycheck-buttercup-with-temp-buffer
+ (flycheck-mode)
+ (flycheck-list-errors)))
+ (expect received-action
+ :to-equal flycheck-error-list-display-buffer-action))))))
;;; test-error-list.el ends here