Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] Remove flymake-mode check from publishDiagnostics handler #596

Closed
gagbo opened this issue Jan 18, 2021 · 54 comments
Closed

[feature] Remove flymake-mode check from publishDiagnostics handler #596

gagbo opened this issue Jan 18, 2021 · 54 comments
Labels

Comments

@gagbo
Copy link

gagbo commented Jan 18, 2021

Since da7ff48, it is a lot harder to use flycheck or any other checker to handle diagnostics when the server sends publishDiagnostics message.

Before that, it was possible, as a client, to only call eglot-flymake-backend once on the buffer to properly set the internal eglot--current-flymake-report-fn and get updates when new diagnostics are received.

It would be amazing to be able to do that again :)

Also discussed (in length) in doomemacs/doomemacs#4471

@joaotavora
Copy link
Owner

This is confirmed. I just have to look at it closely so I don't break the associated problems in #468 and #472

@purcell
Copy link
Contributor

purcell commented Mar 28, 2021

I wonder if a good option here might be to explicitly support an eglot-diagnostics-hook var: users could add functions to this hook, and each would be invoked with a list of flymake diagnostics when the diagnostics are received in eglot-handle-notification. The flymake support itself could use this, replacing eglot--current-flymake-report-fn with a hook entry, and then other uses like the flycheck shim would have a clean integration point. The flymake-mode check which solved #468 and #472 would then remain, but only in the flymake-specific hook function.

purcell added a commit to purcell/eglot that referenced this issue Apr 1, 2021
We provide a hook that is called whenever diagnostics are
received, and each hook function will be passed the diagnostics. This
allows clean extension to support flycheck or other mechanisms for
diagnostics display, while maintaining the existing flymake support
intact.

See joaotavora#596
@joaotavora
Copy link
Owner

