unofficial mirror of emacs-devel@gnu.org 
 help / color / mirror / code / Atom feed
* Adding fix suggestions to Flymake diagnostics
@ 2024-05-26 13:37 Eshel Yaron
  2024-05-26 14:38 ` João Távora
  0 siblings, 1 reply; 11+ messages in thread
From: Eshel Yaron @ 2024-05-26 13:37 UTC (permalink / raw)
  To: Spencer Baugh, João Távora; +Cc: emacs-devel

[-- Attachment #1: Type: text/plain, Size: 1058 bytes --]

Hi Spencer, João, and all,

I've been playing around a bit with adding fix suggestions to Flymake.
The idea is to provide backends with a standard API for associating fix
suggestions with diagnostics, and to provide users with a consistent UI
for applying such suggestions.

Some backends already use various tricks to suggest fixes in one way or
another.  For example, Eglot associates LSP "quickfix" actions with some
diagnostics.  What do you think about standardizing this concept?

I'm attaching a patch with an initial implementation that extends the
Flymake API and adapts Eglot to provide fix suggestions in this manner.
This lets you apply fix suggestions via context-menu-mode right-click
menus, and via a new command flymake-fix that looks for a fix for the
diagnostic at point.  So far I tested it with clangd and gopls.

Note that this patch uses the function refactor-apply-edits from my
refactor.el library (attached as well) in order to apply the fix
suggestions, so you'll need that too if you want to give it a spin.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-fix-suggestions-to-Flymake-diagnostics.patch --]
[-- Type: text/x-patch, Size: 10258 bytes --]

From b68c6013693a163462ba1ff49494bfbe68d3eb6c Mon Sep 17 00:00:00 2001
From: Eshel Yaron <me@eshelyaron.com>
Date: Sun, 26 May 2024 14:41:03 +0200
Subject: [PATCH] Add fix suggestions to Flymake diagnostics

---
 lisp/progmodes/eglot.el   | 81 ++++++++++++++++++++++++++++++---------
 lisp/progmodes/flymake.el | 54 ++++++++++++++++++++++++--
 2 files changed, 112 insertions(+), 23 deletions(-)

diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el
index bb915fb4a91..4daa1f213de 100644
--- a/lisp/progmodes/eglot.el
+++ b/lisp/progmodes/eglot.el
@@ -2382,6 +2382,65 @@ eglot--TextDocumentIdentifier-cache
 server on textDocument/didOpen and similar calls.  TRUENAME is the
 expensive cached value of `file-truename'.")
 
