-
Notifications
You must be signed in to change notification settings - Fork 775
Support jumping to jdt:// URIs with GoTo #1788
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
base: master
Are you sure you want to change the base?
Conversation
When working on a Java project that runs on either Maven or Gradle, jdt.ls takes care of providing ycmd (and therefore all its clients) with neat and useful features like autocompletion suggestions, documentation on the spot, etc. One feature in particular does not appear to be working to its full potential: going to the definition of a class that is defined or declared _outside_ of the project, using some variation of the ':GoTo', either by typing that command directly or using a shortcut that executes it. Particulary, this is what you get greeted with once you attempt to go to the definition of a class located outside of your project (and could be, for example, located inside a JAR file as it is a project dependency): RuntimeError: Cannot jump to location The primary reason is that ycmd is not sending any JSON related to extending the capabilities of jdt.ls, and as a result, we are not getting any `jdt://` URIs that point to the files where the "definitions" we want are located. We receive nothing in return when the client makes a POST request to the endpoint '/run_completer_command' due to the fact that jdt.ls believes that we are not capable of handling `jdt://` URIs, so it spares us from any troubles. Assuming we want support for such feature, we have to start from somewhere. Particularly, we have to let jdt.ls know that we are now able to handle `jdt://` URIs. Specifically, what needs to be sent is the following JSON during initialization in order to deliver the good news: 'extendedClientCapabilities': { 'classFileContentsSupport': True } Introduce a new function AddExtendedClientCapabilities() which takes in a 'settings' parameter and simply adds the _correct_ JSON necessary for jdt.ls to start responding back with `jdt://` URIs. Existing settings provided in .ycm_extra_conf.py are not touched, we simply "append" the above JSON to what already exists. Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
Great. Every time ycmd initializes with jdt.ls, it makes the server aware that it is able to fetch Java class file contents and properly handle them ("classFileContentsSupport" extended capability) - but in reality, that's _not_ the case. In order to make this work, change the handling of incoming 'jdt://` URIs that jdt.ls currently fires off as needed. The goal: use these `jdt://` URIs to jump to the definition of a class or method located in a JAR file (for example, when checking the definition of, say, a method or class from a library). Focus on GoTo() that is located in the file `language_server_completer.py`, since executing `:YcmCompleter GoTo` should work even when the class or method under the cursor is declared or defined inside the Maven/Gradle project, not just in a dependency. Introduce a function IsJdtContentUri() that takes a string input `uri` and checks whether the first few characters match "jdt://". Use this inside GoTo() in `language_server_completer.py` to determine whether to fetch the contents behind the URI by sending another request to the language server: `java/classFileContents` — the extended capability enabled by adding `classFileContentsSupport` to the `extendedClientCapabilities` list. When GoTo() receives a `jdt://` URI, send a request to get the file contents (along with cursor data, specifically where to place the cursor), package all that in a JSON, and hand it over to YouCompleteMe for display. In that JSON, name the key for the file contents "jdt_contents" to communicate to YouCompleteMe that these contents should be displayed in a temporary readonly vim buffer. The communication flow for jumping to `jdt://` URIs with GoTo: ycm ycmd jdt.ls | | | |----GoToDefinition---->| | | | | | |-textDocument/definition->| | | | | |<------LSP response-------| | | | | |-java/classFileContents-->| | | | | |<---class file contents---| | | | |<--jdt_contents JSON---| | | | | A short description of each part of the flow, starting from the top (first) to the bottom (last): 1. This is the part where we run the command from our client, where we execute `:YcmCompleter GoToDefinition`. 2. ycmd sends a `textDocument/definition` to get the `file://` or `jdt://` location and the range pointing to the definition location. 3. If the langauge server responds with a `file://`, then it's business as usual. If a `jdt://` URI appears, send a `java/classFileContents` request [4] to retrieve the file contents. 4. (If [3] returned a `jdt://`) Send `java/classFileContents` to the langauge server to retrieve the file contents. 5. Language server responds with the class file contents. 6. Package the file contents, range, and `jdt://` URI in a JSON and send it to ycm, the original requester. ycm should now make a temporary readonly vim buffer with the contents from the JSON and pointed at the cursor coordinates found in the JSON. There are surely ways to send a better JSON payload back to ycm (the JSON in step [6]), but this approach does the trick for now and handles other cases as well. Additionally, do not treat `jdt://` URIs as file paths, and for the places where they risk being treated as so, conditionally return the file path itself with the `jdt://` included, or if it's not a `jdt://` path, then run the normal conditions. In _LspSymbolListToGoTo(), filter the list of locations to not include any JDT-related results so that they do not appear when typing `:YcmCompleter GoToSymbol <symbol>`. It wouldn't work anyways as these file paths do not point to an actual file and the default behavior is for the client to open an empty new file with the name of the whole JDT URI - behavior that we do not desire. Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
The implementation and logic for handling `jdt://` URIs and getting contents from such URIs is scattered all across abstract classes and methods (namely, `language_server_completer.py` and `language_server_protocol.py`) which is a considerable code smell. We can do better than that... Move all `jdt://` URI handler related code into `java_completer.py` and `java_utils.py` where we take care of `jdt://` handling logic _only_ when we are dealing with a Java project - and therefore when a Java completer is in use. Update unit tests to match refactoring. Helped-by: Ben Jackson <puremourning@users.noreply.github.com> Helped-by: Boris Staletic <boris.staletic@protonmail.com> Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
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.
Reviewable status: 0 of 2 LGTMs obtained
ycmd/tests/language_server/language_server_completer_test.py
line 97 at r1 (raw file):
class MockJavaCompleter( MockCompleter, JavaCompleter ):
these tests don't belong here. should be testing this feature in tests/java/ by actually triggering the code with a real completion engine rather than mocks.
ycmd/completers/java/java_completer.py
line 333 at r1 (raw file):
return { 'bundles': self._bundles, 'capabilities': {
this seems... strange. why would we send definitionProvider: true to the server? and why in this way? what motivated this part of the change?
ycmd/completers/java/java_completer.py
line 673 at r1 (raw file):
def GoTo( self, request_data, handlers ):
is this mostly a copy/paste of the base implementation? it doesn't seem completely focussed on supporting different URL schemes.
Better to add a hook in the base class rather than just copy paste the implementation.
ycmd/completers/java/java_completer.py
line 685 at r1 (raw file):
# flake8 doesn't like long lines so we have to split it up def HandlerCondition( result, request_data ): return result and \
prefer parentheses or split it into 2 ifs
return (
result
and lip._CursorInsideLocation(....)
)
ycmd/completers/java/java_completer.py
line 686 at r1 (raw file):
def HandlerCondition( result, request_data ): return result and \ not language_server_completer._CursorInsideLocation( request_data,
don't use internal functions. If it needs to be public make it so by renaming to remove the leading _
ycmd/completers/java/java_completer.py
line 700 at r1 (raw file):
raise RuntimeError( 'Cannot jump to location' ) first_result = result[ 0 ]
why is the first result special? why is GoTo special?
we should be handling any URI provided by JDT as jdt://. there are many other requests which return file paths which might not be file://
we need to not special case GoTo and rather implement handling for jdt:// URIs more generally.
ycmd/completers/java/java_completer.py
line 727 at r1 (raw file):
def GetClassFileContents( self, request_data ):
I would have expected this to be implemented as a completer subcommand that takes the URI as an argument. We have a whole infrastructure for that, and in the YCM client we have the ability to get "raw" output from the command request for these sorts of things.
See for example how I hacked some Rust-specific stuff here: puremourning@9619268#diff-b59e92be42fece00d4f5c415d99c3a53d145a7f4a816cb6f7839481830dbf3c8
and in the client, there's the command_request and various ways that it's used.
This pull request addresses a limitation in ycmd's integration with jdt.ls, where jumping to definitions in external JAR files (e.g., project dependencies) resulted in errors like "Cannot jump to location." (look at the before vs after videos below). By enabling the
classFileContentsSupport
extended capability during initialization, ycmd signals to jdt.ls that it can handle jdt:// URIs.More information on the commit messages.
Note! This PR should be merged alongside #4302 on the YouCompleteMe repository.
Before vs After
before.mov
after.mov
This change is