From 37a9318feb33d074f7e3508e0dc3b033b2d87c32 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Mon, 7 Jul 2025 21:18:49 +0300 Subject: [PATCH 1/3] jdt.ls: add `classFileContentsSupport` capability 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 --- ycmd/completers/java/java_completer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ycmd/completers/java/java_completer.py b/ycmd/completers/java/java_completer.py index 3380395712..c3b007240b 100644 --- a/ycmd/completers/java/java_completer.py +++ b/ycmd/completers/java/java_completer.py @@ -328,6 +328,9 @@ def __init__( self, user_options ): def DefaultSettings( self, request_data ): return { 'bundles': self._bundles, + 'extendedClientCapabilities': { + 'classFileContentsSupport': True + } # This disables re-checking every open file on every change to every file. # But can lead to stale diagnostics. Unfortunately, this can be kind of From d3fdc9c49f1dfa672b57124d34ce375742c05f39 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Mon, 7 Jul 2025 21:18:49 +0300 Subject: [PATCH 2/3] jdt.ls: support jumping to `jdt://` URIs with GoTo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `. 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 --- ycmd/completers/java/java_completer.py | 3 + .../language_server_completer.py | 32 ++++++++++ .../language_server_protocol.py | 13 ++++ ycmd/responses.py | 20 ++++++- ycmd/tests/java/debug_info_test.py | 30 +++++++--- .../language_server_completer_test.py | 59 +++++++++++++++++++ .../language_server_protocol_test.py | 8 +++ 7 files changed, 154 insertions(+), 11 deletions(-) diff --git a/ycmd/completers/java/java_completer.py b/ycmd/completers/java/java_completer.py index c3b007240b..34f8c9538d 100644 --- a/ycmd/completers/java/java_completer.py +++ b/ycmd/completers/java/java_completer.py @@ -328,6 +328,9 @@ def __init__( self, user_options ): def DefaultSettings( self, request_data ): return { 'bundles': self._bundles, + 'capabilities': { + 'definitionProvider': True + }, 'extendedClientCapabilities': { 'classFileContentsSupport': True } diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index d8a7ee7222..3b6bcf5700 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -2618,6 +2618,15 @@ def GoTo( self, request_data, handlers ): if not result: raise RuntimeError( 'Cannot jump to location' ) + + first_result = result[ 0 ] + if responses.IsJdtContentUri( first_result[ 'uri' ] ): + contents = self.GetClassFileContents( first_result ) + response = first_result.copy() + response[ 'jdt_contents' ] = str( contents ) + + return response + return _LocationListToGoTo( request_data, result ) @@ -2663,6 +2672,26 @@ def GoToDocumentOutline( self, request_data ): return _LspSymbolListToGoTo( request_data, result ) + def GetClassFileContents( self, request_data ): + """Retrieves the contents of a Java class file from the language server + using the provided request data, ensuring the server is initialized before + proceeding; raises a RuntimeError if the server is not ready, sends a + request to obtain the class file contents, and returns the result if + available.""" + if not self._ServerIsInitialized(): + raise RuntimeError( 'Server is initializing. Please wait.' ) + + request_id = self.GetConnection().NextRequestId() + response = self.GetConnection().GetResponse( + request_id, + lsp.ClassFileContents( request_id, request_data ), + REQUEST_TIMEOUT_COMMAND ) + + result = response[ 'result' ] + if result: + return result + + def InitialHierarchy( self, request_data, args ): if not self.ServerIsReady(): raise RuntimeError( 'Server is initializing. Please wait.' ) @@ -3410,6 +3439,9 @@ def _LspSymbolListToGoTo( request_data, symbols ): sorted( symbols, key = lambda s: ( s[ 'kind' ], s[ 'name' ] ) ) ] + locations = [ location for location in locations + if not responses.IsJdtContentUri( location[ 'filepath' ] ) ] + if not locations: raise RuntimeError( "Symbol not found" ) elif len( locations ) == 1: diff --git a/ycmd/completers/language_server/language_server_protocol.py b/ycmd/completers/language_server/language_server_protocol.py index 365f8a04c5..af328bac92 100644 --- a/ycmd/completers/language_server/language_server_protocol.py +++ b/ycmd/completers/language_server/language_server_protocol.py @@ -26,6 +26,7 @@ ToBytes, ToUnicode, UpdateDict ) +from ycmd.responses import IsJdtContentUri Error = collections.namedtuple( 'RequestError', [ 'code', 'reason' ] ) @@ -642,6 +643,12 @@ def Position( line_num, line_value, column_codepoint ): } +def ClassFileContents( request_id, request_data ): + return BuildRequest( request_id, 'java/classFileContents', { + 'uri': request_data[ 'uri' ] + } ) + + def PrepareHierarchy( request_id, request_data, kind ): return BuildRequest( request_id, f'textDocument/prepare{ kind }Hierarchy', { 'textDocument': { @@ -753,10 +760,16 @@ def InlayHints( request_id, request_data ): def FilePathToUri( file_name ): + if IsJdtContentUri( file_name ): + return file_name + return urljoin( 'file:', pathname2url( file_name ) ) def UriToFilePath( uri ): + if IsJdtContentUri( uri ): + return uri + parsed_uri = urlparse( uri ) if parsed_uri.scheme != 'file': raise InvalidUriException( uri ) diff --git a/ycmd/responses.py b/ycmd/responses.py index de87a57f83..dc32575481 100644 --- a/ycmd/responses.py +++ b/ycmd/responses.py @@ -173,13 +173,24 @@ def BuildInlayHintsResponse( inlay_hints, errors = None ): } +def IsJdtContentUri( uri ): + return isinstance( uri, str ) and uri[ : 5 ] == "jdt:/" + + # location.column_number_ is a byte offset def BuildLocationData( location ): + filename = '' + + if IsJdtContentUri( filename ): + filename = location.filename_ + + if location.filename_: + filename = os.path.normpath( location.filename_ ) + return { 'line_num': location.line_number_, 'column_num': location.column_number_, - 'filepath': ( os.path.normpath( location.filename_ ) - if location.filename_ else '' ), + 'filepath': filename, } @@ -223,7 +234,10 @@ def __init__( self, line: int, column: int, filename: str ): self.line_number_ = line self.column_number_ = column if filename: - self.filename_ = os.path.abspath( filename ) + if IsJdtContentUri( filename ): + self.filename_ = filename + else: + self.filename_ = os.path.abspath( filename ) else: # When the filename passed (e.g. by a server) can't be recognized or # parsed, we send an empty filename. This at least allows the client to diff --git a/ycmd/tests/java/debug_info_test.py b/ycmd/tests/java/debug_info_test.py index e0c9440cac..b1fb85bae9 100644 --- a/ycmd/tests/java/debug_info_test.py +++ b/ycmd/tests/java/debug_info_test.py @@ -109,10 +109,16 @@ def test_DebugInfo( self, app ): } ), has_entries( { 'key': 'Settings', - 'value': json.dumps( - { 'bundles': [] }, - indent = 2, - sort_keys = True ) + 'value': json.dumps( { + 'bundles': [], + 'capabilities': { + 'definitionProvider': True + }, + 'extendedClientCapabilities': { + 'classFileContentsSupport': True + } + }, + indent = 2 ) } ), has_entries( { 'key': 'Startup Status', 'value': 'Ready' } ), @@ -144,6 +150,7 @@ def test_DebugInfo_ExtraConf_SettingsValid( self, app ): request_data = BuildRequest( filepath = filepath, filetype = 'java' ) + assert_that( app.post_json( '/debug_info', request_data ).json, has_entry( 'completer', has_entries( { @@ -169,10 +176,17 @@ def test_DebugInfo_ExtraConf_SettingsValid( self, app ): } ), has_entries( { 'key': 'Settings', - 'value': json.dumps( - { 'java.rename.enabled': False, 'bundles': [] }, - indent = 2, - sort_keys = True ) + 'value': json.dumps( { + 'bundles': [], + 'capabilities': { + 'definitionProvider': True + }, + 'extendedClientCapabilities': { + 'classFileContentsSupport': True + }, + 'java.rename.enabled': False + }, + indent = 2 ) } ), has_entries( { 'key': 'Startup Status', 'value': 'Ready' } ), diff --git a/ycmd/tests/language_server/language_server_completer_test.py b/ycmd/tests/language_server/language_server_completer_test.py index 8db91ea36b..bad72c4e46 100644 --- a/ycmd/tests/language_server/language_server_completer_test.py +++ b/ycmd/tests/language_server/language_server_completer_test.py @@ -47,8 +47,11 @@ RangeMatcher ) from ycmd.tests.language_server import IsolatedYcmd, PathToTestFile from ycmd import handlers, utils, responses +from ycmd.responses import IsJdtContentUri import os +MESSAGE_INITIALIZING = 'Server is initializing. Please wait.' + class MockCompleter( lsc.LanguageServerCompleter, DummyCompleter ): def __init__( self, custom_options = {} ): @@ -418,6 +421,15 @@ def Test( responses, command, exception, throws, *args ): } } + jdt_response = { + 'uri': 'jdt://contents/stuff/Member.class', + 'jdt_contents': 'mock contents', + 'range': { + 'start': { 'line': 0, 'character': 0 }, + 'end': { 'line': 0, 'character': 0 }, + } + } + goto_response = has_entries( { 'filepath': filepath, 'column_num': 1, @@ -441,6 +453,13 @@ def Test( responses, command, exception, throws, *args ): for response, goto_handlers, exception, throws in cases: Test( response, goto_handlers, exception, throws ) + with patch( + 'ycmd.completers.language_server.language_server_completer.' + 'LanguageServerCompleter.GetClassFileContents', + return_value = 'mock contents' ): + Test( [ { + 'result': jdt_response + } ], 'GoTo', equal_to( jdt_response ), False ) # All requests return an invalid URI. with patch( @@ -1576,3 +1595,43 @@ def test_LanguageServerCompleter_DistanceOfPointToRange_MultiLineRange( # Point to the right of range. # +1 because diags are half-open ranges. _Check_Distance( ( 3, 8 ), ( 0, 2 ), ( 3, 5 ) , 4 ) + + + @IsolatedYcmd() + def test_LanguageServerCompleter_GetClassFileContents_Success( self, app ): + completer = MockCompleter() + + with patch.object( completer, '_ServerIsInitialized', return_value = True ): + with patch.object( completer.GetConnection(), 'GetResponse', + return_value = { 'result': 'mock contents' } ) as \ + get_response: + request_data = { 'uri': 'jdt://test' } + contents = completer.GetClassFileContents( request_data ) + + assert_that( contents, equal_to( 'mock contents' ) ) + get_response.assert_called_once() + + + @IsolatedYcmd() + def test_LanguageServerCompleter_GetClassFileContents_Uninit( self, *args ): + completer = MockCompleter() + + with self.assertRaises( RuntimeError ) as context: + completer.GetClassFileContents( {} ) + + assert_that( str( context.exception ), equal_to( MESSAGE_INITIALIZING ) ) + + +class IsJdtContentUriTest( TestCase ): + + def test_IsJdtContentUri( self ): + for uri, result in [ + ( "jdt://example/class", True ), + ( "jdt:/contents/jdk.compiler", True ), + ( "file://example/class", False ), + ( "example/class", False ), + ( "jdt/example/class", False ), + ( 123, False ), + ]: + with self.subTest( uri = uri, result = result ): + self.assertEqual( IsJdtContentUri( uri ), result ) diff --git a/ycmd/tests/language_server/language_server_protocol_test.py b/ycmd/tests/language_server/language_server_protocol_test.py index 15c91a0001..918ce24069 100644 --- a/ycmd/tests/language_server/language_server_protocol_test.py +++ b/ycmd/tests/language_server/language_server_protocol_test.py @@ -153,6 +153,8 @@ def test_UriToFilePath_Unix( self ): equal_to( '/usr/local/test/test.test' ) ) assert_that( lsp.UriToFilePath( 'file:///usr/local/test/test.test' ), equal_to( '/usr/local/test/test.test' ) ) + assert_that( lsp.UriToFilePath( 'jdt://contents/Member.class' ), + equal_to( 'jdt://contents/Member.class' ) ) @WindowsOnly @@ -172,18 +174,24 @@ def test_UriToFilePath_Windows( self ): equal_to( 'C:\\usr\\local\\test\\test.test' ) ) assert_that( lsp.UriToFilePath( 'file:///c%3A/usr/local/test/test.test' ), equal_to( 'C:\\usr\\local\\test\\test.test' ) ) + assert_that( lsp.UriToFilePath( 'jdt://contents/Member.class' ), + equal_to( 'jdt://contents/Member.class' ) ) @UnixOnly def test_FilePathToUri_Unix( self ): assert_that( lsp.FilePathToUri( '/usr/local/test/test.test' ), equal_to( 'file:///usr/local/test/test.test' ) ) + assert_that( lsp.FilePathToUri( 'jdt://contents/Member.class' ), + equal_to( 'jdt://contents/Member.class' ) ) @WindowsOnly def test_FilePathToUri_Windows( self ): assert_that( lsp.FilePathToUri( 'C:\\usr\\local\\test\\test.test' ), equal_to( 'file:///C:/usr/local/test/test.test' ) ) + assert_that( lsp.FilePathToUri( 'jdt://contents/Member.class' ), + equal_to( 'jdt://contents/Member.class' ) ) def test_CodepointsToUTF16CodeUnitsAndReverse( self ): From 40dc16e51910530252a66efafd3c8cca07e9fe26 Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Tue, 15 Jul 2025 02:28:41 +0300 Subject: [PATCH 3/3] jdt-handling: move logic into JavaCompleter subclass 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 Helped-by: Boris Staletic Signed-off-by: Chris Sdogkos --- ycmd/completers/java/java_completer.py | 80 +++++++++++++++- ycmd/completers/java/java_utils.py | 50 ++++++++++ .../language_server_completer.py | 32 ------- .../language_server_protocol.py | 13 --- ycmd/responses.py | 20 +--- ycmd/tests/java/debug_info_test.py | 1 - .../language_server_completer_test.py | 92 +++++++++++++++---- .../language_server_protocol_test.py | 9 +- 8 files changed, 210 insertions(+), 87 deletions(-) create mode 100644 ycmd/completers/java/java_utils.py diff --git a/ycmd/completers/java/java_completer.py b/ycmd/completers/java/java_completer.py index 34f8c9538d..0102849a42 100644 --- a/ycmd/completers/java/java_completer.py +++ b/ycmd/completers/java/java_completer.py @@ -25,7 +25,9 @@ from collections import OrderedDict from ycmd import responses, utils -from ycmd.completers.language_server import language_server_completer +from ycmd.completers.java import java_utils +from ycmd.completers.language_server import ( language_server_protocol as lsp, + language_server_completer ) from ycmd.utils import LOGGER NO_DOCUMENTATION_MESSAGE = 'No documentation available for current context' @@ -666,3 +668,79 @@ def Hierarchy( self, request_data, args ): *language_server_completer._LspLocationToLocationAndDescription( request_data, item[ 'from' ] ) ) return result + + + def GoTo( self, request_data, handlers ): + """Issues a GoTo request for each handler in |handlers| until it returns + multiple locations or a location the cursor does not belong since the user + wants to jump somewhere else. If that's the last handler, the location is + returned anyway.""" + if not self.ServerIsReady(): + raise RuntimeError( 'Server is initializing. Please wait.' ) + + self._UpdateServerWithFileContents( request_data ) + + # flake8 doesn't like long lines so we have to split it up + def HandlerCondition( result, request_data ): + return result and \ + not language_server_completer._CursorInsideLocation( request_data, + result[ 0 ] ) + + result = [] + for handler in handlers: + new_result = self._GoToRequest( request_data, handler ) + if new_result: + result = new_result + if len( result ) > 1 or HandlerCondition( result, request_data ): + break + + if not result: + raise RuntimeError( 'Cannot jump to location' ) + + first_result = result[ 0 ] + if java_utils.IsJdtContentUri( first_result[ 'uri' ] ): + contents = self.GetClassFileContents( first_result ) + response = first_result.copy() + response[ 'jdt_contents' ] = str( contents ) + + return response + + return language_server_completer._LocationListToGoTo( request_data, result ) + + + def GoToSymbol( self, request_data, args ): + result = super().GoToSymbol( request_data, args ) + + def locations_filter( filepath ): + return filepath and not java_utils.IsJdtContentUri( filepath ) + + if isinstance( result, list ): + result = sorted( + filter( lambda loc: locations_filter( loc[ 'filepath' ] ), result ), + key = lambda s: ( s[ 'extra_data' ][ 'kind' ], + s[ 'extra_data' ][ 'name' ] ) + ) + + return result + + + def GetClassFileContents( self, request_data ): + """Retrieves the contents of a Java class file from the language server + using the provided request data, ensuring the server is initialized before + proceeding; raises a RuntimeError if the server is not ready, sends a + request to obtain the class file contents, and returns the result if + available.""" + if not self._ServerIsInitialized(): + raise RuntimeError( 'Server is initializing. Please wait.' ) + + request_id = self.GetConnection().NextRequestId() + response = self.GetConnection().GetResponse( + request_id, + lsp.BuildRequest( request_id, 'java/classFileContents', { + 'uri': request_data[ 'uri' ] + } ), + language_server_completer.REQUEST_TIMEOUT_COMMAND ) + + result = response[ 'result' ] + if result: + return result diff --git a/ycmd/completers/java/java_utils.py b/ycmd/completers/java/java_utils.py new file mode 100644 index 0000000000..057c732867 --- /dev/null +++ b/ycmd/completers/java/java_utils.py @@ -0,0 +1,50 @@ +# Copyright (C) 2013-2020 ycmd contributors +# +# This file is part of ycmd. +# +# ycmd is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ycmd is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with ycmd. If not, see . + +import os +from urllib.parse import urljoin, urlparse, unquote +from urllib.request import pathname2url, url2pathname +from ..language_server.language_server_protocol import InvalidUriException + + +def FilePathToUri( file_name ): + if IsJdtContentUri( file_name ): + return file_name + + return urljoin( 'file:', pathname2url( file_name ) ) + + +def UriToFilePath( uri ): + if IsJdtContentUri( uri ): + return uri + + parsed_uri = urlparse( uri ) + if parsed_uri.scheme != 'file': + raise InvalidUriException( uri ) + + # url2pathname doesn't work as expected when uri.path is percent-encoded and + # is a windows path for ex: + # url2pathname('/C%3a/') == 'C:\\C:' + # whereas + # url2pathname('/C:/') == 'C:\\' + # Therefore first unquote pathname. + pathname = unquote( parsed_uri.path ) + return os.path.abspath( url2pathname( pathname ) ) + + +def IsJdtContentUri( uri ): + return isinstance( uri, str ) and uri[ : 5 ] == "jdt:/" diff --git a/ycmd/completers/language_server/language_server_completer.py b/ycmd/completers/language_server/language_server_completer.py index 3b6bcf5700..d8a7ee7222 100644 --- a/ycmd/completers/language_server/language_server_completer.py +++ b/ycmd/completers/language_server/language_server_completer.py @@ -2618,15 +2618,6 @@ def GoTo( self, request_data, handlers ): if not result: raise RuntimeError( 'Cannot jump to location' ) - - first_result = result[ 0 ] - if responses.IsJdtContentUri( first_result[ 'uri' ] ): - contents = self.GetClassFileContents( first_result ) - response = first_result.copy() - response[ 'jdt_contents' ] = str( contents ) - - return response - return _LocationListToGoTo( request_data, result ) @@ -2672,26 +2663,6 @@ def GoToDocumentOutline( self, request_data ): return _LspSymbolListToGoTo( request_data, result ) - def GetClassFileContents( self, request_data ): - """Retrieves the contents of a Java class file from the language server - using the provided request data, ensuring the server is initialized before - proceeding; raises a RuntimeError if the server is not ready, sends a - request to obtain the class file contents, and returns the result if - available.""" - if not self._ServerIsInitialized(): - raise RuntimeError( 'Server is initializing. Please wait.' ) - - request_id = self.GetConnection().NextRequestId() - response = self.GetConnection().GetResponse( - request_id, - lsp.ClassFileContents( request_id, request_data ), - REQUEST_TIMEOUT_COMMAND ) - - result = response[ 'result' ] - if result: - return result - - def InitialHierarchy( self, request_data, args ): if not self.ServerIsReady(): raise RuntimeError( 'Server is initializing. Please wait.' ) @@ -3439,9 +3410,6 @@ def _LspSymbolListToGoTo( request_data, symbols ): sorted( symbols, key = lambda s: ( s[ 'kind' ], s[ 'name' ] ) ) ] - locations = [ location for location in locations - if not responses.IsJdtContentUri( location[ 'filepath' ] ) ] - if not locations: raise RuntimeError( "Symbol not found" ) elif len( locations ) == 1: diff --git a/ycmd/completers/language_server/language_server_protocol.py b/ycmd/completers/language_server/language_server_protocol.py index af328bac92..365f8a04c5 100644 --- a/ycmd/completers/language_server/language_server_protocol.py +++ b/ycmd/completers/language_server/language_server_protocol.py @@ -26,7 +26,6 @@ ToBytes, ToUnicode, UpdateDict ) -from ycmd.responses import IsJdtContentUri Error = collections.namedtuple( 'RequestError', [ 'code', 'reason' ] ) @@ -643,12 +642,6 @@ def Position( line_num, line_value, column_codepoint ): } -def ClassFileContents( request_id, request_data ): - return BuildRequest( request_id, 'java/classFileContents', { - 'uri': request_data[ 'uri' ] - } ) - - def PrepareHierarchy( request_id, request_data, kind ): return BuildRequest( request_id, f'textDocument/prepare{ kind }Hierarchy', { 'textDocument': { @@ -760,16 +753,10 @@ def InlayHints( request_id, request_data ): def FilePathToUri( file_name ): - if IsJdtContentUri( file_name ): - return file_name - return urljoin( 'file:', pathname2url( file_name ) ) def UriToFilePath( uri ): - if IsJdtContentUri( uri ): - return uri - parsed_uri = urlparse( uri ) if parsed_uri.scheme != 'file': raise InvalidUriException( uri ) diff --git a/ycmd/responses.py b/ycmd/responses.py index dc32575481..de87a57f83 100644 --- a/ycmd/responses.py +++ b/ycmd/responses.py @@ -173,24 +173,13 @@ def BuildInlayHintsResponse( inlay_hints, errors = None ): } -def IsJdtContentUri( uri ): - return isinstance( uri, str ) and uri[ : 5 ] == "jdt:/" - - # location.column_number_ is a byte offset def BuildLocationData( location ): - filename = '' - - if IsJdtContentUri( filename ): - filename = location.filename_ - - if location.filename_: - filename = os.path.normpath( location.filename_ ) - return { 'line_num': location.line_number_, 'column_num': location.column_number_, - 'filepath': filename, + 'filepath': ( os.path.normpath( location.filename_ ) + if location.filename_ else '' ), } @@ -234,10 +223,7 @@ def __init__( self, line: int, column: int, filename: str ): self.line_number_ = line self.column_number_ = column if filename: - if IsJdtContentUri( filename ): - self.filename_ = filename - else: - self.filename_ = os.path.abspath( filename ) + self.filename_ = os.path.abspath( filename ) else: # When the filename passed (e.g. by a server) can't be recognized or # parsed, we send an empty filename. This at least allows the client to diff --git a/ycmd/tests/java/debug_info_test.py b/ycmd/tests/java/debug_info_test.py index b1fb85bae9..65a01e7bc6 100644 --- a/ycmd/tests/java/debug_info_test.py +++ b/ycmd/tests/java/debug_info_test.py @@ -150,7 +150,6 @@ def test_DebugInfo_ExtraConf_SettingsValid( self, app ): request_data = BuildRequest( filepath = filepath, filetype = 'java' ) - assert_that( app.post_json( '/debug_info', request_data ).json, has_entry( 'completer', has_entries( { diff --git a/ycmd/tests/language_server/language_server_completer_test.py b/ycmd/tests/language_server/language_server_completer_test.py index bad72c4e46..fffc8de088 100644 --- a/ycmd/tests/language_server/language_server_completer_test.py +++ b/ycmd/tests/language_server/language_server_completer_test.py @@ -47,7 +47,8 @@ RangeMatcher ) from ycmd.tests.language_server import IsolatedYcmd, PathToTestFile from ycmd import handlers, utils, responses -from ycmd.responses import IsJdtContentUri +from ycmd.completers.java.java_utils import IsJdtContentUri +from ycmd.completers.java.java_completer import JavaCompleter import os MESSAGE_INITIALIZING = 'Server is initializing. Please wait.' @@ -93,6 +94,15 @@ def GetServerName( self ): return 'mock_completer' +class MockJavaCompleter( MockCompleter, JavaCompleter ): + def Language( self ): + return 'java' + + + def GetServerName( self ): + return 'mock_java_completer' + + def _TupleToLSPRange( tuple ): return { 'line': tuple[ 0 ], 'character': tuple[ 1 ] } @@ -368,6 +378,66 @@ def test_LanguageServerCompleter_Initialise_Shutdown( self, app ): assert_that( completer.ServerIsReady(), equal_to( False ) ) + @IsolatedYcmd() + def test_LanguageJavaServerCompleter_GoTo( self, app ): + completer = MockJavaCompleter() + completer._server_capabilities = { + 'definitionProvider': True, + 'declarationProvider': True, + 'typeDefinitionProvider': True, + 'implementationProvider': True, + 'referencesProvider': True + } + + if utils.OnWindows(): + filepath = 'C:\\test.test' + else: + filepath = '/test.test' + + contents = 'line1\nline2\nline3' + + request_data = RequestWrap( BuildRequest( + filetype = 'ycmtest', + filepath = filepath, + contents = contents, + line_num = 2, + column_num = 3 + ) ) + + jdt_response = { + 'uri': 'jdt://contents/stuff/Member.class', + 'jdt_contents': 'mock contents', + 'range': { + 'start': { 'line': 0, 'character': 0 }, + 'end': { 'line': 0, 'character': 0 }, + } + } + + @patch.object( completer, 'ServerIsReady', return_value = True ) + def Test( responses, command, exception, throws, *args ): + with patch.object( completer.GetConnection(), + 'GetResponse', + side_effect = responses ): + if throws: + assert_that( + calling( completer.OnUserCommand ).with_args( [ command ], + request_data ), + raises( exception ) + ) + else: + result = completer.OnUserCommand( [ command ], request_data ) + assert_that( result, exception ) + + + with patch( + 'ycmd.completers.java.java_completer.' + 'JavaCompleter.GetClassFileContents', + return_value = 'mock contents' ): + Test( [ { + 'result': jdt_response + } ], 'GoTo', equal_to( jdt_response ), False ) + + @IsolatedYcmd() def test_LanguageServerCompleter_GoTo( self, app ): if utils.OnWindows(): @@ -421,15 +491,6 @@ def Test( responses, command, exception, throws, *args ): } } - jdt_response = { - 'uri': 'jdt://contents/stuff/Member.class', - 'jdt_contents': 'mock contents', - 'range': { - 'start': { 'line': 0, 'character': 0 }, - 'end': { 'line': 0, 'character': 0 }, - } - } - goto_response = has_entries( { 'filepath': filepath, 'column_num': 1, @@ -453,13 +514,6 @@ def Test( responses, command, exception, throws, *args ): for response, goto_handlers, exception, throws in cases: Test( response, goto_handlers, exception, throws ) - with patch( - 'ycmd.completers.language_server.language_server_completer.' - 'LanguageServerCompleter.GetClassFileContents', - return_value = 'mock contents' ): - Test( [ { - 'result': jdt_response - } ], 'GoTo', equal_to( jdt_response ), False ) # All requests return an invalid URI. with patch( @@ -1599,7 +1653,7 @@ def test_LanguageServerCompleter_DistanceOfPointToRange_MultiLineRange( @IsolatedYcmd() def test_LanguageServerCompleter_GetClassFileContents_Success( self, app ): - completer = MockCompleter() + completer = MockJavaCompleter() with patch.object( completer, '_ServerIsInitialized', return_value = True ): with patch.object( completer.GetConnection(), 'GetResponse', @@ -1614,7 +1668,7 @@ def test_LanguageServerCompleter_GetClassFileContents_Success( self, app ): @IsolatedYcmd() def test_LanguageServerCompleter_GetClassFileContents_Uninit( self, *args ): - completer = MockCompleter() + completer = MockJavaCompleter() with self.assertRaises( RuntimeError ) as context: completer.GetClassFileContents( {} ) diff --git a/ycmd/tests/language_server/language_server_protocol_test.py b/ycmd/tests/language_server/language_server_protocol_test.py index 918ce24069..ff5f2ff2d5 100644 --- a/ycmd/tests/language_server/language_server_protocol_test.py +++ b/ycmd/tests/language_server/language_server_protocol_test.py @@ -16,6 +16,7 @@ # along with ycmd. If not, see . from ycmd.completers.language_server import language_server_protocol as lsp +from ycmd.completers.java import java_utils from hamcrest import assert_that, equal_to, calling, is_not, raises from unittest import TestCase from ycmd.tests.test_utils import UnixOnly, WindowsOnly @@ -153,7 +154,7 @@ def test_UriToFilePath_Unix( self ): equal_to( '/usr/local/test/test.test' ) ) assert_that( lsp.UriToFilePath( 'file:///usr/local/test/test.test' ), equal_to( '/usr/local/test/test.test' ) ) - assert_that( lsp.UriToFilePath( 'jdt://contents/Member.class' ), + assert_that( java_utils.UriToFilePath( 'jdt://contents/Member.class' ), equal_to( 'jdt://contents/Member.class' ) ) @@ -174,7 +175,7 @@ def test_UriToFilePath_Windows( self ): equal_to( 'C:\\usr\\local\\test\\test.test' ) ) assert_that( lsp.UriToFilePath( 'file:///c%3A/usr/local/test/test.test' ), equal_to( 'C:\\usr\\local\\test\\test.test' ) ) - assert_that( lsp.UriToFilePath( 'jdt://contents/Member.class' ), + assert_that( java_utils.UriToFilePath( 'jdt://contents/Member.class' ), equal_to( 'jdt://contents/Member.class' ) ) @@ -182,7 +183,7 @@ def test_UriToFilePath_Windows( self ): def test_FilePathToUri_Unix( self ): assert_that( lsp.FilePathToUri( '/usr/local/test/test.test' ), equal_to( 'file:///usr/local/test/test.test' ) ) - assert_that( lsp.FilePathToUri( 'jdt://contents/Member.class' ), + assert_that( java_utils.FilePathToUri( 'jdt://contents/Member.class' ), equal_to( 'jdt://contents/Member.class' ) ) @@ -190,7 +191,7 @@ def test_FilePathToUri_Unix( self ): def test_FilePathToUri_Windows( self ): assert_that( lsp.FilePathToUri( 'C:\\usr\\local\\test\\test.test' ), equal_to( 'file:///C:/usr/local/test/test.test' ) ) - assert_that( lsp.FilePathToUri( 'jdt://contents/Member.class' ), + assert_that( java_utils.FilePathToUri( 'jdt://contents/Member.class' ), equal_to( 'jdt://contents/Member.class' ) )