Subject: Re: bug#77439: [PATCH] Eglot: introduce eglot-show-diagnostics-source

From: João Távora <[email-masked]>
Date: Fri, 18 Apr 2025 20:03:34 +0100

Nicolás Ojeda Bär <[email-masked]> writes:

>
> OK, makes sense. Give me a few days to chew on this and I'll get back here
> once I have something. Thanks.

Heads up, I've started working on this change and have it mostly
finished (some details in'`flymake-show-diagnostics-buffer' and
documentation are missing).

In the patch, the new Flymake version introduces a more powerful
'flymake-make-diagnostic'.  In the same patch there is a change to Eglot
to take advantage of this.

The new proposed customization option is
'flymake-diagnostic-format-alist'.  To omit the LSP diagnostic source
and the code the entries :eldoc-echo and :help-echo in this variable can
be modified.

   (setf (alist-get :eldoc-echo flymake-diagnostic-format-alist)
         '(oneliner)
         (alist-get :help-echo flymake-diagnostic-format-alist)
         '(oneliner))

Spencer, please say if you have any objections, else I will push in a
few days time (after I address some minor shortcomings).

The current patch is after my sig.

João

diff --git a/doc/misc/flymake.texi b/doc/misc/flymake.texi
index 668a72b4cd1..0e43df17aa2 100644
--- a/doc/misc/flymake.texi
+++ b/doc/misc/flymake.texi
@@ -62,7 +62,7 @@ Top
 When the Emacs LSP support mode Eglot is enabled, Flymake will use that
 as an additional back-end.  @xref{Eglot Features,,, eglot, Eglot: The
 Emacs LSP Client}.  Flymake is also designed to be easily extended to
-support new backends via an Elisp interface.  @xref{Extending Flymake}.
+support new backends via an Elisp interface.  @xref{Flymake API}.
 
 @ifnottex
 @insertcopying
@@ -70,7 +70,7 @@ Top
 
 @menu
 * Using Flymake::
-* Extending Flymake::
+* Flymake API::
 * The legacy Proc backend::
 * GNU Free Documentation License::
 * Index::
@@ -210,7 +210,7 @@ Mode line status
 @item @code{?}
 @tab There are no applicable Flymake backends for this buffer, thus Flymake
 cannot annotate it.  To fix this, a user may look to extending Flymake
-and add a new backend (@pxref{Extending Flymake}).
+and add a new backend (@pxref{Flymake API}).
 
 @end multitable
 
@@ -361,19 +361,23 @@ Customizable variables
 A custom face for summarizing diagnostic notes.
 @end vtable
 
-@node Extending Flymake
-@chapter Extending Flymake
+@node Flymake API, The legacy Proc backend, Using Flymake, Top
+@chapter Flymake API
 @cindex extending flymake
 
-Flymake can primarily be extended in one of two ways:
+Flymake's API supports the following use cases:
 
 @enumerate
 @item
-By changing the look and feel of the annotations produced by the
+Changing the look and feel of the annotations produced by the
 different backends.  @xref{Flymake error types}.
 
 @item
-By adding a new buffer-checking backend.  @xref{Backend functions}.
+Adding a new buffer-checking backend.  @xref{Backend functions}.
+
+@item
+Writing extension to and process Flymake's output. @xref{Inspecting
+diagnostics}.
 @end enumerate
 
 The following sections discuss each approach in detail.
@@ -381,6 +385,7 @@ Extending Flymake
 @menu
 * Flymake error types::
 * Backend functions::
+* Inspecting diagnostics::
 @end menu
 
 @node Flymake error types
@@ -673,37 +678,19 @@ Flymake utility functions
 Before delivering them to Flymake, backends create diagnostic objects
 by calling the function @code{flymake-make-diagnostic}.
 
-@deffn Function flymake-make-diagnostic locus beg end type text &optional data
+@deffn Function flymake-make-diagnostic locus beg end type info &optional data
 Make a Flymake diagnostic for the region of text in @var{locus}'s
