Skip to content

Commit

Permalink
Merge pull request #1454 from bstaletic/ra
Browse files Browse the repository at this point in the history
[READY] Migrate from rls to rust-analyzer
  • Loading branch information
mergify[bot] authored Jul 10, 2020
2 parents 0abcfaf + e8ad44e commit c3581c3
Show file tree
Hide file tree
Showing 18 changed files with 347 additions and 240 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ third_party/tsserver
third_party/clangd

# Rust
third_party/rls
third_party/rust-analyzer
ycmd/tests/rust/testdata/common/target
ycmd/tests/rust/testdata/formatting/target

Expand Down
9 changes: 6 additions & 3 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@

TSSERVER_VERSION = '3.8.3'

RUST_TOOLCHAIN = 'nightly-2020-04-17'
RLS_DIR = p.join( DIR_OF_THIRD_PARTY, 'rls' )
RUST_TOOLCHAIN = 'nightly-2020-07-08'
RLS_DIR = p.join( DIR_OF_THIRD_PARTY, 'rust-analyzer' )

BUILD_ERROR_MESSAGE = (
'ERROR: the build failed.\n\n'
Expand Down Expand Up @@ -904,7 +904,10 @@ def EnableRustCompleter( switches ):
env = new_env,
quiet = switches.quiet )

for component in [ 'rls', 'rust-analysis', 'rust-src' ]:
for component in [ 'rust-src',
'rust-analyzer-preview',
'rustfmt',
'clippy' ]:
CheckCall( [ rustup, 'component', 'add', component,
'--toolchain', RUST_TOOLCHAIN ],
env = new_env,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1306,6 +1306,7 @@ def ComputeSignaturesInner( self, request_data ):
arg[ 'label' ] = [
utils.CodepointOffsetToByteOffset( sig_label, begin ),
utils.CodepointOffsetToByteOffset( sig_label, end ) ]
result.setdefault( 'activeParameter', 0 )
return result


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ def Initialize( request_id, project_directory, settings ):
'rootUri': FilePathToUri( project_directory ),
'initializationOptions': settings,
'capabilities': {
'experimental': { 'statusNotification': True },
'workspace': {
'applyEdit': True,
'didChangeWatchedFiles': {
Expand Down
184 changes: 93 additions & 91 deletions ycmd/completers/rust/rust_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
from ycmd.utils import LOGGER, re


LOGFILE_FORMAT = 'rls_'
RLS_BIN_DIR = os.path.abspath(
LOGFILE_FORMAT = 'ra_'
RUST_ROOT = os.path.abspath(
os.path.join( os.path.dirname( __file__ ), '..', '..', '..', 'third_party',
'rls', 'bin' ) )
RUSTC_EXECUTABLE = utils.FindExecutable( os.path.join( RLS_BIN_DIR, 'rustc' ) )
RLS_EXECUTABLE = utils.FindExecutable( os.path.join( RLS_BIN_DIR, 'rls' ) )
RLS_VERSION_REGEX = re.compile( r'^rls (?P<version>.*)$' )
'rust-analyzer' ) )
RA_BIN_DIR = os.path.join( RUST_ROOT, 'bin' )
RUSTC_EXECUTABLE = utils.FindExecutable( os.path.join( RA_BIN_DIR, 'rustc' ) )
RA_EXECUTABLE = utils.FindExecutable( os.path.join(
RA_BIN_DIR, 'rust-analyzer' ) )
RA_VERSION_REGEX = re.compile( r'^rust-analyzer (?P<version>.*)$' )


def _GetCommandOutput( command ):
Expand All @@ -41,154 +43,154 @@ def _GetCommandOutput( command ):
stderr = PIPE ).communicate()[ 0 ].rstrip() )


def _GetRlsVersion( rls_path ):
rls_version = _GetCommandOutput( [ rls_path, '--version' ] )
match = RLS_VERSION_REGEX.match( rls_version )
def _GetRAVersion( ra_path ):
ra_version = _GetCommandOutput( [ ra_path, '--version' ] )
match = RA_VERSION_REGEX.match( ra_version )
if not match:
LOGGER.error( 'Cannot parse Rust Language Server version: %s', rls_version )
LOGGER.error( 'Cannot parse Rust Language Server version: %s', ra_version )
return None
return match.group( 'version' )


def ShouldEnableRustCompleter( user_options ):
if ( user_options[ 'rls_binary_path' ] and
not user_options[ 'rustc_binary_path' ] ):
LOGGER.error( 'Not using Rust completer: RUSTC not specified' )
return False

rls = utils.FindExecutableWithFallback( user_options[ 'rls_binary_path' ],
RLS_EXECUTABLE )
if not rls:
LOGGER.error( 'Not using Rust completer: no RLS executable found at %s',
rls )
return False
LOGGER.info( 'Using Rust completer' )
return True
if ( 'rls_binary_path' in user_options and
not user_options[ 'rust_toolchain_root' ] ):
LOGGER.warning( 'rls_binary_path detected. '
'Did you mean rust_toolchain_root?' )

if user_options[ 'rust_toolchain_root' ]:
# Listen to what the user wanted to use
ra = os.path.join( user_options[ 'rust_toolchain_root' ],
'bin', 'rust-analyzer' )
if not utils.FindExecutable( ra ):
LOGGER.error( 'Not using Rust completer: no rust-analyzer '
'executable found at %s', ra )
return False
else:
return True
else:
return bool( utils.FindExecutable( RA_EXECUTABLE ) )


class RustCompleter( language_server_completer.LanguageServerCompleter ):
def __init__( self, user_options ):
super().__init__( user_options )
self._rls_path = utils.FindExecutableWithFallback(
user_options[ 'rls_binary_path' ],
RLS_EXECUTABLE )
self._rustc_path = utils.FindExecutableWithFallback(
user_options[ 'rustc_binary_path' ],
RUSTC_EXECUTABLE )
if user_options[ 'rust_toolchain_root' ]:
self._rust_root = user_options[ 'rust_toolchain_root' ]
else:
self._rust_root = os.path.dirname( os.path.dirname( RA_EXECUTABLE ) )
self._ra_path = utils.FindExecutable(
os.path.join( self._rust_root, 'bin', 'rust-analyzer' ) )


def _Reset( self ):
self._server_progress = 'Not started'
super()._Reset()
self._server_progress = {}


def GetServerName( self ):
return 'Rust Language Server'


def GetCommandLine( self ):
return [ self._rls_path ]
return [ self._ra_path ]


def GetServerEnvironment( self ):
env = os.environ.copy()
env[ 'RUSTC' ] = self._rustc_path
old_path = env[ 'PATH' ]
ra_bin_dir = os.path.join( self._rust_root, 'bin' )
env[ 'PATH' ] = ra_bin_dir + os.pathsep + old_path
if LOGGER.isEnabledFor( logging.DEBUG ):
env[ 'RUST_LOG' ] = 'rls=trace'
env[ 'RUST_BACKTRACE' ] = '1'
env[ 'RA_LOG' ] = 'rust_analyzer=trace'
return env


def GetProjectRootFiles( self ):
# Without LSP workspaces support, RLS relies on the rootUri to detect a
# Without LSP workspaces support, RA relies on the rootUri to detect a
# project.
# TODO: add support for LSP workspaces to allow users to change project
# without having to restart RLS.
# without having to restart RA.
return [ 'Cargo.toml' ]



def ServerIsReady( self ):
# Assume RLS is ready once building and indexing are done.
# See
# https://github.com/rust-lang/rls/blob/master/contributing.md#rls-to-lsp-client
# for detail on the progress steps.
return ( super().ServerIsReady() and
self._server_progress and
set( self._server_progress.values() ) == { 'building done',
'indexing done' } )
self._server_progress in [ 'invalid', 'ready' ] )


