Skip to content

Commit 159015b

Browse files
committed
Add support for LSP diagnostic "additional information".
This change provides support for additional information that can be provided with an LSP diagnostic, for both Flycheck and Flymake interfaces. The main change treats each piece of "additional information" as a separate diagnostic with the same severity level as the main diagnostic. Flycheck supports the concept of a group, so for Flycheck diagnostics the main diagnostic and all of it's "additional information" diagnostics are placed in the same Flycheck group. "Additional information" diagnostics may refer to a file which is not the current buffer. Flycheck handles this as part of it's normal diagnostic interface, however Flymake makes a distinction between diagnostics related to the current buffer and those external to it (i.e., foreign diagnostics). For Flymake, the foreign diagnostics are managed in the `flymake-list-only-diagnostics` variable. Flymake treats these as interim diagnostics until the file has been opened. Therefore, diagnostics placed in `flymake-list-only-diagnostics` are removed when that buffer reports its own diagnostics. Additionally, with Flymake, foreign diagnostics are only visible when reporting diagnostics for the project (`flymake-show-project-diagnostics`), not when reporting diagnostics for the current buffer (`flymake-show-buffer-diagnostics`). Flycheck on the other hand, does not make this distinction and will report both types of diagnostics together (`flycheck-list-errors`). Additional changes that drive conformity between Flycheck and Flymake were also applied. When an LSP diagnostic provides a source, this is supplied to Flycheck as the "checker", otherwise the default "lsp" checker is used. This is useful to properly identify the LSP backends generating the diagnostics when it is available. Flymake doesn't provide a way to specify the checker (or a code), so the diagnostic message is appended with this information when provided.
1 parent 27d6e79 commit 159015b

File tree

1 file changed

+191
-52
lines changed

1 file changed

+191
-52
lines changed

lsp-diagnostics.el

Lines changed: 191 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -147,35 +147,106 @@ g. `error', `warning') and list of LSP TAGS."
147147
(lsp-diagnostics--flycheck-level level tags)
148148
level)))
149149

