|
| 1 | +;;; lsp-diagnostics.el --- LSP diagnostics integration -*- lexical-binding: t; -*- |
| 2 | +;; |
| 3 | +;; Copyright (C) 2020 emacs-lsp maintainers |
| 4 | +;; |
| 5 | +;; This program is free software; you can redistribute it and/or modify |
| 6 | +;; it under the terms of the GNU General Public License as published by |
| 7 | +;; the Free Software Foundation, either version 3 of the License, or |
| 8 | +;; (at your option) any later version. |
| 9 | + |
| 10 | +;; This program is distributed in the hope that it will be useful, |
| 11 | +;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +;; GNU General Public License for more details. |
| 14 | + |
| 15 | +;; You should have received a copy of the GNU General Public License |
| 16 | +;; along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 17 | +;; |
| 18 | +;;; Commentary: |
| 19 | +;; |
| 20 | +;; LSP diagnostics integration |
| 21 | +;; |
| 22 | +;;; Code: |
| 23 | + |
| 24 | +(require 'lsp-mode) |
| 25 | + |
| 26 | +;;;###autoload |
| 27 | +(define-obsolete-variable-alias 'lsp-diagnostic-package |
| 28 | + 'lsp-diagnostics-provider "lsp-mode 7.0.1") |
| 29 | + |
| 30 | +(defcustom lsp-diagnostics-provider :auto |
| 31 | + "The checker backend provider." |
| 32 | + :type |
| 33 | + '(choice |
| 34 | + (const :tag "Pick flycheck if present and fallback to flymake" :auto) |
| 35 | + (const :tag "Pick flycheck" :flycheck) |
| 36 | + (const :tag "Pick flymake" :flymake) |
| 37 | + (const :tag "Use neither flymake nor lsp" :none) |
| 38 | + (const :tag "Prefer flymake" t) |
| 39 | + (const :tag "Prefer flycheck" nil)) |
| 40 | + :group 'lsp-mode |
| 41 | + :package-version '(lsp-mode . "6.3")) |
| 42 | + |
| 43 | +(define-obsolete-variable-alias 'lsp-flycheck-default-level |
| 44 | + 'lsp-diagnostics-flycheck-default-level "lsp-mode 7.0.1") |
| 45 | + |
| 46 | +(defcustom lsp-diagnostics-flycheck-default-level 'error |
| 47 | + "Error level to use when the server does not report back a diagnostic level." |
| 48 | + :type '(choice |
| 49 | + (const error) |
| 50 | + (const warning) |
| 51 | + (const info)) |
| 52 | + :group 'lsp-mode) |
| 53 | + |
| 54 | +(defcustom lsp-diagnostics-attributes |
| 55 | + `((unnecessary :foreground "dim gray") |
| 56 | + (deprecated :strike-through t)) |
| 57 | + "The Attributes used on the diagnostics. |
| 58 | +List containing (tag attributes) where tag is the LSP diagnostic tag and |
| 59 | +attributes is a `plist' containing face attributes which will be applied |
| 60 | +on top the flycheck face for that error level." |
| 61 | + :type '(repeat list) |
| 62 | + :group 'lsp-mode) |
| 63 | + |
| 64 | +;; Flycheck integration |
| 65 | + |
| 66 | +(declare-function flycheck-mode "ext:flycheck") |
| 67 | +(declare-function flycheck-define-generic-checker |
| 68 | + "ext:flycheck" (symbol docstring &rest properties)) |
| 69 | +(declare-function flycheck-error-new "ext:flycheck" t t) |
| 70 | +(declare-function flycheck-error-message "ext:flycheck" (err) t) |
| 71 | +(declare-function flycheck-define-error-level "ext:flycheck" (level &rest properties)) |
| 72 | +(declare-function flycheck-buffer "ext:flycheck") |
| 73 | + |
| 74 | +(declare-function lsp-cpp-flycheck-clang-tidy-error-explainer "lsp-cpp") |
| 75 | + |
| 76 | +(defvar flycheck-check-syntax-automatically) |
| 77 | +(defvar flycheck-checker) |
| 78 | +(defvar flycheck-checkers) |
| 79 | + |
| 80 | +(defun lsp-diagnostics--flycheck-level (flycheck-level tags) |
| 81 | + "Generate flycheck level from the original FLYCHECK-LEVEL (e. |
| 82 | +g. `error', `warning') and list of LSP TAGS." |
| 83 | + (let ((name (format "lsp-flycheck-%s-%s" |
| 84 | + flycheck-level |
| 85 | + (mapconcat #'symbol-name tags "-")))) |
| 86 | + (or (intern-soft name) |
| 87 | + (let* ((face (--doto (intern (format "lsp-%s-face" name)) |
| 88 | + (copy-face (-> flycheck-level |
| 89 | + (get 'flycheck-overlay-category) |
| 90 | + (get 'face)) |
| 91 | + it) |
| 92 | + (mapc (lambda (tag) |
| 93 | + (apply #'set-face-attribute it nil |
| 94 | + (cl-rest (assoc tag lsp-diagnostics-attributes)))) |
| 95 | + tags))) |
| 96 | + (category (--doto (intern (format "lsp-%s-category" name)) |
| 97 | + (setf (get it 'face) face |
| 98 | + (get it 'priority) 100))) |
| 99 | + (new-level (intern name)) |
| 100 | + (bitmap (or (get flycheck-level 'flycheck-fringe-bitmaps) |
| 101 | + (get flycheck-level 'flycheck-fringe-bitmap-double-arrow)))) |
| 102 | + (flycheck-define-error-level new-level |
| 103 | + :severity (get flycheck-level 'flycheck-error-severity) |
| 104 | + :compilation-level (get flycheck-level 'flycheck-compilation-level) |
| 105 | + :overlay-category category |
| 106 | + :fringe-bitmap bitmap |
| 107 | + :fringe-face (get flycheck-level 'flycheck-fringe-face) |
| 108 | + :error-list-face face) |
| 109 | + new-level)))) |
| 110 | + |
| 111 | +(defun lsp-diagnostics--flycheck-calculate-level (severity tags) |
| 112 | + "Calculate flycheck level by SEVERITY and TAGS." |
| 113 | + (let ((level (pcase severity |
| 114 | + (1 'error) |
| 115 | + (2 'warning) |
| 116 | + (3 'info) |
| 117 | + (4 'info) |
| 118 | + (_ lsp-flycheck-default-level))) |
| 119 | + ;; materialize only first tag. |
| 120 | + (tags (seq-map (lambda (tag) |
| 121 | + (cond |
| 122 | + ((= tag lsp/diagnostic-tag-unnecessary) 'unnecessary) |
| 123 | + ((= tag lsp/diagnostic-tag-deprecated) 'deprecated))) |
| 124 | + tags))) |
| 125 | + (if tags |
| 126 | + (lsp-diagnostics--flycheck-level level tags) |
| 127 | + level))) |
| 128 | + |
| 129 | +(defun lsp-diagnostics--flycheck-start (checker callback) |
| 130 | + "Start an LSP syntax check with CHECKER. |
| 131 | +
|
| 132 | +CALLBACK is the status callback passed by Flycheck." |
| 133 | + |
| 134 | + (remove-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer t) |
| 135 | + |
| 136 | + (->> (lsp--get-buffer-diagnostics) |
| 137 | + (-map (-lambda ((&Diagnostic :message :severity? :tags? :code? |
| 138 | + :range (&Range :start (&Position :line start-line |
| 139 | + :character start-character) |
| 140 | + :end (&Position :line end-line |
| 141 | + :character end-character)))) |
| 142 | + (flycheck-error-new |
| 143 | + :buffer (current-buffer) |
| 144 | + :checker checker |
| 145 | + :filename buffer-file-name |
| 146 | + :message message |
| 147 | + :level (lsp-diagnostics--flycheck-calculate-level severity? tags?) |
| 148 | + :id code? |
| 149 | + :line (lsp-translate-line (1+ start-line)) |
| 150 | + :column (1+ (lsp-translate-column start-character)) |
| 151 | + :end-line (lsp-translate-line (1+ end-line)) |
| 152 | + :end-column (1+ (lsp-translate-column end-character))))) |
| 153 | + (funcall callback 'finished))) |
| 154 | + |
| 155 | +(defun lsp-diagnostics--flycheck-buffer () |
| 156 | + "Trigger flyckeck on buffer." |
| 157 | + (remove-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer t) |
| 158 | + (flycheck-buffer)) |
| 159 | + |
| 160 | +(defun lsp-diagnostics--flycheck-report () |
| 161 | + "Report flycheck. |
| 162 | +This callback is invoked when new diagnostics are received |
| 163 | +from the language server." |
| 164 | + (when (and (or (memq 'idle-change flycheck-check-syntax-automatically) |
| 165 | + (and (memq 'save flycheck-check-syntax-automatically) |
| 166 | + (not (buffer-modified-p)))) |
| 167 | + lsp--cur-workspace) |
| 168 | + ;; make sure diagnostics are published even if the diagnostics |
| 169 | + ;; have been received after idle-change has been triggered |
| 170 | + (->> lsp--cur-workspace |
| 171 | + (lsp--workspace-buffers) |
| 172 | + (mapc (lambda (buffer) |
| 173 | + (when (lsp-buffer-live-p buffer) |
| 174 | + (lsp-with-current-buffer buffer |
| 175 | + (add-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer nil t) |
| 176 | + (lsp--idle-reschedule (current-buffer))))))))) |
| 177 | + |
| 178 | +(defun lsp-diagnostics--flycheck-enable (&rest _) |
| 179 | + "Enable flycheck integration for the current buffer." |
| 180 | + (flycheck-define-generic-checker 'lsp |
| 181 | + "A syntax checker using the Language Server Protocol (LSP) |
| 182 | +provided by lsp-mode. |
| 183 | +See https://github.com/emacs-lsp/lsp-mode." |
| 184 | + :start #'lsp-diagnostics--flycheck-start |
| 185 | + :modes '(lsp-placeholder-mode) ;; placeholder |
| 186 | + :predicate (lambda () lsp-mode) |
| 187 | + :error-explainer (lambda (e) |
| 188 | + (cond ((string-prefix-p "clang-tidy" (flycheck-error-message e)) |
| 189 | + (lsp-cpp-flycheck-clang-tidy-error-explainer e)) |
| 190 | + (t (flycheck-error-message e))))) |
| 191 | + (flycheck-mode 1) |
| 192 | + (setq-local flycheck-checker 'lsp) |
| 193 | + (lsp-flycheck-add-mode major-mode) |
| 194 | + (add-to-list 'flycheck-checkers 'lsp) |
| 195 | + (add-hook 'lsp-diagnostics-updated-hook #'lsp-diagnostics--flycheck-report nil t) |
| 196 | + (add-hook 'lsp-managed-mode-hook #'lsp-diagnostics--flycheck-report nil t)) |
| 197 | + |
| 198 | + |
| 199 | +;; Flymake integration |
| 200 | + |
| 201 | +(declare-function flymake-mode "ext:flymake") |
| 202 | +(declare-function flymake-make-diagnostic "ext:flymake") |
| 203 | +(declare-function flymake-diag-region "ext:flymake") |
| 204 | + |
| 205 | +(defvar flymake-diagnostic-functions) |
| 206 | +(defvar flymake-mode) |
| 207 | +(defvar-local lsp-diagnostics--flymake-report-fn nil) |
| 208 | + |
| 209 | +(defun lsp-diagnostics--flymake-setup () |
| 210 | + "Setup flymake." |
| 211 | + (setq lsp-diagnostics--flymake-report-fn nil) |
| 212 | + (flymake-mode 1) |
| 213 | + (add-hook 'flymake-diagnostic-functions 'lsp-diagnostics--flymake-backend nil t) |
| 214 | + (add-hook 'lsp-diagnostics-updated-hook 'lsp-diagnostics--flymake-after-diagnostics nil t)) |
| 215 | + |
| 216 | +(defun lsp-diagnostics--flymake-after-diagnostics () |
| 217 | + "Handler for `lsp-diagnostics-updated-hook'." |
| 218 | + (cond |
| 219 | + ((and lsp-diagnostics--flymake-report-fn flymake-mode) |
| 220 | + (lsp-diagnostics--flymake-update-diagnostics)) |
| 221 | + ((not flymake-mode) |
| 222 | + (setq lsp-diagnostics--flymake-report-fn nil)))) |
| 223 | + |
| 224 | +(defun lsp-diagnostics--flymake-backend (report-fn &rest _args) |
| 225 | + "Flymake backend using REPORT-FN." |
| 226 | + (let ((first-run (null lsp-diagnostics--flymake-report-fn))) |
| 227 | + (setq lsp-diagnostics--flymake-report-fn report-fn) |
| 228 | + (when first-run |
| 229 | + (lsp-diagnostics--flymake-update-diagnostics)))) |
| 230 | + |
| 231 | +(defun lsp-diagnostics--flymake-update-diagnostics () |
| 232 | + "Report new diagnostics to flymake." |
| 233 | + (funcall lsp-diagnostics--flymake-report-fn |
| 234 | + (-some->> (lsp-diagnostics t) |
| 235 | + (gethash (lsp--fix-path-casing buffer-file-name)) |
| 236 | + (--map (-let* (((&Diagnostic :message :severity? |
| 237 | + :range (range &as &Range |
| 238 | + :start (&Position :line start-line :character) |
| 239 | + :end (&Position :line end-line))) it) |
| 240 | + ((start . end) (lsp--range-to-region range))) |
| 241 | + (when (= start end) |
| 242 | + (if-let ((region (flymake-diag-region (current-buffer) |
| 243 | + (1+ start-line) |
| 244 | + character))) |
| 245 | + (setq start (car region) |
| 246 | + end (cdr region)) |
| 247 | + (lsp-save-restriction-and-excursion |
| 248 | + (goto-char (point-min)) |
| 249 | + (setq start (point-at-bol (1+ start-line)) |
| 250 | + end (point-at-eol (1+ end-line)))))) |
| 251 | + (flymake-make-diagnostic (current-buffer) |
| 252 | + start |
| 253 | + end |
| 254 | + (cl-case severity? |
| 255 | + (1 :error) |
| 256 | + (2 :warning) |
| 257 | + (t :note)) |
| 258 | + message)))) |
| 259 | + ;; This :region keyword forces flymake to delete old diagnostics in |
| 260 | + ;; case the buffer hasn't changed since the last call to the report |
| 261 | + ;; function. See https://github.com/joaotavora/eglot/issues/159 |
| 262 | + :region (cons (point-min) (point-max)))) |
| 263 | + |
| 264 | + |
| 265 | + |
| 266 | +;;;###autoload |
| 267 | +(defun lsp-diagnostics--enable () |
| 268 | + "Enable LSP checker support." |
| 269 | + (when (member lsp-diagnostics-provider '(:auto :none :flycheck :flymake t nil)) |
| 270 | + (lsp-diagnostics-mode 1))) |
| 271 | + |
| 272 | +(defun lsp-diagnostics--disable () |
| 273 | + "Disable LSP checker support." |
| 274 | + (lsp-diagnostics-mode -1)) |
| 275 | + |
| 276 | +;;;###autoload |
| 277 | +(define-minor-mode lsp-diagnostics-mode |
| 278 | + "Toggle LSP diagnostics integration." |
| 279 | + :group 'lsp-mode |
| 280 | + :global nil |
| 281 | + :lighter "" |
| 282 | + (cond |
| 283 | + (lsp-diagnostics-mode |
| 284 | + (cond |
| 285 | + ((or |
| 286 | + (and (eq lsp-diagnostics-provider :auto) |
| 287 | + (functionp 'flycheck-mode)) |
| 288 | + (and (eq lsp-diagnostics-provider :flycheck) |
| 289 | + (or (functionp 'flycheck-mode) |
| 290 | + (user-error "The lsp-diagnostics-provider is set to :flycheck but flycheck is not installed?"))) |
| 291 | + ;; legacy |
| 292 | + (null lsp-diagnostics-provider)) |
| 293 | + (lsp-diagnostics--flycheck-enable)) |
| 294 | + ((or (eq lsp-diagnostics-provider :auto) |
| 295 | + (eq lsp-diagnostics-provider :flymake) |
| 296 | + (eq lsp-diagnostics-provider t)) |
| 297 | + (require 'flymake) |
| 298 | + (lsp-diagnostics--flymake-setup)) |
| 299 | + ((not (eq lsp-diagnostics-provider :none)) |
| 300 | + (lsp--warn "Unable to autoconfigure flycheck/flymake. The diagnostics won't be rendered."))) |
| 301 | + |
| 302 | + (add-hook 'lsp-unconfigure-hook #'lsp-diagnostics--disable nil t)) |
| 303 | + (t |
| 304 | + (remove-hook 'lsp-unconfigure-hook #'lsp-diagnostics--disable t)))) |
| 305 | + |
| 306 | +;;;###autoload |
| 307 | +(add-hook 'lsp-configure-hook (lambda () |
| 308 | + (when lsp-auto-configure |
| 309 | + (lsp-diagnostics--enable)))) |
| 310 | + |
| 311 | +(provide 'lsp-diagnostics) |
| 312 | +;;; lsp-diagnostics.el ends here |
0 commit comments