-
Notifications
You must be signed in to change notification settings - Fork 54
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
[FOR DISCUSSION] - Draft revised API #333
[FOR DISCUSSION] - Draft revised API #333
Conversation
Done: Find an API that makes multi-LS logic more elegant TODO: Move server start and DocumentContentSynchronizer onto the dispatcher thread
Fix embarrassing cut-and-pasto introduced into the hyperlink detection, and potential NPE inside DocumentColorProvider Add shutdown hook for dispatcher thread to test teardown so we don't end up with a huge number of zombie dispatcher threads.
…bilities predicate Embarrassing logic bug: the code was supposed to return a null completable future if the document connection failed or the server predicate was not met, but as originally written always wrapped the LanguageServerWrapper...
…ions Add null/nonnull annotations as requested
…formatting Made the format handler check that no document updates have overtaken a format request. Added support for version check to LSPEclipseUtils. Enhanced format test
…ingle-threaded dispatch logic
Minor code review actions
Using chained futures with multiple async compositions isn't quite correct, even if they are all scheduled onto the same dispatcher. If the connection future is completed then we need the language server operation we wish to execute to be scheduled straight onto the dispatch queue. What we had was scheduling something onto the dispatch queue, that in turn scheduled the operation onto the back of the dispatch queue, allowing another call (e.g. a document update) to interleave.
… UI (and indeed doesn't use the UI thread)
Addendum: There are quite a I think that in all of these cases we should be passing around a This would also obsolete some of the rest of the API: for example there are static methods on I think much of the same applies to Partly to make this kind of thing easier, you will note the proposed api takes a BiFunction that receives both a wrapper as well as the underlying server. In many cases the wrapper argument will be unused, but this is fine, the supplied function will just be something like
and also
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this proposal.
So if I get it right, the general idea is to 1. make LSPExecutor the main entry point, and then 2. make LangaugeServerWrapper the main object to manipulate? I think I like this proposal in general.
One thing I find missing in this proposal is a note about what can be deprecated/removed if we start using this new API. Do you think most of LangaugeServerAccessor can be removed or made internal? Do you think some parts of LanguageServerWrapper can also be removed (eg methods returning direct access to the LanguageServer instance), or maybe we can extract the execute/notify methods into an interface and let only those be referenced/consumed in most cases?
I've also added some code comments inline.
return initializeFuture.thenApplyAsync(theVoid -> { | ||
synchronized (connectedDocuments) { | ||
if (this.connectedDocuments.containsKey(uri)) { | ||
return CompletableFuture.completedFuture(null); | ||
return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those change would be welcome in a separate patch as they're code improvements independently of the API
@@ -435,6 +437,16 @@ public static void applyEdits(IDocument document, List<? extends TextEdit> edits | |||
} | |||
} | |||
|
|||
public static void applyEditsWithVersionCheck(IDocument document, final long expectedModificationStamp, List<? extends TextEdit> edits) throws BadLocationException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest we call is applyEditsToDocumentVersion
and document it with a good javadoc. Particularly, it seems important that the behavior in case of incorrect version gets specified; and for this, it could be better to send a check exception instead of a RuntimeException.
This new method (and the other one in this class) + example of consumers would be good candidates for a separate PR IMO, as it's not really re-designing the API but providing a new utility that would be useful independently of other API changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right - I'm regretting basing this PR off of #251! See my recent comments below - shall I just create a new PR off master
with just the core API changes? All the examples and nice-to-have related changes can be submitted in separate PRs.
import org.eclipse.lsp4j.ServerCapabilities; | ||
import org.eclipse.lsp4j.services.LanguageServer; | ||
|
||
public abstract class LSPExecutor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we try to have it an interface instead?
How to get an instance of this?
What about calling it just LanguageServers
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we try to have it an interface instead?
I guess, although in this case it makes sense to use subclassing as (a) the bulk of the implementation is common to the Document and Project implementations, and (b) these implementations have some methods that are specific to themselves.
How to get an instance of this?
There are two static factory methods forDocument()
and forProject()
What about calling it just
LanguageServers
?
Sure - not wedded to any particular name!
I think LanguageServers
would be good for the static factory methods. For the actual implementation classes LSPDocumentExecutor
and LSPProjectExecutor
, what would they be called instead?
* @return Async result | ||
*/ | ||
@NonNull | ||
<T> CompletableFuture<T> executeOnLatestVersion(@NonNull Function<LanguageServer, ? extends CompletionStage<T>> fn) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want something else than "onLatestVersion"? If not, we may just omit the suffix and call it requestLanguageServer
and notifiyLanguageServer
.
The javadoc shouldn't really mention things about how stuff is dispatch, it should be a bit higher level. Technical comments can be added as @implNote
to the Javadoc IIRC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, the names are confusing, especially if we are making the concurrency guarantees intrinsic to the core API rather than just adding in extra methods with optional extra guarantees. I think I favour execute(fn)
and sendNotification(fn)
- short and simple; notify
is a bit problematic to use on its own as a method name because of Object.notify()
!
Maybe the technical notes about the threading design could be a markdown document at the top level?
Exactly - we want to control access to the LS so that people don't subvert the lifecycle/dispatch/ordering guarantees, and also try and make it as easy as possible to do standard things.
I think pretty much everything in LSA can be removed/made internal. One way of doing this might be to rename all the existing methods
Certainly an interface might be nice - certainly submitting a call to the server, returning the wrapper status and server capabilities are all high-level API methods that ordinary code should be fine to call, whereas the various low-level methods, particularly the lifecycle stuff, should really be used only by other low-level framework code. Similarly for the handful of miscellaneous but important plugin-level stuff like connect/disconnect content types on One thing I have been pondering is whether to have a SingleExecutor that just wraps a single |
Thanks @mickaelistria . I haven't responded in detail just yet because I wanted to try and implement a bit more of the proposed API just to make sure I hadn't written a cheque I couldn't cash, i.e. proposed something that was actually impossible to implement!! I think naming conventions and interfaces and so on are things we can certainly tidy up if we're happy the approach is going to work. Please keep the comments coming - what I am going to do next is try porting a few bits of client code over to use it, to see how that looks. The I think so long as we can capture the major use-cases then that's probably adequate: we can cope with a small amount of specialised code that has corner-case needs and so has to combine low-level functions itself in very unusual ways. |
These examples were already changed into a very similar form by the earlier proposal in PR#251. However, DocumentColorProvider/ColorInformationMining also exhibit a common but potentially undesirable pattern in the codebase - using the response from the server to construct objects which embed the raw LanguageServer as fields - this is so that they can make subsequent calls on the same server without going via LanguageServiceAccessor. I have proposed using LanguageServerWrapper in this case so that subsequent calls can use the high-level API and not potentially subvert the threading/ordering guarantees. This commit shows that this approach seems to work with trivial changes. TODO: Decide what API LanguageServerWrapper should expose and whether it should be hidden behind an interface (or in a new, higher-level object to wrap the wrapper!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for the PR. I hope a find more time tomorrow to do an in depth review of all the conversation and the changed code. So far only a partial review.
formatter.requestFormatting(document, textSelection).thenAcceptAsync( | ||
edits -> shell.getDisplay().asyncExec(() -> formatter.applyEdits(document, edits))); | ||
edits -> shell.getDisplay().asyncExec(() -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like you need to rebase the PR, I already commit version of it which does the same (and to my taste is nicer because it relies on an incremental document version and not timestamps).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does! I'm rather regretting basing this PR off of #251 rather than master... It draws upon the earlier work but actually reimplements most of it, so I could probably have started it from scratch off of master.
I'm mostly trying to get comments and feedback on the approach rather than proposing to merge ASAP, but if we think this is a goer then I'll either do a major clean-up or just open a new PR off master with the relevant commits cherry-picked and cleaned up.
private IPreferenceStore store; | ||
|
||
public DocumentContentSynchronizer(@NonNull LanguageServerWrapper languageServerWrapper, | ||
@NonNull LanguageServer languageServer, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LanguageServerWrapper
already has a reference to the languageServer
, so why pass it on its own? Could we not add a method to the LanguageServerWrapper
if we do not want to call languageServerWrapper.getInitializedServer()
(is it not just fine to do so, or is the problem that it can side effects?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah it's a bit ugly. The key thing I'm trying to achieve with the new dispatch logic is to make it possible to have an (internal) dispatch function of the form getServer().thenComposeAsync({code}, dispatcher), with getServer() doing returning a completable future that guarantees that all the initialisation prerequisites will have completed.
The key thing is that we want in the majority of cases getServer() to return a completable future that has already completed, in which case the block {code}
will be scheduled straight onto the dispatcher thread, i.e. now from the point of view of the caller. This is critical in making sure most of the client code in LSP4e, which is usually initiated from the UI thread, gets its work scheduled at consistent point in time with respect to any document updates.
I think in this case you're right and it's OK to call getInitializedServer()
. The reason I've done this is if you look at the place where this is constructed in LanguageServerWrapper
, it's off the back of an initialisation future, so at that point, we are guaranteed to have the LanguageServer
ready.
I was just a bit over-paranoid about calling some of the methods that return async objects, just in case I accidentally introduced another possible race condition. It's important that the didOpen
message gets sent to the language server before the object returned by LanguageServerWrapper.connect()
gets used for any more calls.
DocumentContentSynchronizer listener = new DocumentContentSynchronizer(this, theDocument, syncKind); |
and
return initializeFuture.thenComposeAsync(theVoid -> { |
are critical.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that and I think it is good, since you want to be sure ;). I think than better than just passing the LS, would be to have a method that gets you the languageServer
as is without doing anything else. Given that getServer
already exists but calls getInitializedServer
internally, I would propose we create a new method unwrap
in LSW that just return languageServer;
. What do you think of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, thought about that - I think having package-protected methods like unwrap
on LanguageServiceAccessor
, LanguageServerWrapper
, LSPExecutor
and DocumentContentSynchronizer
as those are all closely-connected things concerned with the low-level details and can be carefully reviewed to make sure they're sound. So long as the low-level operations are not exposed to the general world, that's very sensible.
Illustrates use of LSPProjectExecutor rather than LSPDocumentExecutor Added a support function to test whether there is any LS that can support the operation - this test arises in a couple of places in the code so makes sense to be API.
@mickaelistria @rubenporras please see my latest couple of checkins - I've ported a couple more bits of code across to see how things look in a wider variety of use-cases. Specifically a project-based executor and also using the I've got this afternoon off but next I'll have a go at format, as that's one of the intended uses of Cheers, A. |
To be complete, for formatting one could propose the pattern of looping though the servers and do a whole roundtrip when looping. Example if we have two servers s1 and s2. Request formatting to s1 with the buffer contents => wait for answer to the request => apply format This is not what we currently do but it would work if the server have orthogonal responsibilities, as if for example s2 would only replaces spaces with tabs, remove trailing whitespaces, setting end lines to UNIX/Windows format or something similar, would work fine. It might not be relevant to the PR but just to be complete. |
I think looping through the servers is still a valid use case it one wants to manage its lifecycle programmatically. That is something we want to do because we want to start / restart different versions of the same LS depending on what our user has selected as his runtime environment. So we need code such as
|
I have at least a case where I want to get the
|
* @return A stream (empty if col is null) | ||
*/ | ||
@NonNull | ||
public static <T> Stream<T> streamSafely(@Nullable Collection<T> col) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move streamSafely
somewhere else, it does not look right on the LanguageServiceAccessor
super.fill(menu, index); | ||
} | ||
|
||
private void scheduleMenuUpdate(final Menu menu, final int index, final LanguageServerWrapper wrapper, final Throwable u, final List<Either<Command, CodeAction>> codeActions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like a lot that such very long lamdas as factored out as methods, but doing it as part of this PR makes hard to see the actual difference, would you mind pushing a PR with this refactor (and other similar) independently? I think we can merge those quickly and make the diff here more clear
|
||
this.request = LSPExecutor.forDocument(document) | ||
.withFilter(capabilities -> LSPEclipseUtils.hasCapability(capabilities.getHoverProvider())) | ||
.collectAll((wapper, server) -> server.getTextDocumentService().hover(params)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is taste, but to me instead of having BiFunctions in the API, it looks more natural having just a Funciton and doing
.collectAll(w -> w.unwrap().getTextDocumentService().hover(params));
the API is simpler to understand and it does not like that the code is more complex or lengthier.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this applies to many calls in this PR, but I comment it only here and the one I already did before. I think than having two parameters, even when only one is used usually it is fine, but if you can derive one from the other easily, that looks weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, the problem is that unwrap
would have to be a public method, which is the sort of thing I'm trying to discourage. It would be perfectly ok to call it inside the lambda function, but you wouldn't be able to prevent people just calling LanguageServerWrapper.unwrap()
.
What about adding overloads to the 3 main methods that take only a single-argument function LanguageServer
, for the majority case when that's all you need? I was trying to avoid an explosion of overloads but that would be manageable, especially as they would just do trivial delegation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about that, but it is also not ideal.
What about actually offering the methods of LanguageServer
in the LanguageServerWrapper
and just implement them by delegating to the LanguageServer
? That is delegating only seven methods, so it is straightforward and would mean that having the wrapper is as good as having the sever handle.
Illustrates the computeFirst() method. Didn't fix up the tests so had to leave the existing code in place and bypass it, since the new implementation returns CF<Optional<List<Edits>>> rather than <CF<List<Edits>>
I've ported the formatting code to the new API 4963536 just as a proof-of-concept so that I could test that the Sorry that it's a bit messy - I had to leave the old code intact (but bypassed) as I didn't want to have to fix up the tests just for a PoC. I have at least tested it with the Corrosion/Rust plugin and it works, at least for the happy case! |
It's not strictly needed here but I also quickly sketched out 1957c25 to show that the related 'optimistic locking' problem can have direct API support - we make the executor track the document timestamp when it dispatches a request, and then add a supporting method to turn the response into a @rubenporras this might particularly interest you, since you implemented the PR for formatting version-check. |
@rubenporras see 1957c25 - I think we can actually offer a higher-level support for optimistic locking elegantly, in which case |
Absolutely, the low-level lifecycle methods are still needed, but I think only rare/specialized code will need access. I think for that reason I should maybe not make |
Hi guys, I've now done all of my PoC work in porting example code to use the API. Sorry that it has meant this PR is not as tightly-focused as it maybe should have been. However you can only really see whether an API works when you try and make use of it! I think I'm happy that this API will cover the core usage of the plugin. I think maybe the next step is to open another PR with just the core API implementation and none of the ported examples nor dead baggage from #251 . Then we can establish the names and method signatures and the way we package things up into interfaces. @mickaelistria @rubenporras what do you think? Since we're proposing to change the core way in which the majority of the code accesses the LS I think the API needs wide buy-in. Is there anyone in particular I should consult @mickaelistria ? Perhaps some of the core committers? |
Sure, if we can do better, then I am happy. I am a bit afraid we want to do too much at once and we might loose ourselves in discussions, but we well see ;) |
…ake the fluent builder be properly class-covariant.
@ahmedneilhussain Is it still relevant since we merged #344 ? |
Now superseded by the real thing! |
Following the discussions on #251 about the need to reform the UI to address concurrency problems, I have gone through the existing API, specifically
LanguageServiceAccessor
,LanguageServerWrapper
andLSPDocumentInfo
in some detail, to try and understand the use cases.Please see
LSPExecutor
in the latest commit for an outline of what I think could be implemented.I have based this commit off of #251 as it will build on some of the work in that branch (but replace some of it), although I possibly should have just kept the design discussion based off master (sorry!)
The core workflow for most uses of the language server goes as follows:
IDocument
or anIProject
as a selector, combined with a server capabilities predicate.Stage 3 is complicated by the fact the calling code must deal with potentially more than one language server in play. At the moment, this is handled ad-hoc by the calling code. This results in a lot of messy code at the various call-sites, which tends to obscure what is going on. I think it would reduce boilerplate to handle this as part of the API, and I think this can be done fairly easily as there are only a small number of actual patterns in play, probably reflecting the fact that there are not too many sensible ways of handling multiple results! I propose:
a. Return all the results aggregated as a
CompletableFuture<List<T>>
. This is by far the commonest case in the existing code, and the one I already implemented in #251 .b. Return all the results separately as
List<CompletableFuture<T>>
. This is a minor use case at present, but crops up in e.g.LSPCodeActionsMenu
where placeholder items are created and replaced as the server results return one by one.c. We only want one result, e.g. edits from a format request. This isn't handled well at the moment - there are a couple of instances of just arbitrarily grabbing the result from the first server in the list, with a TODO about maybe selecting the result from the first server to respond. I think I know how to implement this with a bit of CompletableFuture trickery and so we can have a method returning
CompletableFuture<Optional<T>>
Stages 1 and 2 are complicated by a lot of lifecycle logic, with some pretty subtle considerations. For this reason I propose to implement this part of
LSPExecutor
largely on top of this existing logic insideLanguageServiceAccessor
andLanguageServerWrapper
. This avoids the danger of ditching and reimplementing a lot of presumably battle-hardened code. It also provides a smooth migration path: the existing methods can be deprecated and then made internal-only (with any unused ones deleted).I have proposed a builder-like pattern for the
LSPExecutor
design. I think this works quite well when you have to support a lot of different use cases, many of which are closely related, and you risk having an explosion of overloaded methods. It also offers a degree of future proofing as further customisation could be added on later without breaking existing code.The basic usage would go something like