def SupportedFiletypes( self ):
return [ 'rust' ]


def GetTriggerCharacters( self, server_trigger_characters ):
# The trigger characters supplied by RLS ('.' and ':') are worse than ycmd's
# The trigger characters supplied by RA ('.' and ':') are worse than ycmd's
# own semantic triggers ('.' and '::') so we ignore them.
return []


def ExtraDebugItems( self, request_data ):
project_state = ', '.join(
set( self._server_progress.values() ) ).capitalize()
return [
responses.DebugInfoItem( 'Project State', project_state ),
responses.DebugInfoItem( 'Version', _GetRlsVersion( self._rls_path ) ),
responses.DebugInfoItem( 'RUSTC', self._rustc_path )
responses.DebugInfoItem( 'Project State', self._server_progress ),
responses.DebugInfoItem( 'Version', _GetRAVersion( self._ra_path ) ),
responses.DebugInfoItem( 'Rust Root', self._rust_root )
]


def _ShouldResolveCompletionItems( self ):
# RLS tells us that it can resolve a completion but there is no point since
# no additional information is returned.
return False


def HandleNotificationInPollThread( self, notification ):
# TODO: the building status is currently displayed in the debug info. We
# should notify the client about it through a special status/progress
# message.
if notification[ 'method' ] == 'window/progress':
params = notification[ 'params' ]
progress_id = params[ 'id' ]
message = params[ 'title' ].lower()
if not params[ 'done' ]:
if params[ 'message' ]:
message += ' ' + params[ 'message' ]
if params[ 'percentage' ]:
message += ' ' + params[ 'percentage' ]
else:
message += ' done'