-delimited by @var{beg} and @var{end}.  @var{type} is a diagnostic
-symbol (@pxref{Flymake error types}), and @var{text} is a description
-of the problem detected in this region.  Most commonly @var{locus} is
-the buffer object designating for the current buffer being
-syntax-checked.  However, it may be a string naming a file relative
-to the current working directory.  @xref{Foreign and list-only
-diagnostics}, for when this may be useful.  Depending on the type of
-@var{locus}, @var{beg} and @var{end} are both either buffer positions
-or conses (@var{line} . @var{col}) which specify the line and column
-of the diagnostic's start and end positions, respectively.
-@end deffn
-
-@cindex access diagnostic object
-These objects' properties can be accessed with the functions
-@code{flymake-diagnostic-backend}, @code{flymake-diagnostic-buffer},
-@code{flymake-diagnostic-text}, @code{flymake-diagnostic-beg},
-@code{flymake-diagnostic-end}, @code{flymake-diagnostic-type} and
-@code{flymake-diagnostic-data}.
-
-Additionally, the function @code{flymake-diagnostics} will collect
-such objects in the region you specify.
-
-@cindex collect diagnostic objects
-@deffn Function flymake-diagnostics beg end
-Get a list of Flymake diagnostics in the region determined by
-@var{beg} and @var{end}.  If neither @var{beg} or @var{end} is
-supplied, use the whole buffer, otherwise if @var{beg} is
-non-@code{nil} and @var{end} is @code{nil}, consider only diagnostics
-at @var{beg}.
+delimited by @var{beg} and @var{end}.  @var{type} is a diagnostic symbol
+(@pxref{Flymake error types}).  @var{text} can be a string or a list
+(@var{origin} @var{code} @var{message}) appropriately categorizing and
+describing the diagnostic.  Most commonly, @var{locus} is the buffer
+object designating for the current buffer being syntax-checked.
+However, it may be a string naming a file relative to the current
+working directory.  @xref{Foreign and list-only diagnostics}, for when
+this may be useful.  Depending on the type of @var{locus}, @var{beg} and
+@var{end} are both either buffer positions or conses (@var{line}
+. @var{col}) which specify the line and column of the diagnostic's start
+and end positions, respectively.
 @end deffn
 
 @cindex buffer position from line and column number
@@ -900,6 +887,32 @@ An annotated example backend
 @end group
 @end example
 
+@node Inspecting diagnostics
+@section Inspecting diagnostics
+
+When Flymake has called on the backend and collected its diagnostics, it
+will annotate the buffer with it.  After this happens, Elisp programs
+may call @code{flymake-diagnostics} to collect such objects in a
+specified region.
+
+@cindex collect diagnostic objects
+@deffn Function flymake-diagnostics beg end
+Get a list of Flymake diagnostics in the region determined by
+@var{beg} and @var{end}.  If neither @var{beg} or @var{end} is
+supplied, use the whole buffer, otherwise if @var{beg} is
+non-@code{nil} and @var{end} is @code{nil}, consider only diagnostics
+at @var{beg}.
+@end deffn
+
+@cindex access diagnostic object
+A diagnostic object's properties can be accessed with the functions
+@code{flymake-diagnostic-backend}, @code{flymake-diagnostic-buffer},
+@code{flymake-diagnostic-origin}, @code{flymake-diagnostic-code}
+@code{flymake-diagnostic-message}, @code{flymake-diagnostic-beg},
+@code{flymake-diagnostic-end}, @code{flymake-diagnostic-type} and
+@code{flymake-diagnostic-data}.  @code{flymake-diagnostic-text} will
+compose the diagnostic's origin, code and message in a single string.
+
 @node The legacy Proc backend
 @chapter The legacy ``Proc'' backend
 @cindex legacy proc backend
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index b077f4a6207..216bcfc0d75 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -7,7 +7,7 @@
 ;; Maintainer: João Távora <[email-masked]>
 ;; URL: https://github.com/joaotavora/eglot
 ;; Keywords: convenience, languages
