diagnostic_interface.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # Copyright (C) 2013-2018 YouCompleteMe contributors
  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 collections import defaultdict
  18. from ycm import vimsupport
  19. from ycm.diagnostic_filter import DiagnosticFilter, CompileLevel
  20. class DiagnosticInterface:
  21. def __init__( self, bufnr, user_options ):
  22. self._bufnr = bufnr
  23. self._user_options = user_options
  24. self._diagnostics = []
  25. self._diag_filter = DiagnosticFilter.CreateFromOptions( user_options )
  26. # Line and column numbers are 1-based
  27. self._line_to_diags = defaultdict( list )
  28. self._previous_diag_line_number = -1
  29. self._diag_message_needs_clearing = False
  30. def OnCursorMoved( self ):
  31. if self._user_options[ 'echo_current_diagnostic' ]:
  32. line, _ = vimsupport.CurrentLineAndColumn()
  33. line += 1 # Convert to 1-based
  34. if line != self._previous_diag_line_number:
  35. self._EchoDiagnosticForLine( line )
  36. def GetErrorCount( self ):
  37. return self._DiagnosticsCount( _DiagnosticIsError )
  38. def GetWarningCount( self ):
  39. return self._DiagnosticsCount( _DiagnosticIsWarning )
  40. def PopulateLocationList( self ):
  41. # Do nothing if loc list is already populated by diag_interface
  42. if not self._user_options[ 'always_populate_location_list' ]:
  43. self._UpdateLocationLists()
  44. return bool( self._diagnostics )
  45. def UpdateWithNewDiagnostics( self, diags ):
  46. self._diagnostics = [ _NormalizeDiagnostic( x ) for x in
  47. self._ApplyDiagnosticFilter( diags ) ]
  48. self._ConvertDiagListToDict()
  49. if self._user_options[ 'echo_current_diagnostic' ]:
  50. self._EchoDiagnostic()
  51. if self._user_options[ 'enable_diagnostic_signs' ]:
  52. self._UpdateSigns()
  53. self.UpdateMatches()
  54. if self._user_options[ 'always_populate_location_list' ]:
  55. self._UpdateLocationLists()
  56. def _ApplyDiagnosticFilter( self, diags ):
  57. filetypes = vimsupport.GetBufferFiletypes( self._bufnr )
  58. diag_filter = self._diag_filter.SubsetForTypes( filetypes )
  59. return filter( diag_filter.IsAllowed, diags )
  60. def _EchoDiagnostic( self ):
  61. line, _ = vimsupport.CurrentLineAndColumn()
  62. line += 1 # Convert to 1-based
  63. self._EchoDiagnosticForLine( line )
  64. def _EchoDiagnosticForLine( self, line_num ):
  65. self._previous_diag_line_number = line_num
  66. diags = self._line_to_diags[ line_num ]
  67. if not diags:
  68. if self._diag_message_needs_clearing:
  69. # Clear any previous diag echo
  70. vimsupport.PostVimMessage( '', warning = False )
  71. self._diag_message_needs_clearing = False
  72. return
  73. first_diag = diags[ 0 ]
  74. text = first_diag[ 'text' ]
  75. if first_diag.get( 'fixit_available', False ):
  76. text += ' (FixIt)'
  77. vimsupport.PostVimMessage( text, warning = False, truncate = True )
  78. self._diag_message_needs_clearing = True
  79. def _DiagnosticsCount( self, predicate ):
  80. count = 0
  81. for diags in self._line_to_diags.values():
  82. count += sum( 1 for d in diags if predicate( d ) )
  83. return count
  84. def _UpdateLocationLists( self ):
  85. vimsupport.SetLocationListsForBuffer(
  86. self._bufnr,
  87. vimsupport.ConvertDiagnosticsToQfList( self._diagnostics ) )
  88. def UpdateMatches( self ):
  89. if not self._user_options[ 'enable_diagnostic_highlighting' ]:
  90. return
  91. # Vim doesn't provide a way to update the matches for a different window
  92. # than the current one (which is a view of the current buffer).
  93. if vimsupport.GetCurrentBufferNumber() != self._bufnr:
  94. return
  95. matches_to_remove = vimsupport.GetDiagnosticMatchesInCurrentWindow()
  96. for diags in self._line_to_diags.values():
  97. # Insert squiggles in reverse order so that errors overlap warnings.
  98. for diag in reversed( diags ):
  99. group = ( 'YcmErrorSection' if _DiagnosticIsError( diag ) else
  100. 'YcmWarningSection' )
  101. for pattern in _ConvertDiagnosticToMatchPatterns( diag ):
  102. # The id doesn't matter for matches that we may add.
  103. match = vimsupport.DiagnosticMatch( 0, group, pattern )
  104. try:
  105. matches_to_remove.remove( match )
  106. except ValueError:
  107. vimsupport.AddDiagnosticMatch( match )
  108. for match in matches_to_remove:
  109. vimsupport.RemoveDiagnosticMatch( match )
  110. def _UpdateSigns( self ):
  111. signs_to_unplace = vimsupport.GetSignsInBuffer( self._bufnr )
  112. for line, diags in self._line_to_diags.items():
  113. if not diags:
  114. continue
  115. # We always go for the first diagnostic on the line because diagnostics
  116. # are sorted by errors in priority and Vim can only display one sign by
  117. # line.
  118. name = 'YcmError' if _DiagnosticIsError( diags[ 0 ] ) else 'YcmWarning'
  119. sign = vimsupport.CreateSign( line, name, self._bufnr )
  120. try:
  121. signs_to_unplace.remove( sign )
  122. except ValueError:
  123. vimsupport.PlaceSign( sign )
  124. for sign in signs_to_unplace:
  125. vimsupport.UnplaceSign( sign )
  126. def _ConvertDiagListToDict( self ):
  127. self._line_to_diags = defaultdict( list )
  128. for diag in self._diagnostics:
  129. location = diag[ 'location' ]
  130. bufnr = vimsupport.GetBufferNumberForFilename( location[ 'filepath' ] )
  131. if bufnr == self._bufnr:
  132. line_number = location[ 'line_num' ]
  133. self._line_to_diags[ line_number ].append( diag )
  134. for diags in self._line_to_diags.values():
  135. # We also want errors to be listed before warnings so that errors aren't
  136. # hidden by the warnings; Vim won't place a sign over an existing one.
  137. diags.sort( key = lambda diag: ( diag[ 'kind' ],
  138. diag[ 'location' ][ 'column_num' ] ) )
  139. _DiagnosticIsError = CompileLevel( 'error' )
  140. _DiagnosticIsWarning = CompileLevel( 'warning' )
  141. def _NormalizeDiagnostic( diag ):
  142. def ClampToOne( value ):
  143. return value if value > 0 else 1
  144. location = diag[ 'location' ]
  145. location[ 'column_num' ] = ClampToOne( location[ 'column_num' ] )
  146. location[ 'line_num' ] = ClampToOne( location[ 'line_num' ] )
  147. return diag
  148. def _ConvertDiagnosticToMatchPatterns( diagnostic ):
  149. patterns = []
  150. location_extent = diagnostic[ 'location_extent' ]
  151. if location_extent[ 'start' ][ 'line_num' ] <= 0:
  152. location = diagnostic[ 'location' ]
  153. patterns.append( vimsupport.GetDiagnosticMatchPattern(
  154. location[ 'line_num' ],
  155. location[ 'column_num' ] ) )
  156. else:
  157. patterns.append( vimsupport.GetDiagnosticMatchPattern(
  158. location_extent[ 'start' ][ 'line_num' ],
  159. location_extent[ 'start' ][ 'column_num' ],
  160. location_extent[ 'end' ][ 'line_num' ],
  161. location_extent[ 'end' ][ 'column_num' ] ) )
  162. for diagnostic_range in diagnostic[ 'ranges' ]:
  163. patterns.append( vimsupport.GetDiagnosticMatchPattern(
  164. diagnostic_range[ 'start' ][ 'line_num' ],
  165. diagnostic_range[ 'start' ][ 'column_num' ],
  166. diagnostic_range[ 'end' ][ 'line_num' ],
  167. diagnostic_range[ 'end' ][ 'column_num' ] ) )
  168. return patterns