Merge pull request #956 from mispencer/CsharpErrorDisplay
Support for diagnostic for C#
This commit is contained in:
commit
7aa68d93b6
@ -31,6 +31,7 @@ let s:previous_num_chars_on_current_line = -1
|
|||||||
|
|
||||||
let s:diagnostic_ui_filetypes = {
|
let s:diagnostic_ui_filetypes = {
|
||||||
\ 'cpp': 1,
|
\ 'cpp': 1,
|
||||||
|
\ 'cs': 1,
|
||||||
\ 'c': 1,
|
\ 'c': 1,
|
||||||
\ 'objc': 1,
|
\ 'objc': 1,
|
||||||
\ 'objcpp': 1,
|
\ 'objcpp': 1,
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
|
# along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
import os
|
import os
|
||||||
import glob
|
import glob
|
||||||
from ycm.completers.completer import Completer
|
from ycm.completers.completer import Completer
|
||||||
@ -32,8 +33,36 @@ import logging
|
|||||||
SERVER_NOT_FOUND_MSG = ( 'OmniSharp server binary not found at {0}. ' +
|
SERVER_NOT_FOUND_MSG = ( 'OmniSharp server binary not found at {0}. ' +
|
||||||
'Did you compile it? You can do so by running ' +
|
'Did you compile it? You can do so by running ' +
|
||||||
'"./install.sh --omnisharp-completer".' )
|
'"./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 ):
|
class CsharpCompleter( Completer ):
|
||||||
"""
|
"""
|
||||||
A Completer that uses the Omnisharp server as completion engine.
|
A Completer that uses the Omnisharp server as completion engine.
|
||||||
@ -55,6 +84,9 @@ class CsharpCompleter( Completer ):
|
|||||||
super( CsharpCompleter, self ).__init__( user_options )
|
super( CsharpCompleter, self ).__init__( user_options )
|
||||||
self._omnisharp_port = None
|
self._omnisharp_port = None
|
||||||
self._logger = logging.getLogger( __name__ )
|
self._logger = logging.getLogger( __name__ )
|
||||||
|
self._diagnostic_store = None
|
||||||
|
self._max_diagnostics_to_display = user_options[
|
||||||
|
'max_diagnostics_to_display' ]
|
||||||
|
|
||||||
|
|
||||||
def Shutdown( self ):
|
def Shutdown( self ):
|
||||||
@ -84,6 +116,59 @@ class CsharpCompleter( Completer ):
|
|||||||
if ( not self._omnisharp_port and
|
if ( not self._omnisharp_port and
|
||||||
self.user_options[ 'auto_start_csharp_server' ] ):
|
self.user_options[ 'auto_start_csharp_server' ] ):
|
||||||
self._StartServer( request_data )
|
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 ):
|
def OnUserCommand( self, arguments, request_data ):
|
||||||
@ -283,3 +368,9 @@ def _PathComponents( path ):
|
|||||||
def _GetFilenameWithoutExtension( path ):
|
def _GetFilenameWithoutExtension( path ):
|
||||||
return os.path.splitext( os.path.basename ( path ) )[ 0 ]
|
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
|
||||||
|
@ -19,7 +19,8 @@
|
|||||||
|
|
||||||
from ..server_utils import SetUpPythonPath
|
from ..server_utils import SetUpPythonPath
|
||||||
SetUpPythonPath()
|
SetUpPythonPath()
|
||||||
from .test_utils import Setup, BuildRequest
|
import time
|
||||||
|
from .test_utils import Setup, BuildRequest, PathToTestFile
|
||||||
from webtest import TestApp
|
from webtest import TestApp
|
||||||
from nose.tools import with_setup, eq_
|
from nose.tools import with_setup, eq_
|
||||||
from hamcrest import ( assert_that, contains, contains_string, has_entries,
|
from hamcrest import ( assert_that, contains, contains_string, has_entries,
|
||||||
@ -136,6 +137,56 @@ struct Foo {
|
|||||||
assert_that( response.body, empty() )
|
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 )
|
@with_setup( Setup )
|
||||||
def GetDetailedDiagnostic_ClangCompleter_Works_test():
|
def GetDetailedDiagnostic_ClangCompleter_Works_test():
|
||||||
app = TestApp( handlers.app )
|
app = TestApp( handlers.app )
|
||||||
@ -164,6 +215,49 @@ struct Foo {
|
|||||||
has_entry( 'message', contains_string( "expected ';'" ) ) )
|
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 )
|
@with_setup( Setup )
|
||||||
def GetDetailedDiagnostic_JediCompleter_DoesntWork_test():
|
def GetDetailedDiagnostic_JediCompleter_DoesntWork_test():
|
||||||
app = TestApp( handlers.app )
|
app = TestApp( handlers.app )
|
||||||
|
Loading…
Reference in New Issue
Block a user