-;; Package-Requires: ((emacs "26.3") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.2.1") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (xref "1.6.2"))
+;; Package-Requires: ((emacs "26.3") (eldoc "1.14.0") (external-completion "0.1") (flymake "1.4.0") (jsonrpc "1.0.24") (project "0.9.8") (seq "2.23") (xref "1.6.2"))
 
 ;; This is a GNU ELPA :core package.  Avoid adding functionality
 ;; that is not available in the version of Emacs recorded above or any
@@ -2684,8 +2684,6 @@ eglot-handle-notification
                     ((<= sev 1) 'eglot-error)
                     ((= sev 2)  'eglot-warning)
                     (t          'eglot-note)))
-            (mess (source code message)
-              (concat source (and code (format " [%s]" code)) ": " message))
             (find-it (abspath)
               ;; `find-buffer-visiting' would be natural, but calls the
               ;; potentially slow `file-truename' (bug#70036).
@@ -2706,7 +2704,6 @@ eglot-handle-notification
            for diag-spec across diagnostics
            collect (eglot--dbind ((Diagnostic) range code message severity source tags)
                        diag-spec
-                     (setq message (mess source code message))
                      (pcase-let
                          ((`(,beg . ,end) (eglot-range-region range)))
                        ;; Fallback to `flymake-diag-region' if server
@@ -2729,8 +2726,9 @@ eglot-handle-notification
                        (eglot--make-diag
                         (current-buffer) beg end
                         (eglot--diag-type severity)
-                        message `((eglot-lsp-diag . ,diag-spec)
-                                  (eglot--doc-version . ,version))
+                        (list source code message)
+                        `((eglot-lsp-diag . ,diag-spec)
+                          (eglot--doc-version . ,version))
                         (when-let* ((faces
                                      (cl-loop for tag across tags
                                               when (alist-get tag eglot--tag-faces)
diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el
index 7340fed9be4..c03dc55ecf2 100644
--- a/lisp/progmodes/flymake.el
+++ b/lisp/progmodes/flymake.el
@@ -4,7 +4,7 @@
 
 ;; Author: Pavel Kobyakov <[email-masked]>
 ;; Maintainer: Spencer Baugh <[email-masked]>
-;; Version: 1.3.7
+;; Version: 1.4.0
 ;; Keywords: c languages tools
 ;; Package-Requires: ((emacs "26.1") (eldoc "1.14.0") (project "0.7.1"))
 
@@ -371,7 +371,7 @@ flymake-error
 
 (cl-defstruct (flymake--diag
                (:constructor flymake--diag-make))
-  locus beg end type text backend data overlay-properties overlay
+  locus beg end type origin code message backend data overlay-properties overlay
   ;; FIXME: See usage of these two in `flymake--highlight-line'.
   ;; Ideally they wouldn't be needed.
   orig-beg orig-end)
@@ -381,32 +381,39 @@ flymake-make-diagnostic
                                 beg
                                 end
                                 type
-                                text
+                                info
                                 &optional data
                                 overlay-properties)
   "Make a Flymake diagnostic for LOCUS's region from BEG to END.
 LOCUS is a buffer object or a string designating a file name.
 
-TYPE is a diagnostic symbol and TEXT is string describing the
-problem detected in this region.  DATA is any object that the
-caller wishes to attach to the created diagnostic for later
-retrieval with `flymake-diagnostic-data'.
-
-If LOCUS is a buffer BEG and END should be buffer positions
-inside it.  If LOCUS designates a file, BEG and END should be a
-cons (LINE . COL) indicating a file position.  In this second
-case, END may be omitted in which case the region is computed
-using `flymake-diag-region' if the diagnostic is appended to an
-actual buffer.
-
-OVERLAY-PROPERTIES is an alist of properties attached to the
-created diagnostic, overriding the default properties and any
-properties listed in the `flymake-overlay-control' property of
-the diagnostic's type symbol."
+TYPE is a diagnostic symbol (see Info Node `(Flymake)Flymake error
+types')
+
+INFO is a description of the problem detected.  It may be a string, or
+list of three strings (ORIGIN CODE MESSAGE) appropriately categorizing
+and describing the diagnostic.
+
+DATA is any object that the caller wishes to attach to the created
+diagnostic for later retrieval with `flymake-diagnostic-data'.
+
+If LOCUS is a buffer, BEG and END should be buffer positions inside it.
+If LOCUS designates a file, BEG and END should be a cons (LINE . COL)
+indicating a file position.  In this second case, END may be omitted in
+which case the region is computed using `flymake-diag-region' if the
+diagnostic is appended to an actual buffer.
+
+OVERLAY-PROPERTIES is an alist of properties attached to the created
+diagnostic, overriding the default properties and any properties listed
+in the `flymake-overlay-control' property of the diagnostic's type
+symbol."
   (when (stringp locus)
     (setq locus (expand-file-name locus)))
+  (when (stringp info)
+    (setq info (list nil nil info)))
   (flymake--diag-make :locus locus :beg beg :end end
-                      :type type :text text :data data
+                      :type type :origin (car info) :code (cadr info)
+                      :message (caddr info) :data data
                       :overlay-properties overlay-properties
                       :orig-beg beg
                       :orig-end end))
@@ -432,27 +439,90 @@ flymake--diag-accessor
      ,(format "Get Flymake diagnostic DIAG's %s." (symbol-name thing))
      (,internal diag)))
 
-(flymake--diag-accessor flymake-diagnostic-text flymake--diag-text text)
 (flymake--diag-accessor flymake-diagnostic-type flymake--diag-type type)
 (flymake--diag-accessor flymake-diagnostic-backend flymake--diag-backend backend)
+(flymake--diag-accessor flymake-diagnostic-origin flymake--diag-origin backend)
+(flymake--diag-accessor flymake-diagnostic-code flymake--diag-code backend)
+(flymake--diag-accessor flymake-diagnostic-message flymake--diag-message backend)
 (flymake--diag-accessor flymake-diagnostic-data flymake--diag-data data)
 (flymake--diag-accessor flymake-diagnostic-beg flymake--diag-beg beg)
 (flymake--diag-accessor flymake-diagnostic-end flymake--diag-end end)
 (flymake--diag-accessor flymake-diagnostic-buffer flymake--diag-locus locus)
 
+(defcustom flymake-diagnostic-format-alist
+  '((:help-echo . (origin code oneliner))
+    (:eol . (oneliner))
+    (:eldoc . (origin code message))
+    (:eldoc-echo . (origin code oneliner))
+    (t . (origin code oneliner)))
+  "How to format diagnostics for different Flymake outlets.
+Value is an alist where each element looks like (OUTLET . PARTS).
+OUTLET is a symbol designating an outlet.  One of:
+
+- `:help-echo', for the native Flymake echoing of diagnostics in the
+   echo area as used my `flymake-goto-next-error' and `flymake-goto-prev-error';
+- `:eol', for use with `flymake-show-diagnostics-at-end-of-line';
+- `:eldoc', for use with Flymake's ElDoc backend;
+- `:eldoc-echo', for use with Flymake's ElDoc backend, but for ElDoc's own
+   confined outlets;
+- t for the default outlet.
+
+PARTS says which parts of the diagnostic to include.  It is a list of
+symbols where the following values are meaningful:
+
+- `origin': include diagnostic origin if it exists;
+- `code': include diagnostics code if it exists;
+- `message': include the full diagnostic's message text;
+- `oneliner': include truncated diagnostic text;"
+  :package-version '(Flymake . "1.4.0")
+  :type 'alist)
+
+(cl-defun flymake-diagnostic-text (diag
+                                   &optional (parts '(origin code message)))
+  "Describe diagnostic DIAG's as a string.
+PARTS says which parts of the diagnostic to include.  It is a list of
+symbols as described in `flymake-diagnostic-format-alist' (which see).
+PARTS defaults to `(origin code message)'."
+  (let* ((w parts)
+         (a (and (memq 'origin w) (flymake--diag-origin diag)))
+         (b (and (memq 'code w) (flymake--diag-code diag)))
+         (c (cond ((memq 'message w) (flymake--diag-message diag))
+                  ((memq 'oneliner w)
+                   (let* ((msg (flymake--diag-message diag)))
+                     (substring msg 0 (cl-loop for i from 0 for a across msg
+                                               when (eq a ?\n) return i)))))))
+    (concat a
+            (when (and a b) " ")
+            (when b (concat "[" b "]"))
+            (when (or a b) ": ")
+            c)))
+
+(defun flymake--format-diagnostic (diag outlet face-prop)
+  (let ((txt (flymake-diagnostic-text
+              diag (alist-get outlet flymake-diagnostic-format-alist
+                              (alist-get t flymake-diagnostic-format-alist
+                                         '(origin code message))))))
+    (if face-prop
+        (propertize txt 'face
+                    (flymake--lookup-type-property
+                     (flymake-diagnostic-type diag) face-prop
+                     'flymake-error))
+      txt)))
+
 (defun flymake-diagnostic-oneliner (diag &optional nopaintp)
   "Get truncated one-line text string for diagnostic DIAG.
 This is useful for displaying the DIAG's text to the user in
 confined spaces, such as the echo are.  Unless NOPAINTP is t,
 propertize returned text with the `echo-face' property of DIAG's
 type."
-  (let* ((txt (flymake-diagnostic-text diag))
-         (txt (substring txt 0 (cl-loop for i from 0 for a across txt
-                                        when (eq a ?\n) return i))))
+  (let* ((txt (flymake-diagnostic-text diag '(origin code oneliner))))
     (if nopaintp txt
       (propertize txt 'face
                   (flymake--lookup-type-property
                    (flymake-diagnostic-type diag) 'echo-face 'flymake-error)))))
+(make-obsolete 'flymake-diagnostic-oneliner
+               "use `flymake-diagnostic-text' instead."
+               "Flymake package version 1.4.0")
 
 (cl-defun flymake--really-all-overlays ()
   "Get flymake-related overlays.
@@ -813,7 +883,9 @@ flymake--equal-diagnostic-p
                              flymake--diag-beg
                              flymake-diagnostic-type
                              flymake-diagnostic-backend
-                             flymake-diagnostic-text)
+                             flymake-diagnostic-origin
+                             flymake-diagnostic-code
+                             flymake-diagnostic-message)
                always (equal (funcall comp a) (funcall comp b)))))
 
 (defun flymake--delete-overlay (ov)
@@ -827,9 +899,7 @@ flymake--delete-overlay
 (defun flymake--eol-overlay-summary (src-ovs)
   "Helper function for `flymake--update-eol-overlays'."
   (cl-flet ((summarize (d)
-              (propertize (flymake-diagnostic-oneliner d t) 'face
-                          (flymake--lookup-type-property (flymake--diag-type d)
-                                                         'eol-face))))
+              (flymake--format-diagnostic d :eol 'eol-face)))
     (let* ((diags
             (cl-sort
              (mapcar (lambda (o) (overlay-get o 'flymake-diagnostic)) src-ovs)
@@ -956,7 +1026,8 @@ flymake--highlight-line
         (lambda (window _ov pos)
           (with-selected-window window
             (mapconcat
-             #'flymake-diagnostic-oneliner
+             (lambda (d)
+               (flymake--format-diagnostic d :help-echo 'echo-face))
              (flymake-diagnostics pos)
              "\n"))))
       (default-maybe 'severity (warning-numeric-level :error))
@@ -1562,9 +1633,13 @@ flymake-eldoc-function
 Intended for `eldoc-documentation-functions' (which see)."
   (when-let* ((diags (flymake-diagnostics (point))))
     (funcall report-doc
-             (mapconcat #'flymake-diagnostic-text diags "\n")
-             :echo (mapconcat #'flymake-diagnostic-oneliner
-                              diags "\n"))))
+             (mapconcat (lambda (d)
+                          (flymake--format-diagnostic d :eldoc 'echo-face))
+                        diags "\n")
+             :echo (mapconcat
+                    (lambda (d)
+                      (flymake--format-diagnostic d :eldoc-echo 'echo-face))
+                    diags "\n"))))
 
 (defun flymake-goto-next-error (&optional n filter interactive)
   "Go to Nth next Flymake diagnostic that matches FILTER.
@@ -1922,6 +1997,16 @@ flymake-goto-diagnostic
   (pop-to-buffer
    (flymake-show-diagnostic (if (button-type pos) (button-start pos) pos))))
 
+(defun flymake--tabulated-diagnostic-origin (diag)
+  (or (flymake-diagnostic-origin diag)
+      (let* ((backend (flymake-diagnostic-backend diag))
+             (bname (or (ignore-errors (symbol-name backend))
+                        "(anonymous function)")))
+        (propertize
+         (replace-regexp-in-string "\\(.\\)[^-]+\\(-\\|$\\)"
+                                   "\\1\\2" bname)
+         'help-echo (format "From `%s' backend" backend)))))
+
 (defun flymake--tabulated-entries-1 (diags project-root)
   "Helper for `flymake--diagnostics-buffer-entries'.
 PROJECT-ROOT indicates that each entry should be preceded by the
@@ -1951,9 +2036,7 @@ flymake--tabulated-entries-1
          (;; somehow dead annotated diagnostic, ignore/give up
           t nil))
    for type = (flymake-diagnostic-type diag)
-   for backend = (flymake-diagnostic-backend diag)
-   for bname = (or (ignore-errors (symbol-name backend))
-                   "(anonymous function)")
+   for origin = (flymake--tabulated-diagnostic-origin diag)
    for data-vec = `[,(format "%s" line)
                     ,(format "%s" col)
                     ,(propertize (format "%s"
@@ -1961,13 +2044,8 @@ flymake--tabulated-entries-1
                                           type 'flymake-type-name type))
                                  'face (flymake--lookup-type-property
                                         type 'mode-line-face 'flymake-error))
-                    ,(propertize
-                      (if bname
-                          (replace-regexp-in-string "\\(.\\)[^-]+\\(-\\|$\\)"
-                                                    "\\1\\2" bname)
-                        "(anon)")
-                      'help-echo (format "From `%s' backend" backend))
-                    (,(flymake-diagnostic-oneliner diag t)
+                    ,origin
+                    (,(flymake-diagnostic-text diag '(oneliner))
                      mouse-face highlight
                      help-echo "mouse-2: visit this diagnostic"
                      face nil
@@ -2013,7 +2091,7 @@ flymake--diagnostics-base-tabulated-list-format
     ("Type" 8 ,(lambda (l1 l2)
                  (< (plist-get (car l1) :severity)
                     (plist-get (car l2) :severity))))
-    ("Backend" 8 t)
+    ("Origin" 8 t)
     ("Message" 0 t)])
 
 (define-derived-mode flymake-diagnostics-buffer-mode tabulated-list-mode