emacs/code/elpa/lsp-mode-20230823.446/lsp-diagnostics.el

371 lines
15 KiB
EmacsLisp
Raw Normal View History

;;; lsp-diagnostics.el --- LSP diagnostics integration -*- lexical-binding: t; -*-
;;
;; Copyright (C) 2020 emacs-lsp maintainers
;;
;; 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 <https://www.gnu.org/licenses/>.
;;
;;; Commentary:
;;
;; LSP diagnostics integration
;;
;;; Code:
(require 'lsp-mode)
(defgroup lsp-diagnostics nil
"LSP support for diagnostics"
:prefix "lsp-disagnostics-"
:group 'lsp-mode
:tag "LSP Diagnostics")
;;;###autoload
(define-obsolete-variable-alias 'lsp-diagnostic-package
'lsp-diagnostics-provider "lsp-mode 7.0.1")
(defcustom lsp-diagnostics-provider :auto
"The checker backend provider."
:type
'(choice
(const :tag "Pick flycheck if present and fallback to flymake" :auto)
(const :tag "Pick flycheck" :flycheck)
(const :tag "Pick flymake" :flymake)
(const :tag "Use neither flymake nor lsp" :none)
(const :tag "Prefer flymake" t)
(const :tag "Prefer flycheck" nil))
:group 'lsp-diagnostics
:package-version '(lsp-mode . "6.3"))
;;;###autoload
(define-obsolete-variable-alias 'lsp-flycheck-default-level
'lsp-diagnostics-flycheck-default-level "lsp-mode 7.0.1")
(defcustom lsp-diagnostics-flycheck-default-level 'error
"Error level to use when the server does not report back a diagnostic level."
:type '(choice
(const error)
(const warning)
(const info))
:group 'lsp-diagnostics)
(defcustom lsp-diagnostics-attributes
`((unnecessary :foreground "gray")
(deprecated :strike-through t))
"The Attributes used on the diagnostics.
List containing (tag attributes) where tag is the LSP diagnostic tag and
attributes is a `plist' containing face attributes which will be applied
on top the flycheck face for that error level."
:type '(repeat list)
:group 'lsp-diagnostics)
(defcustom lsp-diagnostics-disabled-modes nil
"A list of major models for which `lsp-diagnostics-mode' should be disabled."
:type '(repeat symbol)
:group 'lsp-diagnostics
:package-version '(lsp-mode . "8.0.0"))
;; Flycheck integration
(declare-function flycheck-mode "ext:flycheck")
(declare-function flycheck-define-generic-checker
"ext:flycheck" (symbol docstring &rest properties))
(declare-function flycheck-error-new "ext:flycheck" t t)
(declare-function flycheck-error-message "ext:flycheck" (err) t)
(declare-function flycheck-define-error-level "ext:flycheck" (level &rest properties))
(declare-function flycheck-buffer "ext:flycheck")
(declare-function flycheck-valid-checker-p "ext:flycheck")
(declare-function flycheck-stop "ext:flycheck")
(defvar flycheck-mode)
(defvar flycheck-check-syntax-automatically)
(defvar flycheck-checker)
(defvar flycheck-checkers)
(defvar-local lsp-diagnostics--flycheck-enabled nil
"True when lsp diagnostics flycheck integration has been enabled in this buffer.")
(defvar-local lsp-diagnostics--flycheck-checker nil
"The value of flycheck-checker before lsp diagnostics was activated.")
(defun lsp-diagnostics--flycheck-level (flycheck-level tags)
"Generate flycheck level from the original FLYCHECK-LEVEL (e.
g. `error', `warning') and list of LSP TAGS."
(let ((name (format "lsp-flycheck-%s-%s"
flycheck-level
(mapconcat #'symbol-name tags "-"))))
(or (intern-soft name)
(let* ((face (--doto (intern (format "%s-face" name))
(copy-face (-> flycheck-level
(get 'flycheck-overlay-category)
(get 'face))
it)
(mapc (lambda (tag)
(apply #'set-face-attribute it nil
(cl-rest (assoc tag lsp-diagnostics-attributes))))
tags)))
(category (--doto (intern (format "%s-category" name))
(setf (get it 'face) face
(get it 'priority) 100)))
(new-level (intern name))
(bitmap (or (get flycheck-level 'flycheck-fringe-bitmaps)
(get flycheck-level 'flycheck-fringe-bitmap-double-arrow))))
(flycheck-define-error-level new-level
:severity (get flycheck-level 'flycheck-error-severity)
:compilation-level (get flycheck-level 'flycheck-compilation-level)
:overlay-category category
:fringe-bitmap bitmap
:fringe-face (get flycheck-level 'flycheck-fringe-face)
:error-list-face face)
new-level))))
(defun lsp-diagnostics--flycheck-calculate-level (severity tags)
"Calculate flycheck level by SEVERITY and TAGS."
(let ((level (pcase severity
(1 'error)
(2 'warning)
(3 'info)
(4 'info)
(_ lsp-flycheck-default-level)))
;; materialize only first tag.
(tags (seq-map (lambda (tag)
(cond
((= tag lsp/diagnostic-tag-unnecessary) 'unnecessary)
((= tag lsp/diagnostic-tag-deprecated) 'deprecated)))
tags)))
(if tags
(lsp-diagnostics--flycheck-level level tags)
level)))
(defun lsp-diagnostics--flycheck-start (checker callback)
"Start an LSP syntax check with CHECKER.
CALLBACK is the status callback passed by Flycheck."
(remove-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer t)
(->> (lsp--get-buffer-diagnostics)
(-map (-lambda ((&Diagnostic :message :severity? :tags? :code? :source?
:range (&Range :start (&Position :line start-line
:character start-character)
:end (&Position :line end-line
:character end-character))))
(flycheck-error-new
:buffer (current-buffer)
:checker checker
:filename buffer-file-name
:message message
:level (lsp-diagnostics--flycheck-calculate-level severity? tags?)
:id code?
:group source?
:line (lsp-translate-line (1+ start-line))
:column (1+ (lsp-translate-column start-character))
:end-line (lsp-translate-line (1+ end-line))
:end-column (1+ (lsp-translate-column end-character)))))
(funcall callback 'finished)))
(defun lsp-diagnostics--flycheck-buffer ()
"Trigger flyckeck on buffer."
(remove-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer t)
(when (bound-and-true-p flycheck-mode)
(flycheck-buffer)))
(defun lsp-diagnostics--flycheck-report ()
"Report flycheck.
This callback is invoked when new diagnostics are received
from the language server."
(when (and (or (memq 'idle-change flycheck-check-syntax-automatically)
(and (memq 'save flycheck-check-syntax-automatically)
(not (buffer-modified-p))))
lsp--cur-workspace)
;; make sure diagnostics are published even if the diagnostics
;; have been received after idle-change has been triggered
(->> lsp--cur-workspace
(lsp--workspace-buffers)
(mapc (lambda (buffer)
(when (and (lsp-buffer-live-p buffer)
(or
(not (bufferp buffer))
(and (get-buffer-window buffer)
(not (-contains? (buffer-local-value 'lsp-on-idle-hook buffer)
'lsp-diagnostics--flycheck-buffer)))))
(lsp-with-current-buffer buffer
(add-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer nil t)
(lsp--idle-reschedule (current-buffer)))))))))
(cl-defgeneric lsp-diagnostics-flycheck-error-explainer (e _server-id)
"Explain a `flycheck-error' E in a generic way depending on the SERVER-ID."
(flycheck-error-message e))
(defvar lsp-diagnostics-mode) ;; properly defined by define-minor-mode below
;;;###autoload
(defun lsp-diagnostics-lsp-checker-if-needed ()
(unless (flycheck-valid-checker-p 'lsp)
(flycheck-define-generic-checker 'lsp
"A syntax checker using the Language Server Protocol (LSP)
provided by lsp-mode.
See https://github.com/emacs-lsp/lsp-mode."
:start #'lsp-diagnostics--flycheck-start
:modes '(lsp-placeholder-mode) ;; placeholder
:predicate (lambda () lsp-diagnostics-mode)
:error-explainer (lambda (e)
(lsp-diagnostics-flycheck-error-explainer
e (lsp--workspace-server-id (car-safe (lsp-workspaces))))))))
(defun lsp-diagnostics-flycheck-enable (&rest _)
"Enable flycheck integration for the current buffer."
(require 'flycheck)
(lsp-diagnostics-lsp-checker-if-needed)
(and (not lsp-diagnostics--flycheck-enabled)
(not (eq flycheck-checker 'lsp))
(setq lsp-diagnostics--flycheck-checker flycheck-checker))
(setq-local lsp-diagnostics--flycheck-enabled t)
(flycheck-mode 1)
(flycheck-stop)
(setq-local flycheck-checker 'lsp)
(lsp-flycheck-add-mode major-mode)
(add-to-list 'flycheck-checkers 'lsp)
(add-hook 'lsp-diagnostics-updated-hook #'lsp-diagnostics--flycheck-report nil t)
(add-hook 'lsp-managed-mode-hook #'lsp-diagnostics--flycheck-report nil t))
(defun lsp-diagnostics-flycheck-disable ()
"Disable flycheck integration for the current buffer is it was enabled."
(when lsp-diagnostics--flycheck-enabled
(flycheck-stop)
(when (eq flycheck-checker 'lsp)
(setq-local flycheck-checker lsp-diagnostics--flycheck-checker))
(setq lsp-diagnostics--flycheck-checker nil)
(setq-local lsp-diagnostics--flycheck-enabled nil)
(when flycheck-mode
(flycheck-mode 1))))
;; Flymake integration
(declare-function flymake-mode "ext:flymake")
(declare-function flymake-make-diagnostic "ext:flymake")
(declare-function flymake-diag-region "ext:flymake")
(defvar flymake-diagnostic-functions)
(defvar flymake-mode)
(defvar-local lsp-diagnostics--flymake-report-fn nil)
(defun lsp-diagnostics--flymake-setup ()
"Setup flymake."
(setq lsp-diagnostics--flymake-report-fn nil)
(add-hook 'flymake-diagnostic-functions 'lsp-diagnostics--flymake-backend nil t)
(add-hook 'lsp-diagnostics-updated-hook 'lsp-diagnostics--flymake-after-diagnostics nil t)
(flymake-mode 1))
(defun lsp-diagnostics--flymake-after-diagnostics ()
"Handler for `lsp-diagnostics-updated-hook'."
(cond
((and lsp-diagnostics--flymake-report-fn flymake-mode)
(lsp-diagnostics--flymake-update-diagnostics))
((not flymake-mode)
(setq lsp-diagnostics--flymake-report-fn nil))))
(defun lsp-diagnostics--flymake-backend (report-fn &rest _args)
"Flymake backend using REPORT-FN."
(let ((first-run (null lsp-diagnostics--flymake-report-fn)))
(setq lsp-diagnostics--flymake-report-fn report-fn)
(when first-run
(lsp-diagnostics--flymake-update-diagnostics))))
(defun lsp-diagnostics--flymake-update-diagnostics ()
"Report new diagnostics to flymake."
(funcall lsp-diagnostics--flymake-report-fn
(-some->> (lsp-diagnostics t)
(gethash (lsp--fix-path-casing buffer-file-name))
(--map (-let* (((&Diagnostic :message :severity?
:range (range &as &Range
:start (&Position :line start-line :character)
:end (&Position :line end-line))) it)
((start . end) (lsp--range-to-region range)))
(when (= start end)
(if-let ((region (flymake-diag-region (current-buffer)
(1+ start-line)
character)))
(setq start (car region)
end (cdr region))
(lsp-save-restriction-and-excursion
(goto-char (point-min))
(setq start (line-beginning-position (1+ start-line))
end (line-end-position (1+ end-line))))))
(flymake-make-diagnostic (current-buffer)
start
end
(cl-case severity?
(1 :error)
(2 :warning)
(t :note))
message))))
;; This :region keyword forces flymake to delete old diagnostics in
;; case the buffer hasn't changed since the last call to the report
;; function. See https://github.com/joaotavora/eglot/issues/159
:region (cons (point-min) (point-max))))
;;;###autoload
(defun lsp-diagnostics--enable ()
"Enable LSP checker support."
(when (and (member lsp-diagnostics-provider '(:auto :none :flycheck :flymake t nil))
(not (member major-mode lsp-diagnostics-disabled-modes)))
(lsp-diagnostics-mode 1)))
(defun lsp-diagnostics--disable ()
"Disable LSP checker support."
(lsp-diagnostics-mode -1))
;;;###autoload
(define-minor-mode lsp-diagnostics-mode
"Toggle LSP diagnostics integration."
:group 'lsp-diagnostics
:global nil
:lighter ""
(cond
(lsp-diagnostics-mode
(cond
((and (or
(and (eq lsp-diagnostics-provider :auto)
(functionp 'flycheck-mode))
(and (eq lsp-diagnostics-provider :flycheck)
(or (functionp 'flycheck-mode)
(user-error "The lsp-diagnostics-provider is set to :flycheck but flycheck is not installed?")))
;; legacy
(null lsp-diagnostics-provider))
(require 'flycheck nil t))
(lsp-diagnostics-flycheck-enable))
((or (eq lsp-diagnostics-provider :auto)
(eq lsp-diagnostics-provider :flymake)
(eq lsp-diagnostics-provider t))
(require 'flymake)
(lsp-diagnostics--flymake-setup))
((not (eq lsp-diagnostics-provider :none))
(lsp--warn "Unable to autoconfigure flycheck/flymake. The diagnostics won't be rendered.")))
(add-hook 'lsp-unconfigure-hook #'lsp-diagnostics--disable nil t))
(t (lsp-diagnostics-flycheck-disable)
(remove-hook 'lsp-unconfigure-hook #'lsp-diagnostics--disable t))))
;;;###autoload
(add-hook 'lsp-configure-hook (lambda ()
(when lsp-auto-configure
(lsp-diagnostics--enable))))
(lsp-consistency-check lsp-diagnostics)
(provide 'lsp-diagnostics)
;;; lsp-diagnostics.el ends here