+(defun eglot--flymake-fix (data)
+  "Return fix suggestions for Flymake diagnostic with DATA."
+  (eglot--dbind ((Diagnostic) range) (alist-get 'eglot-lsp-diag data)
+    (pcase-let ((`(,beg . ,end) (eglot--diag-range-region range)))
+      (let ((actions (eglot-code-actions beg end "quickfix")))
+        (seq-keep
+         (eglot--lambda ((CodeAction) title edit)
+           (eglot--dbind
+               ((WorkspaceEdit) changes documentChanges)
+               edit
+             (let ((prepared
+                    (mapcar
+                     (eglot--lambda ((TextDocumentEdit) textDocument edits)
+                       (eglot--dbind ((VersionedTextDocumentIdentifier)
+                                      uri version)
+                           textDocument
+                         (list (eglot-uri-to-path uri) edits version)))
+                     documentChanges)))
+               (unless (and changes documentChanges)
+                 (cl-loop for (uri edits) on changes by #'cddr
+                          do (push (list (eglot-uri-to-path uri) edits)
+                                   prepared)))
+               (when prepared
+                 (list title
+                       (mapcar
+                        (pcase-lambda (`(,file ,edits . ,_))
+                          (let ((buf (find-file-noselect file)))
+                            (cons buf
+                                  (seq-map (eglot--lambda ((TextEdit)
+                                                           range newText)
+                                             (pcase (with-current-buffer buf
+                                                      (eglot-range-region range))
+                                               (`(,beg . ,end)
+                                                (list beg end newText))))
+                                           edits))))
+                        prepared))))))
+         actions)))))
+
+(defun eglot--diag-range-region (range)
+  (pcase-let ((`(,beg . ,end) (eglot-range-region range)))
+    ;; Fallback to `flymake-diag-region' if server
+    ;; botched the range
+    (when (= beg end)
+      (if-let* ((st (plist-get range :start))
+                (diag-region
+                 (flymake-diag-region
+                  (current-buffer) (1+ (plist-get st :line))
+                  (plist-get st :character))))
+          (setq beg (car diag-region) end (cdr diag-region))
+        (eglot--widening
+         (goto-char (point-min))
+         (setq beg
+               (eglot--bol
+                (1+ (plist-get (plist-get range :start) :line))))
+         (setq end
+               (line-end-position
+                (1+ (plist-get (plist-get range :end) :line)))))))
+    (cons beg end)))
+
 (cl-defmethod eglot-handle-notification
   (server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics
            &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode'
@@ -2413,24 +2472,7 @@ eglot-handle-notification
                        diag-spec
                      (setq message (mess source code message))
                      (pcase-let
-                         ((`(,beg . ,end) (eglot-range-region range)))
-                       ;; Fallback to `flymake-diag-region' if server
-                       ;; botched the range
-                       (when (= beg end)
-                         (if-let* ((st (plist-get range :start))
-                                   (diag-region
-                                    (flymake-diag-region
-                                     (current-buffer) (1+ (plist-get st :line))
-                                     (plist-get st :character))))
-                             (setq beg (car diag-region) end (cdr diag-region))
-                           (eglot--widening
-                            (goto-char (point-min))
-                            (setq beg
-                                  (eglot--bol
-                                   (1+ (plist-get (plist-get range :start) :line))))
-                            (setq end
-                                  (line-end-position
-                                   (1+ (plist-get (plist-get range :end) :line)))))))
+                         ((`(,beg . ,end) (eglot--diag-range-region range)))
                        (eglot--make-diag
                         (current-buffer) beg end
                         (eglot--diag-type severity)
@@ -2439,7 +2481,8 @@ eglot-handle-notification
                                     (cl-loop for tag across tags
                                              when (alist-get tag eglot--tag-faces)
                                              collect it)))
-                          `((face . ,faces))))))
+                          `((face . ,faces)))
+                        #'eglot--flymake-fix)))
            into diags
            finally (cond ((and
                            ;; only add to current report if Flymake
diff --git a/lisp/progmodes/flymake.el b/lisp/progmodes/flymake.el
index 2e602658ea7..edf9e95063b 100644
--- a/lisp/progmodes/flymake.el
+++ b/lisp/progmodes/flymake.el
@@ -368,7 +368,7 @@ flymake-error
   locus beg end type text 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)
+  orig-beg orig-end fix-function)
 
 ;;;###autoload
 (defun flymake-make-diagnostic (locus
@@ -377,7 +377,8 @@ flymake-make-diagnostic
                                 type
                                 text
                                 &optional data
-                                overlay-properties)
+                                overlay-properties
+                                fix-function)
   "Make a Flymake diagnostic for LOCUS's region from BEG to END.
 LOCUS is a buffer object or a string designating a file name.
 
@@ -396,14 +397,24 @@ flymake-make-diagnostic
 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."
+the diagnostic's type symbol.
+
+FIX-FUNCTION, if non-nil, is a function that takes DATA and returns a
+list of fix suggestions for this diagnostic.  Each fix suggestion is a
+list (TITLE EDITS), where TITLE is a string describing the fix and EDITS
+is a list of (FILE-OR-BUFFER . CHANGES) cons cells, where FILE-OR-BUFFER
+is the file name or buffer to edit, and CHANGES is a list of changes to
+perform in FILE-OR-BUFFER.  Each element of CHANGES is in turn a
+list (BEG END STR), where BEG and END are buffer positions to delete and
+STR is the string to insert at BEG afterwards."
   (when (stringp locus)
     (setq locus (expand-file-name locus)))
   (flymake--diag-make :locus locus :beg beg :end end
                       :type type :text text :data data
                       :overlay-properties overlay-properties
                       :orig-beg beg
-                      :orig-end end))
+                      :orig-end end
+                      :fix-function fix-function))
 
 ;;;###autoload
 (defun flymake-diagnostics (&optional beg end)
@@ -849,6 +860,40 @@ flymake--update-eol-overlays
             (overlay-put o 'before-string (flymake--eol-overlay-summary src-ovs))
           (delete-overlay o))))))
 
