diagnostic_interface.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. # Copyright (C) 2013 Google Inc.
  2. #
  3. # This file is part of YouCompleteMe.
  4. #
  5. # YouCompleteMe is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # YouCompleteMe is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with YouCompleteMe. If not, see <http://www.gnu.org/licenses/>.
  17. from __future__ import unicode_literals
  18. from __future__ import print_function
  19. from __future__ import division
  20. from __future__ import absolute_import
  21. # Not installing aliases from python-future; it's unreliable and slow.
  22. from builtins import * # noqa
  23. from future.utils import itervalues, iteritems
  24. from collections import defaultdict
  25. from ycm import vimsupport
  26. from ycm.diagnostic_filter import DiagnosticFilter, CompileLevel
  27. class DiagnosticInterface( object ):
  28. def __init__( self, bufnr, user_options ):
  29. self._bufnr = bufnr
  30. self._user_options = user_options
  31. self._diagnostics = []
  32. self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
  33. # Line and column numbers are 1-based
  34. self._line_to_diags = defaultdict( list )
  35. self._next_sign_id = vimsupport.SIGN_BUFFER_ID_INITIAL_VALUE
  36. self._previous_diag_line_number = -1
  37. self._diag_message_needs_clearing = False
  38. def OnCursorMoved( self ):
  39. if self._user_options[ 'echo_current_diagnostic' ]:
  40. line, _ = vimsupport.CurrentLineAndColumn()
  41. line += 1 # Convert to 1-based
  42. if line != self._previous_diag_line_number:
  43. self._EchoDiagnosticForLine( line )
  44. def GetErrorCount( self ):
  45. return self._DiagnosticsCount( _DiagnosticIsError )
  46. def GetWarningCount( self ):
  47. return self._DiagnosticsCount( _DiagnosticIsWarning )
  48. def PopulateLocationList( self ):
  49. # Do nothing if loc list is already populated by diag_interface
  50. if not self._user_options[ 'always_populate_location_list' ]:
  51. self._UpdateLocationList()
  52. return bool( self._diagnostics )
  53. def UpdateWithNewDiagnostics( self, diags ):
  54. self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
  55. self._ApplyDiagnosticFilter( diags ) ]
  56. self._ConvertDiagListToDict()
  57. if self._user_options[ 'echo_current_diagnostic' ]:
  58. self._EchoDiagnostic()
  59. if self._user_options[ 'enable_diagnostic_signs' ]:
  60. self._UpdateSigns()
  61. if self._user_options[ 'enable_diagnostic_highlighting' ]:
  62. self._UpdateSquiggles()
  63. if self._user_options[ 'always_populate_location_list' ]:
  64. self._UpdateLocationList()
  65. def _ApplyDiagnosticFilter( self, diags ):
  66. filetypes = vimsupport.GetBufferFiletypes( self._bufnr )
  67. diag_filter = self._diag_filter.SubsetForTypes( filetypes )
  68. return filter( diag_filter.IsAllowed, diags )
  69. def _EchoDiagnostic( self ):
  70. line, _ = vimsupport.CurrentLineAndColumn()
  71. line += 1 # Convert to 1-based
  72. self._EchoDiagnosticForLine( line )
  73. def _EchoDiagnosticForLine( self, line_num ):
  74. self._previous_diag_line_number = line_num
  75. diags = self._line_to_diags[ line_num ]
  76. if not diags:
  77. if self._diag_message_needs_clearing:
  78. # Clear any previous diag echo
  79. vimsupport.PostVimMessage( '', warning = False )
  80. self._diag_message_needs_clearing = False
  81. return
  82. first_diag = diags[ 0 ]
  83. text = first_diag[ 'text' ]
  84. if first_diag.get( 'fixit_available', False ):
  85. text += ' (FixIt)'
  86. vimsupport.PostVimMessage( text, warning = False, truncate = True )
  87. self._diag_message_needs_clearing = True
  88. def _DiagnosticsCount( self, predicate ):
  89. count = 0
  90. for diags in itervalues( self._line_to_diags ):
  91. count += sum( 1 for d in diags if predicate( d ) )
  92. return count
  93. def _UpdateLocationList( self ):
  94. vimsupport.SetLocationListForBuffer(
  95. self._bufnr,
  96. vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )
  97. def _UpdateSquiggles( self ):
  98. vimsupport.ClearYcmSyntaxMatches()
  99. for diags in itervalues( self._line_to_diags ):
  100. # Insert squiggles in reverse order so that errors overlap warnings.
  101. for diag in reversed( diags ):
  102. location_extent = diag[ 'location_extent' ]
  103. is_error = _DiagnosticIsError( diag )
  104. if location_extent[ 'start' ][ 'line_num' ] <= 0:
  105. location = diag[ 'location' ]
  106. vimsupport.AddDiagnosticSyntaxMatch(
  107. location[ 'line_num' ],
  108. location[ 'column_num' ],
  109. is_error = is_error )
  110. else:
  111. vimsupport.AddDiagnosticSyntaxMatch(
  112. location_extent[ 'start' ][ 'line_num' ],
  113. location_extent[ 'start' ][ 'column_num' ],
  114. location_extent[ 'end' ][ 'line_num' ],
  115. location_extent[ 'end' ][ 'column_num' ],
  116. is_error = is_error )
  117. for diag_range in diag[ 'ranges' ]:
  118. vimsupport.AddDiagnosticSyntaxMatch(
  119. diag_range[ 'start' ][ 'line_num' ],
  120. diag_range[ 'start' ][ 'column_num' ],
  121. diag_range[ 'end' ][ 'line_num' ],
  122. diag_range[ 'end' ][ 'column_num' ],
  123. is_error = is_error )
  124. def _UpdateSigns( self ):
  125. signs_to_unplace = vimsupport.GetSignsInBuffer( self._bufnr )
  126. for line, diags in iteritems( self._line_to_diags ):
  127. if not diags:
  128. continue
  129. # We always go for the first diagnostic on the line because diagnostics
  130. # are sorted by errors in priority and Vim can only display one sign by
  131. # line.
  132. name = 'YcmError' if _DiagnosticIsError( diags[ 0 ] ) else 'YcmWarning'
  133. sign = vimsupport.DiagnosticSign( self._next_sign_id,
  134. line,
  135. name,
  136. self._bufnr )
  137. try:
  138. signs_to_unplace.remove( sign )
  139. except ValueError:
  140. vimsupport.PlaceSign( sign )
  141. self._next_sign_id += 1
  142. for sign in signs_to_unplace:
  143. vimsupport.UnplaceSign( sign )
  144. def _ConvertDiagListToDict( self ):
  145. self._line_to_diags = defaultdict( list )
  146. for diag in self._diagnostics:
  147. location = diag[ 'location' ]
  148. bufnr = vimsupport.GetBufferNumberForFilename( location[ 'filepath' ] )
  149. if bufnr == self._bufnr:
  150. line_number = location[ 'line_num' ]
  151. self._line_to_diags[ line_number ].append( diag )
  152. for diags in itervalues( self._line_to_diags ):
  153. # We also want errors to be listed before warnings so that errors aren't
  154. # hidden by the warnings; Vim won't place a sign over an existing one.
  155. diags.sort( key = lambda diag: ( diag[ 'kind' ],
  156. diag[ 'location' ][ 'column_num' ] ) )
  157. _DiagnosticIsError = CompileLevel( 'error' )
  158. _DiagnosticIsWarning = CompileLevel( 'warning' )
  159. def _NormalizeDiagnostic( diag ):
  160. def ClampToOne( value ):
  161. return value if value > 0 else 1
  162. location = diag[ 'location' ]
  163. location[ 'column_num' ] = ClampToOne( location[ 'column_num' ] )
  164. location[ 'line_num' ] = ClampToOne( location[ 'line_num' ] )
  165. return diag