Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion ycmd/completers/java/java_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -328,6 +330,12 @@ def __init__( self, user_options ):
def DefaultSettings( self, request_data ):
return {
'bundles': self._bundles,
'capabilities': {
'definitionProvider': True
},
'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
Expand Down Expand Up @@ -660,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
50 changes: 50 additions & 0 deletions ycmd/completers/java/java_utils.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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:/"
29 changes: 21 additions & 8 deletions ycmd/tests/java/debug_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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' } ),
Expand Down Expand Up @@ -169,10 +175,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' } ),
Expand Down
113 changes: 113 additions & 0 deletions ycmd/tests/language_server/language_server_completer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,12 @@
RangeMatcher )
from ycmd.tests.language_server import IsolatedYcmd, PathToTestFile
from ycmd import handlers, utils, responses
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.'


class MockCompleter( lsc.LanguageServerCompleter, DummyCompleter ):
def __init__( self, custom_options = {} ):
Expand Down Expand Up @@ -90,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 ] }

Expand Down Expand Up @@ -365,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():
Expand Down Expand Up @@ -1576,3 +1649,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 = MockJavaCompleter()

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 = MockJavaCompleter()

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 )
9 changes: 9 additions & 0 deletions ycmd/tests/language_server/language_server_protocol_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# along with ycmd. If not, see <http://www.gnu.org/licenses/>.

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
Expand Down Expand Up @@ -153,6 +154,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( java_utils.UriToFilePath( 'jdt://contents/Member.class' ),
equal_to( 'jdt://contents/Member.class' ) )


@WindowsOnly
Expand All @@ -172,18 +175,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( java_utils.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( java_utils.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( java_utils.FilePathToUri( 'jdt://contents/Member.class' ),
equal_to( 'jdt://contents/Member.class' ) )


def test_CodepointsToUTF16CodeUnitsAndReverse( self ):
Expand Down
Loading