feat: add getDiagnostics function #9
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "patch-1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds a function that can be used to retrieve all diagnostics that are present in the provided state. This PR basically addresses this discussion over on discuss.codemirror.net that was originally posted for CodeMirror 5, but since this functionality doesn't exist in the v6 as well, I figured it would be great to finally have it.
Description
What this PR does is add a new function,
getDiagnosticsthat returns all diagnostics that are present within a provided state. To do so, it basically just takes thelintStatefield and iterates over the generated decorations, collecting the attached diagnostics along the way and returning them as a single array.This can be used to attach various linters to the state and retrieve them. Users can then afterwards filter them (e.g. look at their
sourceproperty) and do something with it. This can prove handy for example when you have one or more linters active that probe a file for problems and need to access them outside of the linter function in order to prevent double work.Rationale
Normally, the
actionfield can be used to define some actions the user can perform to fix or improve the linter error. However, sometimes the potential actions are too many or need to be fetched on-demand. Therefore, it is handy to have a function to quickly retrieve whatever the linters have found and therefore to avoid having to check the file's contents twice.Example usecase
I've basically moved forward with this because I'm using the built-in linter function to create a custom spell checker (because I can't completely rely on the OS for doing the spell checking).
Said spell checker contains a ton of code to ensure we're spell-checking correctly (i.e. takes into account brackets and custom quote symbols). However, while functionally a spell checker is a linter, potential
actionsthat can be taken are normally provided in a context menu and not viaactionslike other linters.In order to avoid having to run the spellchecker twice over the text, one can use this function to retrieve all diagnostics currently attached to the state whenever the user requests a context menu, filter out anything whose
sourceis my spell checker and check if there's a spelling error underneath the cursor position, and provide suggestions as applicable.Caveats
Currently, this simply takes all diagnostics irrespective of the originating
Facets, both because I'm still not comfortably proficient with the use of facets, and because normally the amount of linter errors/warnings/infos should be fairly manageable. Therefore, this PR basically rests on the range iterator being fast.Also, one could also simply save the decorations manually in the stateField, but I felt this implementation is the least intrusive into the existing code, hence preventing potential other problems.
I tried to stick to the conventions of the repo, but please feel free to outline any potential problems with this implementation and what I can change.
Thank you for your outstanding work – CodeMirror 6 is simply amazing!
Would it work to listen to transactions with the
setDiagnosticsEffectand act on the array of diagnostics they provide, instead of formulating this as a state accessor?It would somehow work, but unnecessarily increase the complexity of the state management.
Utilizing a
getDiagnosticsaccessor as I propose here has the benefit that it changes nothing about either my own code or the linter plugin's code. As soon as I need to actually have a look at those diagnostics, I can access the diagnostics with a single function call.Furthermore, I am here talking of the use case of some external code taking a look at the
stateto see if something is in there. For purely internal code that already utilizesStateFields, the effect listener is definitely a viable option.In the case of external code needing to take a look at the state, the
getDiagnosticsaccessor has, in my view, some benefits:The alternative that you suggest has, if I don't misunderstand anything blatantly (please say so, if I do), several drawbacks for the use-case I'm describing:
StateFieldthat simply collects diagnostics as they are being set by thelintplugin and that can then be queried withview.state.field(customField). This would essentially replicate the changes I propose in this PR with an extra step.StateFieldhas to touch the diagnostics every iteration they change, this imposes a tiny overhead over just accessing them when absolutely necessaryOr, is there some benefit that the detour via the state effect listener has that I am not seeing here?
If people are going to call this a lot (which is a reasonable thing to do, if it is an exported function), converting the range set to an array again and again is definitely not an example of efficiency. Additionally, the diagnostic objects in the range set may hold positions that no longer correspond to their current position, or even be filtered out entirely, making this a rather error-prone interface that I'm not willing to commit to. See also #7 and #8.
Hm, I've read through #7 and #8 and now I have a few more questions.
You state here as well as in #8 that the diagnostics being returned by that accessor could be outdated if the document has in the meantime been updated and the linter did not (yet) re-run the linter sources. However, if we use an update listener, doesn't this approach have exactly the same issue, just that the (potentially outdated) diagnostics now reside in a different state field ? After all, the effect will only be fired once the sources are all done computing their set of diagnostics. Therefore, any approach in this regard would be somewhat error-prone, except you add even more code to always clean out the diagnostics array when the document changes and wait for the state effect to arrive.
I absolutely agree that the solution advocated for in #7, #8, and #9 does impose some overhead when it comes to iterating over the decoration ranges and re-converting them to diagnostics. The other way to implement this then would be to simply store the diagnostics separately of the decorations instead of attaching them to the specs. Then, returning the array of current diagnostics would add only very little overhead.
I think if the
getDiagnosticsaccessor is properly documented, one should expect users of this to be sane enough to know that, since the linters run not synchronous to state updates, these decorations may be outdated.Nevertheless, if you decide that it is not worth implementing it as-is, I have another proposal for you that would address the issue you describe and would prevent errors much better, regardless of whether the state management is located within the lint plugin or externalized to custom-written state fields:
lintStatefield (to ease accessing them)currentDiagnosticsandoldDiagnosticscurrentDiagnosticscurrentDiagnosticstooldDiagnostics. As soon as new diagnostics have been computed, they are written tocurrentDiagnostics, except the state has changed again, in which case they will be directly written tooldDiagnosticsgetDiagnosticsaccessor receives another parameter,precise(similarly to some other interfaces that CodeMirror exposes which are also somewhat volatile). Ifpreciseistrue, onlycurrentDiagnosticsis returned, even if it is just an empty array because the sources have not yet finished linting the document. If not,oldDiagnosticsmay also be returned while the linter computes the next batch of diagnostics.Depending on how much you would be willing to commit to having "outdated" diagnostics stored in the state, we could also just get rid of the second array, which would make the required changes less difficult to implement and would make the accessor definitely return 100% safe diagnostics that are not outdated.
This would have the following benefits:
StateFieldsthat do not shield them from the outdating-issue that you describeWould this be a compromise you're willing to settle on? Given that apparently there now have been three consecutive PRs that all address this one, single issue, I think there are definitely benefits to it, and people find it curious why there is no simple accessor.
If you're willing to give it a shot, I will implement the changes as necessary and update this PR accordingly. I would very much welcome if we could work this out :)
Given the nature of transaction effects, the fact that this is a snapshot is a lot more obvious in the current interface.
The approach with current and old diagnostics seems to complicate the concept without providing much benefit.
An internal iterator that runs a function over the diagnostics rangeset, passing the current position + the original object might be a simple compromise. Would that work for you?
That sounds good. How do you imagine this to be implemented?
Does attached patch look like it'd cover your use case?
Absolutely, yes!
Released in 6.1.0
Pull request closed