;;; 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
;; Package-Version: 20260810.759
;; Package-Revision: ac651c6b3101
;; 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:

;; This package is deprecated: Flycheck 38's built-in
;; `flycheck-annotate-mode' replaces it.  See the README for migration.

;; 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 (line-beginning-position)) (line-end-position))))
               (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

(defvar-local flycheck-inline--phantoms nil
  "Remember which phantoms were added to the buffer.")

(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))

(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)
  (let* ((lines (mapcar 'flycheck-error-line errors))
         (line-range (cons (apply 'min lines) (apply 'max lines)))
         (columns (seq-filter 'identity (mapcar 'flycheck-error-column errors)))
         (column-range (and columns
                            (cons (apply 'min columns) (apply 'max columns)))))
    (mapc #'flycheck-inline-display-error
          (seq-filter
           (lambda (error)
             (let ((line (flycheck-error-line error))
                   (column (flycheck-error-column error)))
               (and
                (>= line (car line-range))
                (<= line (cdr line-range))
                (or (and column-range column
                         (>= column (car column-range))
                         (<= column (cdr column-range)))
                    t))))
           (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-help-echo-function nil)
    (setq-local flycheck-display-errors-function #'flycheck-inline-display-errors)
    (setq-local flycheck-clear-displayed-errors-function #'flycheck-inline-hide-errors))
   ;; 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-help-echo-function)
    (kill-local-variable 'flycheck-display-errors-function)
    (kill-local-variable 'flycheck-clear-displayed-errors-function)
    (flycheck-inline-hide-errors))))

(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