Hi @purcell , I see you have a new pull request. I haven't looked at it, because I'm so very busy at the moment, sorry. But have you read the linked issue by @gagbo (doomemacs/doomemacs#4471)?

We analysed the problem in depth there. As far as I remember, the problem hinges on this line:

into diags
         finally (cond ((and flymake-mode eglot--current-flymake-report-fn)
                        (save-restriction

If one removes flymake-mode from the and condition, things are fine and no hook is required in principle.

The problem is that removing that symbol causes other bugs. But those bugs are Flymake's Eglot's and should really be taken care of. This is why I marked this issue "BUG". So I will look into your PR, but it feels like we're iterating something for which a simpler solution has already been found, a solution that doesn't require opening any new interfaces to the Eglot system.

That solution just needs to be implemented and tested. It starts with removing the flymake-mode and playing around a bit with Eglot until diagnostics counting goes out of sync. Then fix that 😆 I'm sorry but I'm short on time. But maybe with your Elisp expertise you could help, who knows 😜 : ?

@purcell
Copy link
Contributor

purcell commented Apr 1, 2021

@joaotavora Thanks, I hadn't seen all of the discussion there, and don't have permissions to comment on it.

The approach I've taken in #658 essentially leaves the flymake code as-is, including its handling of the unreported diagnostics and flymake-mode check: the idea is that flycheck support could then be implemented by adding an orthogonal callback to eglot-diagnostics-hook, and that such flycheck support may or may not perform its own handling of unreported diagnostics.

Part of the current issue is that we (Doom, and my hacky code, which I think eventually turned into the Doom code after I published it in a gist and it got passed around) end up abusing eglot-flymake-backend for flycheck, so things are always likely to break in that situation due to mismatches between the two systems, and the (fairly reasonable!) flymake-mode test is just one example of that.

I don't know all the background of the flymake-related bugs here, and obviously there are reasons to want to keep eglot's external API minimal, but I'm not sure that the best route is to fiddle around to keep the current "sneakily hackable" code working: it seems fairly inevitable that it would break again.

/cc @gagbo & @hlissner

@joaotavora
Copy link
Owner

joaotavora commented Apr 1, 2021

abusing eglot-flymake-backend for flycheck

That's the thing, though. I don't actually think you're abusing it. eglot-flymake-backend is a function that adheres, or at least it should adhere, to a set interface (described more or less in the flymake-backend-functions docstring). So as long as it has those well defined properties and respects them faithfully, it should in theory be usable by things other than Glymake. If you have a screw or a cogwheel that's in a certain shape it should be usable in boats, planes, automobiles,etc.

it seems fairly inevitable that it would break again.

why do you suspect this?

@purcell
Copy link
Contributor

purcell commented Apr 1, 2021

I don't actually think you're abusing it.

You could be right. I can certainly imagine that it should be possible to write a general flycheck checker which would use any flymake backend, but at the same time any one of those flymake backends would be perfectly entitled to assume it was being run by flymake and not flycheck.

why do you suspect this?

Just because contributors using flymake shouldn't need to think about whether changes to the *-flymake-* functions break when those functions are hacked for flycheck purposes. Like, on the face of it, adding the flymake-mode test was a completely reasonable thing to do, and it would have been hard to foresee at the time that it might break some wacky hacks for flycheck purposes.

There are a few trade-offs here, so I'm just sharing some thoughts, and I'm more than happy to defer to your judgement! 😄

@joaotavora
Copy link
Owner

I'm experimenting with removing the flymake-mode test there. There are bugs there anyway, like when using an eslint backend together with the regular eglot backend, so I need to investigate them regardless.

There are a few trade-offs here, so I'm just sharing some thoughts, and I'm more than happy to defer to your judgement! smile

Sure. I haven't made up my mind yet. And certainly, I haven't fixed the bug, so it's good to have a PR that offers another, different way out. We may end up doing both and offering two ways to accomplish the same goal (though that is sometimes a footgun). So let's see what I can come up with half an hour of end-of-day semi-wasted brain power.

@joaotavora
Copy link
Owner

joaotavora commented Apr 1, 2021

(wasted but not 🍺 -wasted :-D, just tired)

@purcell
Copy link
Contributor

purcell commented Apr 1, 2021

I should also try sketching out what the flycheck code would look like with my PR and paste it there, since that's probably quite relevant too.

@joaotavora
Copy link
Owner

As you prefer. Pasting it in the other PR issue is also fine.

@purcell
Copy link
Contributor

purcell commented Apr 1, 2021

Pasting it in the other PR issue is also fine.

Yeah, that was my plan. 👍

@joaotavora
Copy link
Owner

Boy am I tired, I really read "paste it here" :-/

@purcell
Copy link
Contributor

purcell commented Apr 1, 2021

Maybe just put your feet up for some well-deserved rest instead!

joaotavora added a commit that referenced this issue Apr 1, 2021
Loosen coupling between eglot-flymake-backend and flymake-mode.  The
flymake-mode check in 'eglot-handle-notification publishDiagnostics'
was a hack (and it wasn't even functioning correctly on M-x
eglot-shutdown/eglot-reconnect).

This should also allow eglot-flymake-backend to be driven by
diagnostic-annotating frontends other than Flymake, such as the
popular Flycheck package.

* eglot.el (eglot--managed-mode): Use eglot--report-to-flymake.
(eglot-handle-notification textDocument/publishDiagnostics): Use
eglot--report-to-flymake.
@joaotavora
Copy link
Owner

Can you give this commit a quick spin?

@purcell
Copy link
Contributor

purcell commented Apr 2, 2021

This commit seems to help, but the end result of the flycheck hackery seems erratic to me: flycheck apparently ends up in a "waiting for callback" state some of the time, and sometimes only a subset of the errors in a buffer show up.

Here's what I think the overall issue is: there are two paths to calling eglot--current-flymake-report-fn, depending on whether there are unreported diagnostics at the time that function is set. This is by design, because report-fn can be called multiple times, as the docstring for eglot-flymake-backend states. But a flycheck callback must only be called exactly once, so it can't simply be wrapped and passed as a report-fn to eglot-flymake-backend: even resetting the callback for every flycheck check cycle, that callback function will be called either once or twice.

So it seems to me that there's a fundamental mismatch between the callback lifecycles of flymake and flycheck here, which makes integrating via eglot-flymake-backend problematic. Flycheck wants to drive the process of producing errors, while flymake receives them passively and continuously.

With my PR, that mismatch can be handled as follows: we translate and save the diagnostics into a "current errors" buffer-local var when they are received. Whenever flycheck decides to run a check, those errors are immediately returned. But of course at some unexpected time, the LSP server might returns some fresh diagnostics, so what we also need to do then is trigger a fresh check, effectively making LSP trigger the flycheck cycle:

      (defvar-local flycheck-eglot--current-errors nil)

      (defun flycheck-eglot--on-diagnostics (diags)
        (setq flycheck-eglot--current-errors
              (mapcar (lambda (diag)
                        (with-current-buffer (flymake--diag-buffer diag)
                          (flycheck-error-new-at-pos
                           (flymake--diag-beg diag)
                           (pcase (flymake--diag-type diag)
                             ('eglot-error 'error)
                             ('eglot-warning 'warning)
                             ('eglot-note 'info)
                             (_ (error "Unknown diag type, %S" diag)))
                           (flymake--diag-text diag)
                           :checker 'eglot
                           :end-pos (flymake--diag-end diag))))
                      diags))
        ;; The LSP sent us new diagnostics, so we trigger flycheck such that it
        ;; will pick them up
        (run-at-time nil nil 'flycheck-buffer))

      (defun flycheck-eglot--start (checker callback)
        ;; Ensure we'll receive diagnostics. This only needs doing once, but
        ;; it's a no-op if we do it multiple times, and here is a handy place to put it.
        (add-hook 'eglot-diagnostics-hook 'flycheck-eglot--on-diagnostics nil t)
        ;; Don't actually start a new check, because we don't drive that process,
        ;; but instead immediately report the currently-known errors.
        (funcall callback 'finished flycheck-eglot--current-errors))

      (defun flycheck-eglot--available-p ()
        (bound-and-true-p eglot--managed-mode))

      (flycheck-define-generic-checker 'eglot
        "Report `eglot' diagnostics using `flycheck'."
        :start #'flycheck-eglot--start
        :predicate #'flycheck-eglot--available-p
        :modes '(prog-mode text-mode))

      (add-to-list 'flycheck-checkers 'eglot)

This works pretty nicely for me, as far as I can tell.

@gagbo
Copy link
Author

gagbo commented Apr 2, 2021

Part of the current issue is that we (Doom, and my hacky code, which I think eventually turned into the Doom code after I published it in a gist and it got passed around)

I didn't know that @purcell, sorry for not finding the correct source. I initially took the snippet from a flycheck issue conversation flycheck/flycheck#1592 (enjoy the github back-reference spam from when I was rebasing the PR for Doom) but I didn't know if the initial version wasn't "original".

The old version (without flymake-mode check)

I always assumed that the LSP server sent all diagnostics on publishDiagnostics, and under that assumption the current callback worked well. Since there's only 1 path in eglot's handler that goes from publishDiagnostics reception to calling the report-fn, the

(lambda (flymake-diags &rest _)
       (funcall callback
                'finished
                (mapcar #'flymake-diag->flycheck-err flymake-diags)))

lambda should only be called once, and therefore flycheck diags should be up to date. At least I don't remember having complaints in Doom issue tracker with this version.

I guess for servers that send incremental diagnostics that would be terrible, but I don't know the protocol and the ecosystem enough to know how often it happens.

Now

I'll try to find time to test f9388ff as well, and I'll reply when I have time. But for what it's worth, I also think that having a hook for diagnostics in the API is going to be more idiomatic and simpler to maintain. Simpler because with a hook we don't have to assume anything about how other checkers/packages work, just about keeping a consistent format for the diagnostics objects in the unreported list.

Cheers,
G

PS: edited to change title levels so they’re less "in your face !"

@purcell
Copy link
Contributor

purcell commented Apr 2, 2021

I didn't know that @purcell, sorry for not finding the correct source.

LOL, don't apologise, I just wouldn't want someone else to be blamed for the initial monstrosity. 😁

the lambda should only be called once, and therefore flycheck diags should be up to date. At least I don't remember having complaints in Doom issue tracker with this version.

As far as I can tell, here a lambda is passed to eglot-flymake-backend, and that can lead to the enclosed flycheck callback being called twice before the next flycheck cycle starts if there are already unreported diagnostics. But yeah, in some situations I think this wouldn't show up so in practice it might not be something you'd notice.

@joaotavora
Copy link
Owner

But a flycheck callback must only be called exactly once

Really? Interesting. In LSP, unlike the vast majority of syntax-checking scenarios, diagnostics don't come in just because you request them. They come in whenever they feel like it. Thus, Flymake, who normally contacts the backends after the buffer state has stabilized for a little while (like 0.5), has to be prepared to receive multiple responses from the callback.

If Flycheck isn't doing this as well (are you 100% sure?) then I would say that's a flaw in Flycheck, or rather a fundamental limitation of Flycheck in the face of LSP. Maybe it works if it's "lucky". Indeed many LSP servers do send only one diagnostic notification per change, but the LSP server doesn't require them to. I'm pretty sure I'm seen LSP servers that send diagnostics immediately after launch, and some others that take their sweet time. I'm vaguely remember a LSP server sending them in multiple batches after startup.

This ties together with your PR. I've seen your PR now and all it does is wrap the "middle" diagnostics calls. There are other diagnostic-reporting situations that the hook doesn't catch, and that's a flaw in that proposal that would have to be addressed. For example, Flycheck will not be notified of diagnostics that were already in the buffer before pressed M-x flycheck-mode right?
Maybe that's a bug one can live with, sure, because it'll eventually sync out, I suppose. but in that case I wonder why this very same semi-buggy behaviour can't be emulated with the latest eglot.el version.

But I'm not here to try to make life hard for any of you, so what do you say to this. In that commit, I created a unary helper function called eglot--report-to-flymake that takes a list of diagnostics. If I remove one - and call it eglot-report-diagnostics you can probably add-function to it and it works like a hook. I'm not crazy about marrying myself to another API export when eglot-flymake-backend should already accomplish the same, but maybe it helps.

In the long term, though, I think the fix should be having Flycheck relax/enhance its backend-communication skills.

On maybe you could try to bring your favorite Flycheck features to Flymake? What are they? What are you missing? Would love to hear.

@joaotavora
Copy link
Owner

I'll try to find time to test f9388ff as well, and I'll reply when I have time. But for what it's worth, I also think that having a hook for diagnostics in the API is going to be more idiomatic and simpler to maintain.

It's perhaps more simple to maintain on your end, not mine :-) A hook is another interface (and @purcell 's hook does have some problems).

Simpler because with a hook we don't have to assume anything about how other checkers/packages work, just about keeping a consistent format for the diagnostics objects in the unreported list.

You don't have have to "assume" anything about how Flymake works. It is all be impeccably specified in the docstring of flymake-backend-functions, or at least it should be :-)

@purcell mentioned a problem regarding eglot-flymake-backend, which invokes its callback more than once per call. I replied that this is a problem in Flycheck, but I also think that a good Flycheck checker (not Flycheck itself) can cache the results in some variable and then send the diagnostics to Flycheck in a single batch (if that's indeed what Flycheck needs).

@purcell
Copy link
Contributor

purcell commented Apr 2, 2021

If Flycheck isn't doing this as well (are you 100% sure?) then I would say that's a flaw in Flycheck, or rather a fundamental limitation of Flycheck in the face of LSP.

Yes:

`finished'
     The syntax checker has finished with a proper error report
     for the current buffer.  DATA is the (potentially empty)
     list of `flycheck-error' objects reported by the syntax
     check.

     This report finishes the current syntax check.

Maybe it works if it's "lucky".

I think that's indeed the case, and that it's a limitation of flycheck.

I also think that a good Flycheck checker (not Flycheck itself) can cache the results in some variable and then send the diagnostics to Flycheck in a single batch (if that's indeed what Flycheck needs).

Yes and no. Such caching is possible, but I think there is also the inevitable issue of who is driving the process: flycheck wants to initiate a check itself and then receive updates, so if we know that there are updates (because the LSP callback told us so), then we'll need to tell flycheck to ask us about them. That's what my code snippet above does, but that principle might be applicable even with the current eglot code, ie. without my PR, unsure.

Indeed many LSP servers do send only one diagnostic notification per change, but the LSP server doesn't require them to. I'm pretty sure I'm seen LSP servers that send diagnostics immediately after launch, and some others that take their sweet time. I'm vaguely remember a LSP server sending them in multiple batches after startup.

Yes, that could get sticky. How would you know whether previously-received diagnostics are still current in the batched case?

This ties together with your PR. I've seen your PR now and all it does is wrap the "middle" diagnostics calls. There are other diagnostic-reporting situations that the hook doesn't catch, and that's a flaw in that proposal that would have to be addressed. For example, Flycheck will not be notified of diagnostics that were already in the buffer before pressed M-x flycheck-mode right?

Correct, and I agree, I'm not convinced the PR is complete. My goal was to sketch out a more decoupled integration mechanism so that the flycheck quirks could be handled independently.

But I'm not here to try to make life hard for any of you, so what do you say to this. In that commit, I created a unary helper function called eglot--report-to-flymake that takes a list of diagnostics. If I remove one - and call it eglot-report-diagnostics you can probably add-function to it and it works like a hook. I'm not crazy about marrying myself to another API export when eglot-flymake-backend should already accomplish the same, but maybe it helps.

Yes, I think that could make sense and would be more or less equivalent. This is probably the best way to move forward now.

In the long term, though, I think the fix should be having Flycheck relax/enhance its backend-communication skills.

Yes, potentially, but I expect this would be a significant technical challenge.

On maybe you could try to bring your favorite Flycheck features to Flymake? What are they? What are you missing? Would love to hear.

Mainly all the display integration options: having errors either display on focus, or appear in a separate full-window tabulated list, plus the ability to show/merge errors from multiple chained backends. There are quite a few reasons it became much more popular than flymake, not least the large number of default and third-party checkers available.

The major part for me is that I use flycheck for everything else, so using flymake for the subset of my work which involves LSP is a jarring inconsistency: when error display and navigation is different it can get confusing.

Perhaps an alternate way to go is writing a package that gives flymake the ability to run flycheck checkers... I haven't really thought that through. 😄

@joaotavora
Copy link
Owner

Yes, that could get sticky. How would you know whether previously-received diagnostics are still current in the batched case?

Anything that hasn't been reporter yet for a given value of report-fn is considered "current"

Yes and no. Such caching is possible, but I think there is also the inevitable issue of who is driving the process:

Right, I think I understand. This caching idea is useless because you don't know when to "stop". Scratch that idea. So what happens if you try your backend adapter and one of these imperfect approaches?

  • ignore all but the first call to report-fn
  • assume each call to report-fn is meant to completely refresh the diagnostics with a new set
  • ignore all calls with nil diagnostics, then respect the first one with non-nil, and ignore all the later ones.

This is probably not perfect, and you will miss some diagnostics in some edge cases, but I can't see it performing any worse than a solution built around eglot-report-diagnostics or eglot-diagnostics-hook. Meaning the same fundamental synchronization problems would apply whichever of the 3 techniques: hook, callback, or add-function.

appear in a separate full-window tabulated list,

Are you aware of flymake-show-diagnostics-buffer?

show/merge errors from multiple chained backends

showing erros from multiple backends is possible. Merging them is not. Where are merging rules specified?

There are quite a few reasons it became much more popular than flymake, not least the large number of default and third-party checkers available.

I think the main reason is that Flymake was a different beast when Flycheck appeared. I rewrote it completely.

Perhaps an alternate way to go is writing a package that gives flymake the ability to run flycheck checkers... I haven't really thought that through. smile

Yes, yes, very much so. Basically doing the reverse of what you did in that infamous gist. Write a function that takes in a Flycheck checker, whatever it is (you probably know what it looks like), and spits out a Flymake backend. Since Flymake backend-communication seems to be a superset of Flycheck's, it looks more viable.

@purcell
Copy link
Contributor

purcell commented Apr 2, 2021

* ignore all but the first call to `report-fn`
* assume each call to `report-fn` is meant to completely refresh the diagnostics with a new set

Yes, this one is my current strategy.

* ignore all calls with nil diagnostics, then respect the first one with non-nil, and ignore all the later ones.

I think a call with nil would be necessary in order to reflect when all the errors in a buffer have been resolved.

Are you aware of flymake-show-diagnostics-buffer?

Yes, I've seen that, and on the face of it that's a similar facility. I can't remember what bugged me about it but I'll try to remember and provide more useful feedback.

showing erros from multiple backends is possible. Merging them is not. Where are merging rules specified?

Sorry, "merging" was a bad choice of word on my part. I meant simply showing all errors from all chained checkers in a single list buffer.

I think the main reason is that Flymake was a different beast when Flycheck appeared. I rewrote it completely.

Yes, this is probably true, and I am grateful for your work!

Yes, yes, very much so. Basically doing the reverse of what you did in that infamous gist. Write a function that takes in a Flycheck checker, whatever it is (you probably know what it looks like), and spits out a Flymake backend. Since Flymake backend-communication seems to be a superset of Flycheck's, it looks more viable.

I agree. There are a few forms this could take, including wrapping individual checkers, or allowing flycheck to run its entire checker chain. I haven't looked into the viability of either but it could be an interesting approach.

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

Re: backend-adapter "invariant"

I've always been in the camp of

  • assume each call to report-fn is meant to completely refresh the diagnostics with a new set

Therefore a call with a nil argument effectively cleared the diagnostics list (that's a behaviour some Doom users complained about, and I didn't have any good solution for that I guess).

Re: why not Flymake ?

On maybe you could try to bring your favorite Flycheck features to Flymake? What are they? What are you missing? Would love to hear.

Main reason for me is just the sheer quantity of checkers available. It probably stems from flycheck being the de facto checker for a while, but whenever I want to start using a linter, I know there's a very high probability that a flycheck checker is already written and packaged. Not really the case for Flymake. For example golangci-lint is something I used when I wrote Go, and I found easily a flycheck checker based on that, not a flymake one; and even if it's not a lot of code, it's still code that I don't have to write/care about.

EDIT: another example is tide, the js package which is currently married to flycheck. It’s probably on them to help users use flymake over flycheck if they want; but meanwhile there are situations where you kind of have to use a flycheck checker, and "having all diags in a single list" requirement makes me lean to flycheck.

Having a generic adapter to make the flycheck->flymake conversion would be good, but I guess it's a lot of work as well.

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

I'm just using the lambda from before, calling this eglot-flymake-backend when emacs starts editing a managed buffer :

(eglot-flymake-backend
     (lambda (flymake-diags &rest _)
       (funcall callback
                'finished
                (mapcar #'flymake-diag->flycheck-err flymake-diags))))

@joaotavora
Copy link
Owner

ah, correct. Then what happens when you try this?

(eglot-flymake-backend
     (lambda (flymake-diags &rest _)
       (when flymake-diags
         (funcall callback
                  'finished
                  (mapcar #'flymake-diag->flycheck-err flymake-diags)))))

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

I'm expecting this to work, but also the diagnostics never to be cleared out after the user fixed the issue. That's definitely something to fix on my end though. How does flymake handles diagnostics that should be removed on a specific region ?

@joaotavora
Copy link
Owner

Here's an important bit. make sure you don't accidentally have flymake and flycheck turned on at the same time! At least make sure to add flymake to eglot-stay-out-of.

@joaotavora
Copy link
Owner

I'm expecting this to work, but also the diagnostics never to be cleared out after the user fixed the issue.

After the user fixes the issue, the buffer has been modified, a new batch of diagnostics is requested that should replace the previous one.

How does flymake handles diagnostics that should be removed on a specific region ?

I could tell you, but that doesn't matter immensely now, I think. What's important is how Flycheck handles this. What is the protocol for calling a callback with finished? What is it supposed to do?

@joaotavora
Copy link
Owner

After the user fixes the issue, the buffer has been modified, a new batch of diagnostics is requested that should replace the previous one.

The problem indeed with the when above is that eventually if diagnostics end up being exactly 0, Flycheck never gets notified of them.

So maybe I would remove the when I suggested earlier, and triple check that Flymake isn't secretly kicking in to dispute internal variables with Flycheck.

@joaotavora
Copy link
Owner

triple check that Flymake isn't secretly kicking in to dispute internal variables with Flycheck.

To do this, simply inspect flymake-diagnostic-functions in the Eglot-managed buffer. It should not contain eglot-flymake-backend

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

The callback implementation is an internal implementation detail for flycheck. As a checker writer, I have to call the callback with the new state ('finished) and the list of diagnostics (the mapcar).

There's a hook that deactivates flymake with this code (through (flymake-mode -1)), but I'll add to eglot-stay-out-of to be sure

@joaotavora
Copy link
Owner

As a checker writer, I have to call the callback with the new state ('finished) and the list of diagnostics (the mapcar).

I'm not asking you about the implementation, I'm asking you what does Flycheck promise to you, the checker-writer as you well put it, when you call the callback like that. Does it make fairies and pizza appear, or what? What is the protocol?

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

I actually don't know, never dug too deep there, I'll have to soon enough I guess, but for now I can't really say.

@joaotavora
Copy link
Owner

joaotavora commented Apr 3, 2021

Would expect it to be documented in https://www.flycheck.org/en/latest/developer/developing.html, but really isn't.

Anyway, I think there's at least one brute-force (but not that horrible either) way to fix this

(defvar some-timer nil)
(let ((callback the-magical-flycheck-callback-youre-using))
  (when some-timer (cancel-timer some-timer))
  ;; if after two seconds the LSP hasn't done its job, assume there are no diagnostics
  (setq some-timer (run-with-timer 2 nil callback 'finished (list))
  (eglot-flymake-backend
   (lambda (flymake-diags &rest _)
     (when flymake-diags
       (cancel-timer some-timer)
       (funcall callback
                'finished
                (mapcar #'flymake-diag->flycheck-err flymake-diags))))))

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

triple check that Flymake isn't secretly kicking in to dispute internal variables with Flycheck.

To do this, simply inspect flymake-diagnostic-functions in the Eglot-managed buffer. It should not contain eglot-flymake-backend

I had to (add-to-list 'eglot-stay-out-of 'flymake-diagnostic-functions) since it doesn't seem like eglot--setq-saving is aware of the feature providing the symbol there, but this way I'm able to keep all diagnostics indeed.

with the when clause everything stays, but every time I make a small edit and save (triggering a RA analysis), the diagnostics accumulate and I have duplicates:
image

Without there's probably a nil message that gets sent and everything is wiped:
image

I'll try looking into flycheck internals to see what can be done, and maybe digging the issue tracker

@joaotavora
Copy link
Owner

Diagnostics accumulate? Strange. You really have to understand what 'finished does. Are you using an internal FlyCheck implementation detail, perhaps?

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

I'm using flycheck-define-generic-checker to create the checker (fixed the link), and the eglot-flymake-backend call is basically the implementation of the :start function. I'll read all flycheck code and find a workaround

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

I digged a little more, in flycheck, flymake, eglot and lsp-mode code, and it seems that it's a lot harder to have a proper flycheck integration.

As

  • I really don't want to maintain that code again
  • and Doom allows to pin dependencies,

I basically still had to use an eglot internal to trigger a flycheck-checker on time, I'm pasting the summary of my approach (pushed in a doom PR doomemacs/doomemacs#4837) below :

;; Eglot is using flymake's better architecture regarding state management to send
;; often small updates (including empty ones) to the reporting functions.
;;
;; This does not work well with Flycheck, which
;; - expects each call to the callback
;; function to contain a single list with all diagnostics
;; - wipes all diagnostics if the callback is called with nil
;; - does not trigger updates unless specifically polled with flycheck-buffer
;;
;; Therefore the plan of this adaptation is to leverage flymake to maintain an
;; accurate state, and hook eglot to trigger a flycheck-buffer call on update.
;; This is slightly the same idea that LSP-mode applies to get new diagnostics
;; on publishDiagnostics notification (the handler of the notification installs
;; a "transient idle timer" that calls flycheck-buffer)
;;
;; It works by creating a bridge function which can query flymake-diagnostics
;; and then advice eglot--report-to-flymake to call flycheck-buffer accordingly
;;
;; There are 2 limitations to this approach:
;; - flymake-diagnostics actually relies on the overlays to know the current
;; diagnostics, so a trick should be found to mask the overlays without deleting
;; them, if we want to avoid multiple annotations. But the overlays still exist.
;; - eglot--report-to-flymake is an internal function and might change whenever,
;; so eglot's pin will be hard to move.
;;
;; Currently there are no plans to mitigate those limitations, as this code is
;; quite hard to maintain and help to make this integration better lacks a little.

I'm pretty sure it's not the correct approach at all, relying on a lot of duplicated effects/diagnostics and internals of everything, but I really just wanted to get this working and be done with it. If other people want to use this as a base to make something that works better, they can, but I don't think I'll give more of my time to that shim :)

@joaotavora
Copy link
Owner

OK, I get it that you're tired of this. But all this work to avoid using the internal implementation detail and in the end you're still using it? 🤦

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

Yeah, I guess I couldn't really find a way to make it work in the end. Maybe someone else will have a go at it and actually make a proper flycheck-only solution. At least it's ready now, but I don't want to dig into flycheck checkers to handle the state when checker can be called with lists args, with no way of semantically separate "wipe diagnostics" from "nothing new" cases

@purcell
Copy link
Contributor

purcell commented Apr 3, 2021

@gagbo With the latest eglot code, this seems to work for me: https://gist.github.com/purcell/78c870fb88ff12906ccc3d07b0389bd8

Maybe give something like that a try locally and see if you observe any issues?

@joaotavora
Copy link
Owner

Well but pinning an Eglot version is a big drawback right? Anyway, do put the commit here I can have a look later on.

@gagbo
Copy link
Author

gagbo commented Apr 3, 2021

@gagbo With the latest eglot code, this seems to work for me: https://gist.github.com/purcell/78c870fb88ff12906ccc3d07b0389bd8

Maybe give something like that a try locally and see if you observe any issues?

This works but I really don't get why, my brain is burnt. I don't see how what I tried before didn't work but this does. I'll add attribution and adapt this, if it's okay.

Well but pinning an Eglot version is a big drawback right? Anyway, do put the commit here I can have a look later on.

Pinning isn't really a drawback, everything is pinned by default, and bumps are made periodically depending on the needed fixes.

@purcell
Copy link
Contributor

purcell commented Apr 4, 2021

I'll add attribution and adapt this, if it's okay.

Yes, of course.

@purcell
Copy link
Contributor

purcell commented Apr 4, 2021

I can't remember what bugged me about [flymake-show-diagnostics-buffer] but I'll try to remember and provide more useful feedback.

I remembered! It doesn't update automatically. A pretty trivial complaint, I know, but I like to keep the errors window open next to the code for certain types of work, and the flycheck version updates continuously as new checks take place.

@purcell
Copy link
Contributor

purcell commented Apr 4, 2021

Also, this is starting to come together: https://github.com/purcell/flymake-flycheck/blob/main/flymake-flycheck.el 😁

@joaotavora
Copy link
Owner

joaotavora commented Apr 4, 2021 via email

bhankas pushed a commit to bhankas/emacs that referenced this issue Sep 18, 2022
Loosen coupling between eglot-flymake-backend and flymake-mode.  The
flymake-mode check in 'eglot-handle-notification publishDiagnostics'
was a hack (and it wasn't even functioning correctly on M-x
eglot-shutdown/eglot-reconnect).

This should also allow eglot-flymake-backend to be driven by
diagnostic-annotating frontends other than Flymake, such as the
popular Flycheck package.

* eglot.el (eglot--managed-mode): Use eglot--report-to-flymake.
(eglot-handle-notification textDocument/publishDiagnostics): Use
eglot--report-to-flymake.
bhankas pushed a commit to bhankas/emacs that referenced this issue Sep 19, 2022
Loosen coupling between eglot-flymake-backend and flymake-mode.  The
flymake-mode check in 'eglot-handle-notification publishDiagnostics'
was a hack (and it wasn't even functioning correctly on M-x
eglot-shutdown/eglot-reconnect).

This should also allow eglot-flymake-backend to be driven by
diagnostic-annotating frontends other than Flymake, such as the
popular Flycheck package.

* eglot.el (eglot--managed-mode): Use eglot--report-to-flymake.
(eglot-handle-notification textDocument/publishDiagnostics): Use
eglot--report-to-flymake.
bhankas pushed a commit to bhankas/emacs that referenced this issue Sep 19, 2022
Loosen coupling between eglot-flymake-backend and flymake-mode.  The
flymake-mode check in 'eglot-handle-notification publishDiagnostics'
was a hack (and it wasn't even functioning correctly on M-x
eglot-shutdown/eglot-reconnect).

This should also allow eglot-flymake-backend to be driven by
diagnostic-annotating frontends other than Flymake, such as the
popular Flycheck package.

* eglot.el (eglot--managed-mode): Use eglot--report-to-flymake.
(eglot-handle-notification textDocument/publishDiagnostics): Use
eglot--report-to-flymake.

#596: joaotavora/eglot#596
jollaitbot pushed a commit to sailfishos-mirror/emacs that referenced this issue Oct 12, 2022
Loosen coupling between eglot-flymake-backend and flymake-mode.  The
flymake-mode check in 'eglot-handle-notification publishDiagnostics'
was a hack (and it wasn't even functioning correctly on M-x
eglot-shutdown/eglot-reconnect).

This should also allow eglot-flymake-backend to be driven by
diagnostic-annotating frontends other than Flymake, such as the
popular Flycheck package.

* eglot.el (eglot--managed-mode): Use eglot--report-to-flymake.
(eglot-handle-notification textDocument/publishDiagnostics): Use
eglot--report-to-flymake.

GitHub-reference: fix joaotavora/eglot#596
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants