From 465019b19746351b065823963550b3d67d73d335 Mon Sep 17 00:00:00 2001 From: "Spencer G. Jones" Date: Fri, 14 Feb 2014 11:15:07 -0700 Subject: [PATCH] Support for SyntaxErrors in CSharp completer --- autoload/youcompleteme.vim | 1 + python/ycm/completers/cs/cs_completer.py | 91 +++++++++++++++++++ python/ycm/server/tests/diagnostics_test.py | 96 ++++++++++++++++++++- 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/autoload/youcompleteme.vim b/autoload/youcompleteme.vim index 160b33e6..77ce429a 100644 --- a/autoload/youcompleteme.vim +++ b/autoload/youcompleteme.vim @@ -31,6 +31,7 @@ let s:previous_num_chars_on_current_line = -1 let s:diagnostic_ui_filetypes = { \ 'cpp': 1, + \ 'cs': 1, \ 'c': 1, \ 'objc': 1, \ 'objcpp': 1, diff --git a/python/ycm/completers/cs/cs_completer.py b/python/ycm/completers/cs/cs_completer.py index c2b9aaa7..ad1c45d2 100755 --- a/python/ycm/completers/cs/cs_completer.py +++ b/python/ycm/completers/cs/cs_completer.py @@ -18,6 +18,7 @@ # You should have received a copy of the GNU General Public License # along with YouCompleteMe. If not, see . +from collections import defaultdict import os import glob from ycm.completers.completer import Completer @@ -32,8 +33,36 @@ import logging SERVER_NOT_FOUND_MSG = ( 'OmniSharp server binary not found at {0}. ' + 'Did you compile it? You can do so by running ' + '"./install.sh --omnisharp-completer".' ) +MIN_LINES_IN_FILE_TO_PARSE = 5 +INVALID_FILE_MESSAGE = 'File is invalid.' +FILE_TOO_SHORT_MESSAGE = ( + 'File is less than {0} lines long; not parsing.'.format( + MIN_LINES_IN_FILE_TO_PARSE ) ) +NO_DIAGNOSTIC_MESSAGE = 'No diagnostic for current line!' +#TODO: Handle this better than dummy classes +class CsharpDiagnostic: + def __init__ ( self, ranges, location, location_extent, text, kind ): + self.ranges_ = ranges + self.location_ = location + self.location_extent_ = location_extent + self.text_ = text + self.kind_ = kind + + +class CsharpDiagnosticRange: + def __init__ ( self, start, end ): + self.start_ = start + self.end_ = end + + +class CsharpDiagnosticLocation: + def __init__ ( self, line, column, filename ): + self.line_number_ = line + self.column_number_ = column + self.filename_ = filename + class CsharpCompleter( Completer ): """ A Completer that uses the Omnisharp server as completion engine. @@ -55,6 +84,9 @@ class CsharpCompleter( Completer ): super( CsharpCompleter, self ).__init__( user_options ) self._omnisharp_port = None self._logger = logging.getLogger( __name__ ) + self._diagnostic_store = None + self._max_diagnostics_to_display = user_options[ + 'max_diagnostics_to_display' ] def Shutdown( self ): @@ -84,6 +116,59 @@ class CsharpCompleter( Completer ): if ( not self._omnisharp_port and self.user_options[ 'auto_start_csharp_server' ] ): self._StartServer( request_data ) + return + + filename = request_data[ 'filepath' ] + contents = request_data[ 'file_data' ][ filename ][ 'contents' ] + if contents.count( '\n' ) < MIN_LINES_IN_FILE_TO_PARSE: + raise ValueError( FILE_TOO_SHORT_MESSAGE ) + + if not filename: + raise ValueError( INVALID_FILE_MESSAGE ) + + syntax_errors = self._GetResponse( '/syntaxerrors', + self._DefaultParameters( request_data ) ) + + diagnostics = [ self._SyntaxErrorToDiagnostic( x ) for x in + syntax_errors[ "Errors" ] ] + + self._diagnostic_store = DiagnosticsToDiagStructure( diagnostics ) + + return [ responses.BuildDiagnosticData( x ) for x in + diagnostics[ : self._max_diagnostics_to_display ] ] + + + def _SyntaxErrorToDiagnostic( self, syntax_error ): + filename = syntax_error[ "FileName" ] + + location = CsharpDiagnosticLocation( syntax_error[ "Line" ], syntax_error[ "Column" ], filename ) + location_range = CsharpDiagnosticRange( location, location ) + return CsharpDiagnostic( list(), location, location_range, syntax_error[ "Message" ], "E" ) + + + def GetDetailedDiagnostic( self, request_data ): + current_line = request_data[ 'line_num' ] + 1 + current_column = request_data[ 'column_num' ] + 1 + current_file = request_data[ 'filepath' ] + + if not self._diagnostic_store: + raise ValueError( NO_DIAGNOSTIC_MESSAGE ) + + diagnostics = self._diagnostic_store[ current_file ][ current_line ] + if not diagnostics: + raise ValueError( NO_DIAGNOSTIC_MESSAGE ) + + closest_diagnostic = None + distance_to_closest_diagnostic = 999 + + for diagnostic in diagnostics: + distance = abs( current_column - diagnostic.location_.column_number_ ) + if distance < distance_to_closest_diagnostic: + distance_to_closest_diagnostic = distance + closest_diagnostic = diagnostic + + return responses.BuildDisplayMessageResponse( + closest_diagnostic.text_ ) def OnUserCommand( self, arguments, request_data ): @@ -283,3 +368,9 @@ def _PathComponents( path ): def _GetFilenameWithoutExtension( path ): return os.path.splitext( os.path.basename ( path ) )[ 0 ] +def DiagnosticsToDiagStructure( diagnostics ): + structure = defaultdict( lambda : defaultdict( list ) ) + for diagnostic in diagnostics: + structure[ diagnostic.location_.filename_ ][ + diagnostic.location_.line_number_ ].append( diagnostic ) + return structure diff --git a/python/ycm/server/tests/diagnostics_test.py b/python/ycm/server/tests/diagnostics_test.py index 43384f2d..1af3c202 100644 --- a/python/ycm/server/tests/diagnostics_test.py +++ b/python/ycm/server/tests/diagnostics_test.py @@ -19,7 +19,8 @@ from ..server_utils import SetUpPythonPath SetUpPythonPath() -from .test_utils import Setup, BuildRequest +import time +from .test_utils import Setup, BuildRequest, PathToTestFile from webtest import TestApp from nose.tools import with_setup, eq_ from hamcrest import ( assert_that, contains, contains_string, has_entries, @@ -136,6 +137,56 @@ struct Foo { assert_that( response.body, empty() ) +@with_setup( Setup ) +def Diagnostics_CsCompleter_ZeroBasedLineAndColumn_test(): + app = TestApp( handlers.app ) + filepath = PathToTestFile( 'testy/Program.cs' ) + contents = open( filepath ).read() + event_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + event_name = 'FileReadyToParse' ) + + results = app.post_json( '/event_notification', event_data ) + + # We need to wait until the server has started up. + while True: + result = app.post_json( '/run_completer_command', + BuildRequest( completer_target = 'filetype_default', + command_arguments = ['ServerReady'], + filetype = 'cs' ) ).json + if result: + break + time.sleep( 0.2 ) + + event_data = BuildRequest( filepath = filepath, + event_name = 'FileReadyToParse', + filetype = 'cs', + contents = contents ) + + results = app.post_json( '/event_notification', event_data ).json + + assert_that( results, + contains( + has_entries( { + 'text': contains_string( "Unexpected symbol `}'', expecting identifier" ), + 'location': has_entries( { + 'line_num': 9, + 'column_num': 1 + } ), + 'location_extent': has_entries( { + 'start': has_entries( { + 'line_num': 9, + 'column_num': 1, + } ), + 'end': has_entries( { + 'line_num': 9, + 'column_num': 1, + } ), + } ) + } ) ) ) + + @with_setup( Setup ) def GetDetailedDiagnostic_ClangCompleter_Works_test(): app = TestApp( handlers.app ) @@ -164,6 +215,49 @@ struct Foo { has_entry( 'message', contains_string( "expected ';'" ) ) ) +@with_setup( Setup ) +def GetDetailedDiagnostic_CsCompleter_Works_test(): + app = TestApp( handlers.app ) + filepath = PathToTestFile( 'testy/Program.cs' ) + contents = open( filepath ).read() + event_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + event_name = 'FileReadyToParse' ) + + app.post_json( '/event_notification', event_data ) + + # We need to wait until the server has started up. + while True: + result = app.post_json( '/run_completer_command', + BuildRequest( completer_target = 'filetype_default', + command_arguments = ['ServerReady'], + filetype = 'cs' ) ).json + if result: + break + time.sleep( 0.2 ) + + app.post_json( '/event_notification', event_data ) + + diag_data = BuildRequest( filepath = filepath, + filetype = 'cs', + contents = contents, + line_num = 9, + column_num = 1, + start_column = 1 ) + + results = app.post_json( '/detailed_diagnostic', diag_data ).json + assert_that( results, + has_entry( 'message', contains_string( "Unexpected symbol `}'', expecting identifier" ) ) ) + + + # We need to turn off the CS server so that it doesn't stick around + app.post_json( '/run_completer_command', + BuildRequest( completer_target = 'filetype_default', + command_arguments = ['StopServer'], + filetype = 'cs' ) ) + + @with_setup( Setup ) def GetDetailedDiagnostic_JediCompleter_DoesntWork_test(): app = TestApp( handlers.app )