with self._server_info_mutex:
self._server_progress[ progress_id ] = message
if notification[ 'method' ] == 'rust-analyzer/status':
if self._server_progress not in [ 'invalid', 'ready' ]:
self._server_progress = notification[ 'params' ]

super().HandleNotificationInPollThread( notification )


def GetType( self, request_data ):
hover_response = self.GetHoverResponse( request_data )
def ConvertNotificationToMessage( self, request_data, notification ):
if notification[ 'method' ] == 'rust-analyzer/status':
message = notification[ 'params' ]
if message != 'invalid': # RA produces a better message for `invalid`
return responses.BuildDisplayMessageResponse(
'Initializing Rust completer: {}'.format( message ) )
return super().ConvertNotificationToMessage( request_data, notification )

for item in hover_response:
if isinstance( item, dict ) and 'value' in item:
return responses.BuildDisplayMessageResponse( item[ 'value' ] )

raise RuntimeError( 'Unknown type.' )
def GetType( self, request_data ):
try:
hover_response = self.GetHoverResponse( request_data )[ 'value' ]
except language_server_completer.NoHoverInfoException:
raise RuntimeError( 'Unknown type.' )

# rust-analyzer's hover format looks like this:
#
# ```rust
# namespace
# ```
#
# ```rust
# type info
# ```
#
# ___
# docstring
#
# To extract the type info, we take everything up to `___` line,
# then find the last occurence of "```" as the end index and "```rust"
# as the start index and return the slice.
hover_response = hover_response.split( '\n___\n', 2 )[ 0 ]
start = hover_response.rfind( '```rust\n' ) + len( '```rust\n' )
end = hover_response.rfind( '\n```' )
return responses.BuildDisplayMessageResponse( hover_response[ start:end ] )


def GetDoc( self, request_data ):
hover_response = self.GetHoverResponse( request_data )

# RLS returns a list that may contain the following elements:
# - a documentation string;
# - a documentation url;
# - [{language:rust, value:<type info>}].

try:
hover_response = self.GetHoverResponse( request_data )
except language_server_completer.NoHoverInfoException:
raise RuntimeError( 'No documentation available.' )

# Strips all empty lines and lines starting with "```" to make the hover
# response look like plain text. For the format, see the comment in GetType.
lines = hover_response[ 'value' ].split( '\n' )
documentation = '\n'.join(
[ item.strip() for item in hover_response if isinstance( item, str ) ] )

if not documentation:
raise RuntimeError( 'No documentation available for current context.' )

line for line in lines if line and not line.startswith( '```' ) ).strip()
return responses.BuildDetailedInfoResponse( documentation )
3 changes: 1 addition & 2 deletions ycmd/default_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
"disable_signature_help": 0,
"gopls_binary_path": "",
"gopls_args": [],
"rls_binary_path": "",
"rustc_binary_path": "",
"rust_toolchain_root": "",
"tsserver_binary_path": "",
"roslyn_binary_path": "",
"mono_binary_path": ""
Expand Down
4 changes: 2 additions & 2 deletions ycmd/tests/rust/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ def set_up_shared_app():
shared_app = SetUpApp()
with IgnoreExtraConfOutsideTestsFolder():
StartRustCompleterServerInDirectory( shared_app,
PathToTestFile( 'common', 'src' ) )
PathToTestFile( 'common' ) )
yield
StopCompleterServer( shared_app, 'rust' )


def StartRustCompleterServerInDirectory( app, directory ):
app.post_json( '/event_notification',
BuildRequest(
filepath = os.path.join( directory, 'main.rs' ),
filepath = os.path.join( directory, 'src', 'main.rs' ),
event_name = 'FileReadyToParse',
filetype = 'rust' ) )
WaitUntilCompleterServerReady( app, 'rust' )
Expand Down
4 changes: 2 additions & 2 deletions ycmd/tests/rust/debug_info_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def DebugInfo_RlsVersion_test( app ):
'value': instance_of( str )
} ),
has_entries( {
'key': 'RUSTC',
'key': 'Rust Root',
'value': instance_of( str )
} )
)
Expand Down Expand Up @@ -113,7 +113,7 @@ def DebugInfo_NoRlsVersion_test( get_command_output, app ):
'value': none()
} ),
has_entries( {
'key': 'RUSTC',
'key': 'Rust Root',
'value': instance_of( str )
} )
)
Expand Down
Loading

0 comments on commit c3581c3

Please sign in to comment.