286 lines
9.9 KiB
EmacsLisp
286 lines
9.9 KiB
EmacsLisp
;;; flycheck-inline.el --- Display Flycheck errors inline -*- lexical-binding: t; -*-
|
||
|
||
;; Copyright (C) 2017-2018 fmdkdd
|
||
|
||
;; Author: fmdkdd
|
||
;; URL: https://github.com/flycheck/flycheck-inline
|
||
;; Keywords: tools, convenience
|
||
;; Version: 0.1-cvs
|
||
;; Package-Requires: ((emacs "25.1") (flycheck "32"))
|
||
|
||
;; This file is not part of GNU Emacs.
|
||
|
||
;; This program is free software; you can redistribute it and/or modify
|
||
;; it under the terms of the GNU General Public License as published by
|
||
;; the Free Software Foundation, either version 3 of the License, or
|
||
;; (at your option) any later version.
|
||
|
||
;; This program is distributed in the hope that it will be useful,
|
||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
;; GNU General Public License for more details.
|
||
|
||
;; You should have received a copy of the GNU General Public License
|
||
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
||
;;; Commentary:
|
||
|
||
;; Provide an error display function to show Flycheck errors inline, directly
|
||
;; below their location in the buffer.
|
||
;;
|
||
;; # Setup
|
||
;;
|
||
;; Enable the local minor mode for all flycheck-mode buffers:
|
||
;;
|
||
;; (with-eval-after-load 'flycheck
|
||
;; (add-hook 'flycheck-mode-hook #'flycheck-inline-mode))
|
||
|
||
;;; Code:
|
||
|
||
(require 'flycheck)
|
||
(require 'seq)
|
||
|
||
;;; Displaying line-long overlays (phantoms)
|
||
|
||
(defun flycheck-inline-phantom-display (msg &optional pos err)
|
||
"Display MSG in a phantom directly below POS.
|
||
|
||
MSG is a string that will be put in a line-long overlay (phantom)
|
||
at the line immediately following POS. If POS is nil, current
|
||
point is used instead.
|
||
|
||
Return the displayed phantom."
|
||
(pcase-let* ((p (or pos (point)))
|
||
(`(,offset . ,pos-eol)
|
||
(save-excursion
|
||
(goto-char p)
|
||
(cons (- p (point-at-bol)) (point-at-eol))))
|
||
(ov (make-overlay pos-eol (1+ pos-eol)))
|
||
;; If the error is on the last line, and that line doesn't end
|
||
;; with a newline, the overlay will be displayed at the end of
|
||
;; the line instead of below it. Adding a newline before the
|
||
;; message fixes it.
|
||
(str (concat (when (eq pos-eol (point-max)) "\n")
|
||
(flycheck-inline-indent-message offset msg)
|
||
"\n")))
|
||
(overlay-put ov 'phantom t)
|
||
(overlay-put ov 'after-string str)
|
||
(overlay-put ov 'error err)
|
||
ov))
|
||
|
||
(defun flycheck-inline--contains-point (phantom &optional pt)
|
||
"Whether the given error overlay contains the position PT otherwise `(point)'"
|
||
(let* ((pos (or pt (point)))
|
||
(err (overlay-get phantom 'error))
|
||
(region (flycheck-error-region-for-mode err 'symbols)))
|
||
(and phantom
|
||
;; Must be one of our phantoms (probably unneeded).
|
||
(overlay-get phantom 'phantom)
|
||
;; The underlying error must currently exist.
|
||
err
|
||
(memq err flycheck-current-errors)
|
||
;; Most importantly, point must be within the error bounds.
|
||
region
|
||
(>= pos (car region))
|
||
(<= pos (cdr region)))))
|
||
|
||
(defun flycheck-inline-phantom-delete (phantom)
|
||
"Delete PHANTOM if its region doesn't contain point.
|
||
|
||
Returns the overlay removed or nil."
|
||
(if (flycheck-inline--contains-point phantom)
|
||
nil
|
||
(progn (delete-overlay phantom) t)))
|
||
|
||
(defun flycheck-inline-indent-message (offset msg)
|
||
"Indent all lines of MSG by OFFSET spaces.
|
||
|
||
MSG is trimmed beforehand."
|
||
(let* ((pad (make-string offset ?\s))
|
||
(rep (concat "\n" pad)))
|
||
(concat pad
|
||
(replace-regexp-in-string "\n" rep (string-trim msg)))))
|
||
|
||
|
||
;;; Customization
|
||
|
||
(defgroup flycheck-inline nil
|
||
"Display Flycheck errors inline."
|
||
:prefix "flycheck-inline-"
|
||
:group 'flycheck
|
||
:link '(url-link :tag "Github" "https://github.com/flycheck/flycheck-inline"))
|
||
|
||
(defface flycheck-inline-error
|
||
'((t :inherit compilation-error))
|
||
"Flycheck-inline face for errors."
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:group 'flycheck-inline)
|
||
|
||
(defface flycheck-inline-warning
|
||
'((t :inherit compilation-warning))
|
||
"Flycheck-inline face for warnings."
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:group 'flycheck-inline)
|
||
|
||
(defface flycheck-inline-info
|
||
'((t :inherit compilation-info))
|
||
"Flycheck-inline face for informational messages."
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:group 'flycheck-inline)
|
||
|
||
(defcustom flycheck-inline-display-function #'flycheck-inline-display-phantom
|
||
"Function to display inline errors.
|
||
|
||
This function is used to display inline all errors at point, as
|
||
well as all related errors. It has the signature (MSG &optional
|
||
POS ERR), where MSG is the error message to display, POS its
|
||
buffer position, and ERR is the flycheck error in general."
|
||
:group 'flycheck-inline
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:type '(function :tag "Inline error display function")
|
||
:risky t)
|
||
|
||
(defcustom flycheck-inline-clear-function #'flycheck-inline-clear-phantoms
|
||
"Function to clear all inline errors.
|
||
|
||
It takes no arguments and should remove all inline errors created
|
||
by `flycheck-inline-display-function'."
|
||
:group 'flycheck-inline
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:type '(function :tag "Inline error clear function")
|
||
:risky t)
|
||
|
||
(defcustom flycheck-inline-display-error-id t
|
||
"Whether to display error IDs inline.
|
||
|
||
If non-nil, inline errors will contain the error ID. Error IDs
|
||
are optional: not all checkers suplpy this information. Error
|
||
IDs can also be seen in Flycheck's error list."
|
||
:group 'flycheck-inline
|
||
:type 'boolean
|
||
:package-version '(flycheck-inline . "0.1")
|
||
:safe #'booleanp)
|
||
|
||
|
||
;;; Displaying inline errors with phantoms
|
||
|
||
(defun flycheck-inline--displayed-p (err)
|
||
"Whether the given error is displayed with any inline overlays."
|
||
(seq-find (lambda (p) (eq err (overlay-get p 'error)))
|
||
flycheck-inline--phantoms))
|
||
|
||
(defvar-local flycheck-inline--phantoms nil
|
||
"Remember which phantoms were added to the buffer.")
|
||
|
||
(defun flycheck-inline-display-phantom (msg &optional pos err)
|
||
"Display MSG at POS representing error ERR using phantoms.
|
||
|
||
POS defaults to point."
|
||
(unless (flycheck-inline--displayed-p err)
|
||
(push (flycheck-inline-phantom-display msg pos err) flycheck-inline--phantoms)))
|
||
|
||
(defun flycheck-inline-clear-phantoms ()
|
||
"Remove all phantoms from buffer that don't contain point."
|
||
(setq flycheck-inline--phantoms
|
||
(seq-remove #'flycheck-inline-phantom-delete flycheck-inline--phantoms)))
|
||
|
||
|
||
|
||
;;; Display inline errors
|
||
|
||
(defun flycheck-inline--error-position (err)
|
||
"Return the position to insert ERR at."
|
||
(if (flycheck-relevant-error-other-file-p err)
|
||
;; Display overlays for other-file errors on the first line
|
||
(point-min)
|
||
(flycheck-error-pos err)))
|
||
|
||
(defun flycheck-inline--error-message (err)
|
||
"Return the message to display for ERR."
|
||
(let ((filename (flycheck-error-filename err))
|
||
(id (flycheck-error-id err)))
|
||
(concat (when (and filename (not (equal filename (buffer-file-name))))
|
||
(format "In \"%s\":\n" (file-relative-name filename default-directory)))
|
||
(flycheck-error-message err)
|
||
(when (and id flycheck-inline-display-error-id)
|
||
(format " [%s]" id)))))
|
||
|
||
(defun flycheck-inline--error-face (err)
|
||
"Return the face used to display ERR."
|
||
(pcase (flycheck-error-level err)
|
||
(`info 'flycheck-inline-info)
|
||
(`warning 'flycheck-inline-warning)
|
||
(`error 'flycheck-inline-error)))
|
||
|
||
(defun flycheck-inline-display-error (err)
|
||
"Display `flycheck-error' ERR inline."
|
||
(let* ((pos (flycheck-inline--error-position err))
|
||
(msg (propertize (flycheck-inline--error-message err)
|
||
'face (flycheck-inline--error-face err))))
|
||
(funcall flycheck-inline-display-function msg pos err)))
|
||
|
||
(defun flycheck-inline-hide-errors ()
|
||
"Hide all inline messages currently being shown."
|
||
(funcall flycheck-inline-clear-function))
|
||
|
||
(defun flycheck-inline-display-errors (errors)
|
||
"Display ERRORS, and all related errors, inline.
|
||
|
||
ERRORS is a list of `flycheck-error' objects."
|
||
(flycheck-inline-hide-errors)
|
||
(mapc #'flycheck-inline-display-error
|
||
(seq-uniq
|
||
(seq-mapcat #'flycheck-related-errors errors))))
|
||
|
||
|
||
;;; Global and local minor modes
|
||
|
||
;;;###autoload
|
||
(define-minor-mode flycheck-inline-mode
|
||
"A minor mode to show Flycheck error messages line.
|
||
|
||
When called interactively, toggle `flycheck-inline-mode'. With
|
||
prefix ARG, enable `flycheck-inline-mode' if ARG is positive,
|
||
otherwise disable it.
|
||
|
||
When called from Lisp, enable `flycheck-inline-mode' if ARG is
|
||
omitted, nil or positive. If ARG is `toggle', toggle
|
||
`flycheck-inline-mode'. Otherwise behave as if called
|
||
interactively.
|
||
|
||
In `flycheck-inline-mode', show Flycheck error messages inline,
|
||
directly below the error reported location."
|
||
:group 'flycheck-inline
|
||
:require 'flycheck-inline
|
||
(cond
|
||
;; Use our display function.
|
||
(flycheck-inline-mode
|
||
(setq-local flycheck-display-errors-function #'flycheck-inline-display-errors)
|
||
(add-hook 'post-command-hook #'flycheck-inline-hide-errors nil 'local))
|
||
;; Reset the display function and remove ourselves from all hooks but only
|
||
;; if the mode is still active.
|
||
((not flycheck-inline-mode)
|
||
(kill-local-variable 'flycheck-display-errors-function)
|
||
(flycheck-inline-hide-errors)
|
||
(remove-hook 'post-command-hook #'flycheck-inline-hide-errors 'local))))
|
||
|
||
(defun turn-on-flycheck-inline ()
|
||
"Turn on `flycheck-inline-mode' in Flycheck buffers."
|
||
;; Make sure to turn on flycheck-inline in this buffer, either directly if
|
||
;; flycheck is already loaded, or via a hook if flycheck hasn't been loaded
|
||
;; yet.
|
||
(if flycheck-mode
|
||
(flycheck-inline-mode)
|
||
(add-hook 'flycheck-mode-hook #'flycheck-inline-mode nil 'local)))
|
||
|
||
;;;###autoload
|
||
(define-global-minor-mode global-flycheck-inline-mode
|
||
flycheck-inline-mode turn-on-flycheck-inline
|
||
"Toggle flycheck-inline in all Flycheck buffers."
|
||
:group 'flycheck-inline
|
||
:require 'flycheck-inline)
|
||
|
||
(provide 'flycheck-inline)
|
||
|
||
;;; flycheck-inline.el ends here
|