150+
(lsp-defun lsp-diagnostics--flycheck-range-to-region
151+
((range &as &Range
152+
:start (start &as &Position
153+
:line start-line
154+
:character start-column)
155+
:end (end &as &Position
156+
:line end-line
157+
:character end-column))
158+
external)
159+
"Determine diagnostic region from RANGE."
160+
(if external
161+
(let ((start-line (1+ start-line))
162+
(start-column (1+ start-column))
163+
(end-line (1+ end-line))
164+
(end-column (1+ end-column)))
165+
(if (lsp--position-equal start end)
166+
(if (= start-column 1)
167+
;; Highlight entire line
168+
(cons (cons start-line nil)
169+
(cons start-line nil))
170+
;; Approximate using `flycheck-highlighting-mode'
171+
(cons (cons start-line start-column)
172+
(cons start-line nil)))
173+
(cons (cons start-line start-column)
174+
(cons end-line end-column))))
175+
;; Diagnostic in current buffer
176+
(let ((start-line (lsp-translate-line (1+ start-line)))
177+
(start-column (1+ (lsp-translate-column start-column)))
178+
(end-line (lsp-translate-line (1+ end-line)))
179+
(end-column (1+ (lsp-translate-column end-column))))
180+
(if (lsp--position-equal start end)
181+
(if (= start-column 1)
182+
;; Highlight entire line
183+
(cons (cons start-line nil)
184+
(cons start-line nil))
185+
;; Approximate using `flycheck-highlighting-mode'
186+
(cons (cons start-line start-column)
187+
(cons start-line nil)))
188+
(cons (cons start-line start-column)
189+
(cons end-line end-column))))))
190+
150191
(defun lsp-diagnostics--flycheck-start (checker callback)
151192
"Start an LSP syntax check with CHECKER.
152193
153194
CALLBACK is the status callback passed by Flycheck."
154195

155196
(remove-hook 'lsp-on-idle-hook #'lsp-diagnostics--flycheck-buffer t)
156197

157-
(->> (lsp--get-buffer-diagnostics)
158-
(-map (-lambda ((&Diagnostic :message :severity? :tags? :code? :source?
159-
:range (&Range :start (start &as &Position
160-
:line start-line
161-
:character start-character)
162-
:end (end &as &Position
163-
:line end-line
164-
:character end-character))))
165-
(flycheck-error-new
166-
:buffer (current-buffer)
167-
:checker checker
168-
:filename buffer-file-name
169-
:message message
170-
:level (lsp-diagnostics--flycheck-calculate-level severity? tags?)
171-
:id code?
172-
:group source?
173-
:line (lsp-translate-line (1+ start-line))
174-
:column (1+ (lsp-translate-column start-character))
175-
:end-line (lsp-translate-line (1+ end-line))
176-
:end-column (unless (lsp--position-equal start end)
177-
(1+ (lsp-translate-column end-character))))))
178-
(funcall callback 'finished)))
198+
(let ((diagnostics nil)
199+
(path (lsp--fix-path-casing buffer-file-name)))
200+
(seq-doseq (diagnostic (lsp--get-buffer-diagnostics))
201+
(-let* (((&Diagnostic :message :severity? :tags? :code?
202+
:source? :related-information?
203+
:range) diagnostic)
204+
(level (lsp-diagnostics--flycheck-calculate-level severity? tags?))
205+
(checker (if (stringp source?) (intern source?) checker))
206+
(group (gensym)))
207+
(-let ((((start-line . start-column) .
208+
(end-line . end-column))
209+
(lsp-diagnostics--flycheck-range-to-region range nil)))
210+
(push
211+
(flycheck-error-new
212+
:buffer (current-buffer)
213+
:checker checker
214+
:filename buffer-file-name
215+
:message message
216+
:level level
217+
:id code?
218+
:group group
219+
:line start-line
220+
:column start-column
221+
:end-line end-line
222+
:end-column end-column)
223+
diagnostics))
224+
(seq-doseq (related-info related-information?)
225+
(-let* (((&DiagnosticRelatedInformation
226+
:message :location (&Location :range :uri)) related-info)
227+
(related-file (lsp--fix-path-casing (lsp--uri-to-path uri)))
228+
(external (not (equal path related-file)))
229+
(((start-line . start-column) .
230+
(end-line . end-column))
231+
(lsp-diagnostics--flycheck-range-to-region range external)))
232+
(push
233+
(flycheck-error-new
234+
:buffer (current-buffer)
235+
:checker checker
236+
:filename related-file
237+
:message message
238+
:level level
239+
:id code?
240+
:group group
241+
:line start-line
242+
:column start-column
243+
:end-line end-line
244+
:end-column end-column)
245+
diagnostics)))))
246+
247+
;; Refresh diagnostics
248+
(setq diagnostics (nreverse diagnostics))
249+
(funcall callback 'finished diagnostics)))
179250

180251
(defun lsp-diagnostics--flycheck-buffer ()
181252
"Trigger flyckeck on buffer."
@@ -260,6 +331,7 @@ See https://github.com/emacs-lsp/lsp-mode."
260331
(declare-function flymake-diag-region "ext:flymake")
261332

262333
(defvar flymake-diagnostic-functions)
334+
(defvar flymake-list-only-diagnostics)
263335
(defvar flymake-mode)
264336
(defvar-local lsp-diagnostics--flymake-report-fn nil)
265337

@@ -285,38 +357,105 @@ See https://github.com/emacs-lsp/lsp-mode."
285357
(when first-run
286358
(lsp-diagnostics--flymake-update-diagnostics))))
287359

360+
(defun lsp-diagnostics--flymake-calculate-level (severity?)
361+
"Determine SEVERITY mapping, defaulting to error."
362+
363+
(when (stringp severity?)
364+
(setq severity? (string-to-number severity?)))
365+
366+
(pcase severity?
367+
((pred null) :error)
368+
((pred (= lsp/diagnostic-severity-error)) :error)
369+
((pred (= lsp/diagnostic-severity-warning)) :warning)
370+
((pred (= lsp/diagnostic-severity-information)) :note)
371+
((pred (= lsp/diagnostic-severity-hint)) :note)
372+
(_ :error)))
373+
374+
(lsp-defun lsp-diagnostics--flymake-range-to-region
375+
((range &as &Range
376+
:start (start &as &Position
377+
:line start-line
378+
:character start-column)
379+
:end (end &as &Position
380+
:line end-line
381+
:character end-column))
382+
external)
383+
"Determine diagnostic region from RANGE."
384+
(let ((start-line (1+ start-line))
385+
(end-line (1+ end-line)))
386+
(if external
387+
(cons (cons start-line start-column)
388+
(cons end-line end-column))
389+
(if (lsp--position-equal start end)
390+
(if-let ((region (flymake-diag-region (current-buffer)
391+
start-line
392+
start-column)))
393+
(cons (car region) (cdr region))
394+
(lsp-save-restriction-and-excursion
395+
(goto-char (point-min))
396+
(cons (line-beginning-position start-line)
397+
(line-end-position end-line))))
398+
(lsp--range-to-region range)))))
399+
400+
(defun lsp-diagnostics--flymake-message (message code? source?)
401+
"Construct diagnostic message with MESSAGE, CODE and SOURCE."
402+
(let* ((code (and code? (format " [%s]" code?)))
403+
(source (and source? (format " (%s)" source?))))
404+
(concat message code source)))
405+
288406
(defun lsp-diagnostics--flymake-update-diagnostics ()
289407
"Report new diagnostics to flymake."
290-
(funcall lsp-diagnostics--flymake-report-fn
291-
(-some->> (lsp-diagnostics t)
292-
(gethash (lsp--fix-path-casing buffer-file-name))
293-
(--map (-let* (((&Diagnostic :message :severity?
294-
:range (range &as &Range
295-
:start (&Position :line start-line :character)
296-
:end (&Position :line end-line))) it)
297-
((start . end) (lsp--range-to-region range)))
298-
(when (= start end)
299-
(if-let ((region (flymake-diag-region (current-buffer)
300-
(1+ start-line)
301-
character)))
302-
(setq start (car region)
303-
end (cdr region))
304-
(lsp-save-restriction-and-excursion
305-
(goto-char (point-min))
306-
(setq start (line-beginning-position (1+ start-line))
307-
end (line-end-position (1+ end-line))))))
308-
(flymake-make-diagnostic (current-buffer)
309-
start
310-
end
311-
(cl-case severity?
312-
(1 :error)
313-
(2 :warning)
314-
(t :note))
315-
message))))
316-
;; This :region keyword forces flymake to delete old diagnostics in
317-
;; case the buffer hasn't changed since the last call to the report
318-
;; function. See https://github.com/joaotavora/eglot/issues/159
319-
:region (cons (point-min) (point-max))))
408+
(let ((foreign-diagnostics (ht-create))
409+
(domestic-diagnostics nil)
410+
(path (lsp--fix-path-casing buffer-file-name)))
411+
412+
;; Remove any "foreign" diagnostics which may have existed prior
413+
;; to this buffer having been loaded.
414+
(setq flymake-list-only-diagnostics
415+
(assoc-delete-all path flymake-list-only-diagnostics))
416+
417+
(seq-doseq (diagnostic (lsp--get-buffer-diagnostics))
418+
(-let* (((&Diagnostic :message :severity? :code?
419+
:source? :related-information?
420+
:range) diagnostic)
421+
(level (lsp-diagnostics--flymake-calculate-level severity?))
422+
(message (lsp-diagnostics--flymake-message message code? source?)))
423+
(-let (((start . end)
424+
(lsp-diagnostics--flymake-range-to-region range nil)))
425+
(push
426+
(flymake-make-diagnostic (current-buffer) start end level message)
427+
domestic-diagnostics))
428+
(seq-doseq (related-info related-information?)
429+
(-let* (((&DiagnosticRelatedInformation
430+
:message :location (&Location :range :uri)) related-info)
431+
(related-file (lsp--fix-path-casing (lsp--uri-to-path uri)))
432+
(external (not (equal path related-file)))
433+
((start . end)
434+
(lsp-diagnostics--flymake-range-to-region range external))
435+
(message (lsp-diagnostics--flymake-message message code? source?)))
436+
(if external
437+
(push
438+
(flymake-make-diagnostic related-file start end level message)
439+
(gethash related-file foreign-diagnostics))
440+
(push
441+
(flymake-make-diagnostic (current-buffer) start end level message)
442+
domestic-diagnostics))))))
443+
444+
;; Refresh foreign diagnostics
445+
(maphash
446+
(lambda (foreign-file diagnostics)
447+
(setq flymake-list-only-diagnostics
448+
(assoc-delete-all foreign-file flymake-list-only-diagnostics))
449+
(push (cons foreign-file diagnostics) flymake-list-only-diagnostics))
450+
foreign-diagnostics)
451+
452+
;; Refresh domestic diagnostics
453+
(funcall lsp-diagnostics--flymake-report-fn
454+
domestic-diagnostics
455+
;; This :region keyword forces flymake to delete old diagnostics in
456+
;; case the buffer hasn't changed since the last call to the report
457+
;; function. See https://github.com/joaotavora/eglot/issues/159
458+
:region (cons (point-min) (point-max)))))
320459

321460

322461

0 commit comments

Comments
 (0)