Skip to content

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

christolis
Copy link

@christolis christolis commented Jul 14, 2025

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 Reviewable

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>
Copy link
Member

@puremourning puremourning left a 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants