From 35022472e5a6a794b3fc7b3efc2724f730eed0ea Mon Sep 17 00:00:00 2001 From: Chris Sdogkos Date: Thu, 10 Jul 2025 14:22:41 +0300 Subject: [PATCH] jdt.ls: properly handle URI by showing contents Thanks to commit ad64e63b (jdt.ls: support jumping to `jdt://` URIs with GoTo, 2025-07-07) on the ycmd repository, it has the capability to properly handle `jdt://` URIs that are predominantly returned when a user executes the `:YcmCompleter GoTo` command or some variation of it. Introduce _GetClassNameFromJdtURI() which gets the classname from a JDT URI (e.g. Member.class) using regular expression magic so that ycm displays it on the status bar of the temporary buffer which is made to see the contents of that class file. On _HandleGotoResponse(), add a new condition which checks to see if there is a key called 'jdt_contents' in the JSON response that ycmd provides. If that condition is true, create a temporary readonly buffer, set the buffer name to the file name that is received at an earlier stage from the 'uri' key, and set the contents of the buffer to match what is received from the key 'jdt_contents'. Finally, set the syntax highlighting to 'java' and jump the cursor to the line and column specified from the JSON (specifically found in the 'range' key). Note that the temporary buffer made does _NOT_ get its contents from any file - no new temporary file is created or deleted during this process, everything is handled inside vim. Signed-off-by: Chris Sdogkos --- python/ycm/client/command_request.py | 30 ++++++++++++ .../ycm/tests/client/command_request_test.py | 49 +++++++++++++++++++ python/ycm/vimsupport.py | 41 ++++++++++++++++ 3 files changed, 120 insertions(+) diff --git a/python/ycm/client/command_request.py b/python/ycm/client/command_request.py index a9045686d8..1362e2fe45 100644 --- a/python/ycm/client/command_request.py +++ b/python/ycm/client/command_request.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +import re from ycm.client.base_request import ( BaseRequest, BuildRequestData, BuildRequestDataForLocation ) @@ -142,11 +143,40 @@ def StringResponse( self ): return "" + def _GetClassNameFromJdtURI( self, uri ): + if not isinstance( uri, str ): + return None + + matches = re.findall( r'([^\\(]+\.class)', uri ) + if matches: + return matches[ -1 ] + return None + + def _HandleGotoResponse( self, buffer_command, modifiers ): if isinstance( self._response, list ): vimsupport.SetQuickFixList( [ vimsupport.BuildQfListItem( x ) for x in self._response ] ) vimsupport.OpenQuickFixList( focus = True, autoclose = True ) + elif 'jdt_contents' in self._response: + class_name = self._GetClassNameFromJdtURI( self._response.get( 'uri' ) ) + + range = self._response.get( 'range' ) + range_start = range.get( 'start' ) + + line_num = range_start[ 'line' ] if range_start else 0 + column_num = range_start[ 'character' ] if range_start else 0 + + contents = self._response.get( 'jdt_contents' ) + + if not contents: + raise RuntimeError( 'JDT contents not available.' ) + + vimsupport.CreateTemporaryReadonlyBuffer( contents=contents, + buffer_name=class_name, + syntax='java', + line=line_num, + column=column_num ) elif self._response.get( 'file_only' ): vimsupport.JumpToLocation( self._response[ 'filepath' ], None, diff --git a/python/ycm/tests/client/command_request_test.py b/python/ycm/tests/client/command_request_test.py index 368b87c4ec..2c2945845c 100644 --- a/python/ycm/tests/client/command_request_test.py +++ b/python/ycm/tests/client/command_request_test.py @@ -51,12 +51,35 @@ def GoToListTest( command, response ): assert_that( open_qf_list.called ) +def GoToJdt( command, response ): + with patch( 'ycm.vimsupport.CreateTemporaryReadonlyBuffer' ) as temp_buffer: + request = CommandRequest( [ command ] ) + request._response = response + request.RunPostCommandActionsIfNeeded( '' ) + temp_buffer.assert_called_once_with( + contents=response[ 'jdt_contents' ], + buffer_name=JDT_GOTO[ 'uri' ], + syntax='java', + line=JDT_GOTO[ 'range' ][ 'start' ][ 'line' ], + column=JDT_GOTO[ 'range' ][ 'start' ][ 'character' ] ) + + BASIC_GOTO = { 'filepath': 'test', 'line_num': 10, 'column_num': 100, } +JDT_GOTO = { + 'jdt_contents': 'public class Member {}', + 'uri': 'jdt://contents/stuff/whatever/Member.class', + 'range': { + 'start': { + 'line': 1, + 'character': 5 + } + } +} BASIC_FIXIT = { 'fixits': [ { @@ -298,6 +321,7 @@ def test_GoTo_Single( self ): for test, command, response in [ [ GoToTest, 'AnythingYouLike', BASIC_GOTO ], [ GoToTest, 'GoTo', BASIC_GOTO ], + [ GoToJdt, 'GoTo', JDT_GOTO ], [ GoToTest, 'FindAThing', BASIC_GOTO ], [ GoToTest, 'FixItGoto', BASIC_GOTO ], [ GoToListTest, 'AnythingYouLike', [ BASIC_GOTO ] ], @@ -306,3 +330,28 @@ def test_GoTo_Single( self ): ]: with self.subTest( test = test, command = command, response = response ): test( command, response ) + + +class JdtUriParsingTest( TestCase ): + def setUp( self ): + self.cmd_req = CommandRequest( [] ) + + def test_get_class_name_from_jdt_uri_with_valid_uri( self ): + uri = "jdt://contents/JDA-5.6.1.jar/net.dv8tion.jda.api."\ + "entities/Member.class?\\u003dapplication/%5C/Users%5C/user"\ + "%5C/.gradle%5C/caches%5C/modules-2%5C/files-2.1%5C/net."\ + "dv8tion%5C/JDA%5C/5.6.1%5C/e69e9bf2049f96bcad99e0bbe4"\ + "ddce9c67947cf6%5C/JDA-5.6.1.jar\\u003d/gradle_used_by_"\ + "scope\\u003d/main,test\\u003d/%3Cnet.dv8tion.jda.api.en"\ + "tities(Member.class" + result = self.cmd_req._GetClassNameFromJdtURI( uri ) + self.assertEqual( result, "Member.class" ) + + def test_get_class_name_from_jdt_uri_with_no_class( self ): + uri = "jdt://contents/com/example/Member" + result = self.cmd_req._GetClassNameFromJdtURI( uri ) + self.assertIsNone( result ) + + def test_get_class_name_from_jdt_uri_with_non_string( self ): + result = self.cmd_req._GetClassNameFromJdtURI( None ) + self.assertIsNone( result ) diff --git a/python/ycm/vimsupport.py b/python/ycm/vimsupport.py index 2a29b1ede1..ef80fcaa53 100644 --- a/python/ycm/vimsupport.py +++ b/python/ycm/vimsupport.py @@ -1497,3 +1497,44 @@ def BuildQfListItem( goto_data_item ): qf_item[ 'col' ] = goto_data_item[ 'column_num' ] return qf_item + + +def CreateTemporaryReadonlyBuffer( contents=None, + buffer_name='[YCM-Temp-Readonly]', + syntax=None, + line=None, + column=None ): + """ + Create a new temporary readonly buffer in Vim, optionally filled + with contents. The buffer will not be associated with a file and + will be readonly and unmodifiable. + """ + vim.command( 'enew' ) + buf = vim.current.buffer + + vim.command( f"file { buffer_name }" ) + + if contents: + buf.options[ 'modifiable' ] = True + buf[ : ] = contents.splitlines() + buf.options[ 'modifiable' ] = False + + buf.options[ 'readonly' ] = True + buf.options[ 'modifiable' ] = False + buf.options[ 'bufhidden' ] = 'wipe' + buf.options[ 'buftype' ] = 'nofile' + buf.options[ 'buflisted' ] = False + + vim.command( 'setlocal noswapfile' ) + vim.command( 'setlocal nobuflisted' ) + vim.command( 'setlocal nobuflisted' ) + + if syntax: + vim.command( f'set syntax={ syntax }' ) + + if line is not None and column is not None: + vim.current.window.cursor = ( line + 1, column ) + + vim.command( 'normal! zz' ) + + return buf