+(defun flymake-diagnostic-context-menu (menu click)
+  "Extend MENU with fix suggestions for diagnostic at CLICK."
+  (when-let ((diag (mouse-posn-property (event-start click)
+                                        'flymake-diagnostic))
+             (fix-fun (flymake--diag-fix-function diag))
+             (fixes (funcall fix-fun (flymake--diag-data diag)))
+             (i 1))
+    (dolist (fix fixes)
+      (define-key menu (vector (intern (format "flymake-fix-%d" i)))
+                  `(menu-item ,(format "Fix: %s" (car fix))
+                              ,(lambda ()
+                                 (interactive)
+                                 (refactor-apply-edits (cadr fix)))
+                              ,@(cddr fix)))
+      (cl-incf i)))
+  menu)
+
+(defun flymake-fix (pos)
+  "Fix Flymake diagnostic at POS."
+  (interactive "d")
+  ;; TODO - fix _all_ diagnostics at point.
+  (if-let ((diag (car (flymake-diagnostics pos))))
+      (if-let ((fix-fun (flymake--diag-fix-function diag))
+               (fixes (funcall fix-fun (flymake--diag-data diag))))
+          (refactor-apply-edits
+           (car (if (cdr fixes)
+                    (alist-get
+                     (completing-read (format-prompt "Fix" (caar fixes))
+                                      fixes nil t nil nil (caar fixes))
+                     fixes nil nil #'string=)
+                  (cdar fixes))))
+        (message "No fix available for this diagnostic"))
+    (user-error "No diagnostic at this position")))
+
 (cl-defun flymake--highlight-line (diagnostic &optional foreign)
   "Attempt to overlay DIAGNOSTIC in current buffer.
 
@@ -956,6 +1001,7 @@ flymake--highlight-line
              (flymake-diagnostics pos)
              "\n"))))
       (default-maybe 'severity (warning-numeric-level :error))
+      (default-maybe 'context-menu-functions '(flymake-diagnostic-context-menu))
       ;; Use (PRIMARY . SECONDARY) priority, to avoid clashing with
       ;; `region' face, for example (bug#34022).
       (default-maybe 'priority (cons nil (+ 40 (overlay-get ov 'severity)))))
-- 
2.45.0


[-- Attachment #3: refactor.el --]
[-- Type: application/emacs-lisp, Size: 10547 bytes --]

^ permalink raw reply related	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2024-06-03 18:10 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-05-26 13:37 Adding fix suggestions to Flymake diagnostics Eshel Yaron
2024-05-26 14:38 ` João Távora
2024-05-26 15:56   ` Eshel Yaron
2024-05-26 20:24     ` João Távora
2024-05-27 12:15       ` Eshel Yaron
2024-05-27 14:02         ` João Távora
2024-05-27 17:40           ` Eshel Yaron
2024-05-27 22:03             ` João Távora
2024-05-31  7:15               ` Eshel Yaron
2024-05-31  8:55                 ` João Távora
2024-06-03 18:10                   ` Eshel Yaron

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).